diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000..e33d426873 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,8 @@ +--- +Checks: "-*, + boost-*, + portability-*, +" +WarningsAsErrors: '' +HeaderFilterRegex: '' +FormatStyle: none diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000000..1ce10fa6cd --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,87 @@ +name: CMake + +on: + pull_request: + branches: [ master ] + +env: + BUILD_TYPE: RelWithDebInfo + +jobs: + Ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Add OpenMW PPA Dependancies + run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update + + - name: Install Building Dependancies + 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: Install gtest + run: | + export CONFIGURATION="Release" + export GOOGLETEST_DIR="." + export GENERATOR="Unix Makefiles" + export CC="gcc" + export CXX="g++" + sudo -E CI/build_googletest.sh + + - name: Configure + run: cmake -S . -B . -DGTEST_ROOT="$(pwd)/googletest/build" -DGMOCK_ROOT="$(pwd)/googletest/build" -DBUILD_UNITTESTS=ON -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS='-Werror' -DCMAKE_CXX_FLAGS="-Werror -Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" + + - name: Build + run: cmake --build . --config ${{env.BUILD_TYPE}} --parallel 3 + + - name: Test + run: ./openmw_test_suite + +# - 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 + + - name: Install Building Dependancies + 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: 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 diff --git a/.gitignore b/.gitignore index 1a164592a1..cc9cb3e116 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ CMakeCache.txt cmake_install.cmake Makefile makefile -build* +build*/ prebuilt ##windows build process @@ -34,7 +34,6 @@ CMakeLists.txt.user* .vscode ## resources -data resources /*.cfg /*.desktop @@ -72,6 +71,7 @@ components/ui_contentselector.h docs/mainpage.hpp docs/Doxyfile docs/DoxyfilePages +docs/source/reference/lua-scripting/generated_html moc_*.cxx *.cxx_parameters *qrc_launcher.cxx @@ -84,13 +84,3 @@ moc_*.cxx *.[ao] *.so venv/ - -## recastnavigation unused files -extern/recastnavigation/.travis.yml -extern/recastnavigation/CONTRIBUTING.md -extern/recastnavigation/Docs/ -extern/recastnavigation/Doxyfile -extern/recastnavigation/README.md -extern/recastnavigation/RecastDemo/ -extern/recastnavigation/Tests/ -extern/recastnavigation/appveyor.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 25a04d536e..9d816970ba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,130 +1,402 @@ +default: + interruptible: true + +# Note: We set `needs` on each job to control the job DAG. +# See https://docs.gitlab.com/ee/ci/yaml/#needs stages: - build + - test + +# https://blog.nimbleways.com/let-s-make-faster-gitlab-ci-cd-pipelines/ +variables: + FF_USE_NEW_SHELL_ESCAPE: "true" + FF_USE_FASTZIP: "true" + # These can be specified per job or per pipeline + ARTIFACT_COMPRESSION_LEVEL: "fast" + CACHE_COMPRESSION_LEVEL: "fast" -.Debian: +.Ubuntu_Image: tags: - docker - linux - image: debian:bullseye + image: ubuntu:focal + rules: + - if: $CI_PIPELINE_SOURCE == "push" + +.Ubuntu: + extends: .Ubuntu_Image cache: paths: - apt-cache/ - ccache/ - before_script: - - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - - apt-get update -yq - - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake build-essential libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt5opengl5-dev libopenal-dev libopenscenegraph-dev libunshield-dev libtinyxml-dev libmygui-dev libbullet-dev liblz4-dev ccache git clang stage: build script: + - df -h - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh - cd build - cmake --build . -- -j $(nproc) + - df -h + - du -sh . + - find . | grep '\.o$' | xargs rm -f + - df -h + - du -sh . - cmake --install . - - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite --gtest_output="xml:tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_detournavigator_navmeshtilescache_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 '^tests.xml$' | xargs -I '{}' rm -rf './{}' + - cd .. + - df -h + - du -sh build/ + - du -sh build/install/ + - du -sh apt-cache/ + - du -sh ccache/ artifacts: paths: - build/install/ -Debian_GCC: - extends: .Debian +Coverity: + extends: .Ubuntu_Image + stage: build + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + before_script: + - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic + - curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN + - tar xfz /tmp/cov-analysis-linux64.tgz + script: + - CI/before_script.linux.sh + # Remove the specific targets and build everything once we can do it under 3h + - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw esmtool bsatool niftest openmw-wizard openmw-launcher openmw-iniimporter openmw-essimporter openmw-navmeshtool openmw-cs + after_script: + - tar cfz cov-int.tar.gz cov-int + - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME + --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL + --form file=@cov-int.tar.gz --form version="$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA" + --form description="CI_COMMIT_SHORT_SHA / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" + variables: + CC: clang + CXX: clang++ + CXXFLAGS: -O0 + artifacts: + paths: + - /builds/OpenMW/openmw/cov-int/build-log.txt + +Ubuntu_GCC: + extends: .Ubuntu cache: - key: Debian_GCC.v2 + key: Ubuntu_GCC.v3 + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: CC: gcc CXX: g++ - CCACHE_SIZE: 3G + CCACHE_SIZE: 4G + # 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 -Debian_GCC_tests: - extends: .Debian +.Ubuntu_GCC_tests: + extends: Ubuntu_GCC cache: - key: Debian_GCC_tests.v2 + key: Ubuntu_GCC_tests.v3 variables: - CC: gcc - CXX: g++ CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml -Debian_Clang: - extends: .Debian +.Ubuntu_GCC_tests_Debug: + extends: Ubuntu_GCC cache: - key: Debian_Clang.v2 + key: Ubuntu_GCC_tests_Debug.v2 variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O0 -D_GLIBCXX_ASSERTIONS + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +.Ubuntu_GCC_tests_asan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_asan.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=leak + CMAKE_EXE_LINKER_FLAGS: -fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=leak + ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +.Ubuntu_GCC_tests_ubsan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_ubsan.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O0 -fsanitize=undefined + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +.Ubuntu_GCC_tests_tsan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_tsan.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=thread -fPIE + CMAKE_EXE_LINKER_FLAGS: -pthread -pie -fsanitize=thread + TSAN_OPTIONS: second_deadlock_stack=1:halt_on_error=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +.Ubuntu_GCC_tests_coverage: + extends: .Ubuntu_GCC_tests_Debug + cache: + key: Ubuntu_GCC_tests_coverage.v1 + variables: + BUILD_WITH_CODE_COVERAGE: 1 + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage + coverage: /^\s*lines:\s*\d+.\d+\%/ + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + junit: build/tests.xml + +.Ubuntu_Static_Deps: + extends: Ubuntu_Clang + rules: + - if: $CI_PIPELINE_SOURCE == "push" + changes: + - "**/CMakeLists.txt" + - "cmake/**/*" + - "CI/**/*" + - ".gitlab-ci.yml" + cache: + key: Ubuntu_Static_Deps.V1 + paths: + - apt-cache/ + - ccache/ + - build/extern/fetched/ + before_script: + - CI/install_debian_deps.sh clang openmw-deps openmw-deps-static + variables: + CI_OPENMW_USE_STATIC_DEPS: 1 CC: clang CXX: clang++ - CCACHE_SIZE: 2G + CXXFLAGS: -O0 + timeout: 3h -Debian_Clang_tests: - extends: .Debian +.Ubuntu_Static_Deps_tests: + extends: .Ubuntu_Static_Deps cache: - key: Debian_Clang_tests.v2 + key: Ubuntu_Static_Deps_tests.V1 variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 CC: clang CXX: clang++ + CXXFLAGS: -O0 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +Ubuntu_Clang: + extends: .Ubuntu + before_script: + - CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic + cache: + key: Ubuntu_Clang.v2 + variables: + CC: clang + CXX: clang++ + CI_CLANG_TIDY: 1 + CCACHE_SIZE: 2G + # 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 + +.Ubuntu_Clang_tests: + extends: Ubuntu_Clang + cache: + key: Ubuntu_Clang_tests.v3 + variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml -MacOS: +Ubuntu_Clang_tests_Debug: + extends: Ubuntu_Clang + cache: + key: Ubuntu_Clang_tests_Debug.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/tests.xml + +Ubuntu_Clang_integration_tests: + extends: .Ubuntu_Image + stage: test + needs: + - Ubuntu_Clang + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: Ubuntu_Clang_integration_tests.v1 + paths: + - .cache/pip + - apt-cache/ + before_script: + - CI/install_debian_deps.sh openmw-integration-tests + - pip3 install --user numpy matplotlib termtables click + script: + - CI/run_integration_tests.sh + +.MacOS: + image: macos-11-xcode-12 tags: - - macos + - shared-macos-amd64 stage: build only: variables: - $CI_PROJECT_ID == "7107382" + cache: + paths: + - ccache/ script: - - rm -fr build/* # remove anything in the build directory + - rm -fr build # remove the build directory - CI/before_install.osx.sh + - export CCACHE_BASEDIR="$(pwd)" + - export CCACHE_DIR="$(pwd)/ccache" + - mkdir -pv "${CCACHE_DIR}" + - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.osx.sh - - cd build; make -j2 package - - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.dmg"; done + - 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 + - ccache -s artifacts: paths: - build/OpenMW-*.dmg - "build/**/*.log" -variables: &engine-targets - targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" - -variables: &cs-targets - targets: "openmw-cs,bsatool,esmtool,niftest" +macOS12_Xcode13: + extends: .MacOS + image: macos-12-xcode-13 + cache: + key: macOS12_Xcode13.v1 + variables: + CCACHE_SIZE: 3G .Windows_Ninja_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 + - choco source disable -n=chocolatey - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y + - choco install ccache -y - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install ninja -y - choco install python -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N + - $env:CCACHE_BASEDIR = Get-Location + - $env:CCACHE_DIR = "$(Get-Location)\ccache" + - New-Item -Type Directory -Force -Path $env:CCACHE_DIR + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t -C $multiview -E - cd MSVC2019_64_Ninja - .\ActivateMSVC.ps1 - - cmake --build . --config $config --target ($targets.Split(',')) + - cmake --build . --config $config + - ccache --show-stats - 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 + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-v2 + key: ninja-v3 paths: + - ccache - deps - MSVC2019_64_Ninja/deps/Qt artifacts: @@ -140,81 +412,92 @@ variables: &cs-targets - MSVC2019_64_Ninja/*/*/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log + # 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 -Windows_Ninja_Engine_Release: +.Windows_Ninja_Release: extends: - .Windows_Ninja_Base variables: - <<: *engine-targets config: "Release" -Windows_Ninja_Engine_Debug: +.Windows_Ninja_Release_MultiView: extends: - .Windows_Ninja_Base variables: - <<: *engine-targets - config: "Debug" - -Windows_Ninja_Engine_RelWithDebInfo: - extends: - - .Windows_Ninja_Base - variables: - <<: *engine-targets - config: "RelWithDebInfo" - -Windows_Ninja_CS_Release: - extends: - - .Windows_Ninja_Base - variables: - <<: *cs-targets + multiview: "-M" config: "Release" -Windows_Ninja_CS_Debug: +.Windows_Ninja_Debug: extends: - .Windows_Ninja_Base variables: - <<: *cs-targets config: "Debug" -Windows_Ninja_CS_RelWithDebInfo: +.Windows_Ninja_RelWithDebInfo: extends: - .Windows_Ninja_Base variables: - <<: *cs-targets config: "RelWithDebInfo" + # Gitlab can't successfully execute following binaries due to unknown reason + # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" .Windows_MSBuild_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 + - choco source disable -n=chocolatey - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y + - choco install ccache -y - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install python -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V + - $env:CCACHE_BASEDIR = Get-Location + - $env:CCACHE_DIR = "$(Get-Location)\ccache" + - New-Item -Type Directory -Force -Path $env:CCACHE_DIR + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t -C $multiview -E - cd MSVC2019_64 - - cmake --build . --config $config --target ($targets.Split(',')) + - cmake --build . --config $config + - ccache --show-stats - 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 + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-v2 + key: msbuild-v3 paths: + - ccache - deps - MSVC2019_64/deps/Qt artifacts: @@ -230,68 +513,59 @@ Windows_Ninja_CS_RelWithDebInfo: - MSVC2019_64/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*/*.log + # 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 -Windows_MSBuild_Engine_Release: +.Windows_MSBuild_Release: extends: - .Windows_MSBuild_Base variables: - <<: *engine-targets config: "Release" +# temporarily disabled while this isn't the thing we link 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" +# - if: $CI_PIPELINE_SOURCE == "schedule" -Windows_MSBuild_Engine_Debug: +.Windows_MSBuild_Debug: extends: - .Windows_MSBuild_Base variables: - <<: *engine-targets config: "Debug" -Windows_MSBuild_Engine_RelWithDebInfo: +Windows_MSBuild_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: - <<: *engine-targets config: "RelWithDebInfo" + # Gitlab can't successfully execute following binaries due to unknown reason + # executables: "openmw_test_suite.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" + - if: $CI_PIPELINE_SOURCE == "schedule" -Windows_MSBuild_CS_Release: - extends: - - .Windows_MSBuild_Base - variables: - <<: *cs-targets - config: "Release" -Windows_MSBuild_CS_Debug: - extends: - - .Windows_MSBuild_Base - variables: - <<: *cs-targets - config: "Debug" - -Windows_MSBuild_CS_RelWithDebInfo: - extends: - - .Windows_MSBuild_Base - variables: - <<: *cs-targets - config: "RelWithDebInfo" - -Debian_AndroidNDK_arm64-v8a: +Ubuntu_AndroidNDK_arm64-v8a: tags: - linux - image: debian:bullseye + image: psi29a/android-ndk:focal-ndk22 + rules: + - if: $CI_PIPELINE_SOURCE == "push" variables: CCACHE_SIZE: 3G cache: - key: Debian_AndroidNDK_arm64-v8a.v2 + key: Ubuntu__Focal_AndroidNDK_r22b_arm64-v8a.v2 paths: - apt-cache/ - ccache/ + - build/extern/fetched/ before_script: - - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - - echo "deb http://deb.debian.org/debian unstable main contrib" > /etc/apt/sources.list - - echo "google-android-ndk-installer google-android-installers/mirror select https://dl.google.com" | debconf-set-selections - - apt-get update -yq - - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer + - CI/install_debian_deps.sh gcc stage: build script: + - df -h - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" @@ -299,8 +573,35 @@ Debian_AndroidNDK_arm64-v8a: - CI/before_script.android.sh - cd build - cmake --build . -- -j $(nproc) - - cmake --install . + # - cmake --install . # no one uses builds anyway, disable until 'no space left' is resolved - ccache -s + - df -h + - ls | grep -v -e '^extern$' -e '^install$' | xargs -I '{}' rm -rf './{}' + - cd .. + - df -h + - du -sh build/ + # - du -sh build/install/ # no install dir because it's commented out above + - du -sh apt-cache/ + - du -sh ccache/ + - du -sh build/extern/fetched/ artifacts: paths: - build/install/ + # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. + timeout: 1h30m + +.FindMissingMergeRequests: + image: python:latest + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: FindMissingMergeRequests.v1 + paths: + - .cache/pip + before_script: + - pip3 install --user requests click discord_webhook + script: + - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..e0b39ec495 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + diff --git a/.resubmitted_merge_requests.txt b/.resubmitted_merge_requests.txt new file mode 100644 index 0000000000..1585a60ec1 --- /dev/null +++ b/.resubmitted_merge_requests.txt @@ -0,0 +1,8 @@ +1471 +1450 +1420 +1314 +1216 +1172 +1160 +1051 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2f457798a7..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,105 +0,0 @@ -language: cpp -branches: - only: - - master - - coverity_scan - - /openmw-.*$/ -env: - global: - # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created - # via the "travis encrypt" command using the project repo's public key - - secure: "jybGzAdUbqt9vWR/GEnRd96BgAi/7Zd1+2HK68j/i/8+/1YH2XxLOy4Jv/DUBhBlJIkxs/Xv8dRcUlFOclZDHX1d/9Qnsqd3oUVkD7k1y7cTOWy9TBQaE/v/kZo3LpzA3xPwwthrb0BvqIbOfIELi5fS5s8ba85WFRg3AX70wWE=" -cache: ccache -addons: - apt: - sources: - - sourceline: 'ppa:openmw/openmw' - # - ubuntu-toolchain-r-test # for GCC-10 - packages: [ - # Dev - build-essential, cmake, clang-tools, ccache, - # Boost - libboost-filesystem-dev, libboost-iostreams-dev, libboost-program-options-dev, libboost-system-dev, - # FFmpeg - libavcodec-dev, libavformat-dev, libavutil-dev, libswresample-dev, libswscale-dev, - # Audio, Video and Misc. deps - libsdl2-dev, libqt5opengl5-dev, libopenal-dev, libunshield-dev, libtinyxml-dev, liblz4-dev - # The other ones from OpenMW ppa - libbullet-dev, libopenscenegraph-dev, libmygui-dev - ] - coverity_scan: # TODO: currently takes too long, disabled openmw/openmw-cs for now. - project: - name: "OpenMW/openmw" - description: "" - branch_pattern: coverity_scan - notification_email: 1122069+psi29a@users.noreply.github.com - build_command_prepend: "cov-configure --comptype gcc --compiler gcc-5 --template; cmake . -DBUILD_OPENMW=FALSE -DBUILD_OPENCS=FALSE" - build_command: "make VERBOSE=1 -j3" -matrix: - include: - - name: OpenMW (all) on MacOS 10.15 with Xcode 11.6 - os: osx - osx_image: xcode11.6 - if: branch != coverity_scan - - name: OpenMW (all) on Ubuntu Focal with GCC - os: linux - dist: focal - if: branch != coverity_scan - - name: OpenMW (tests only) on Ubuntu Focal with GCC - os: linux - dist: focal - if: branch != coverity_scan - env: - - BUILD_TESTS_ONLY: 1 - - name: OpenMW (openmw) on Ubuntu Focal with Clang's Static Analysis - os: linux - dist: focal - env: - - MATRIX_EVAL="CC=clang && CXX=clang++" - - ANALYZE="scan-build --force-analyze-debug-code --use-cc clang --use-c++ clang++" - if: branch != coverity_scan - compiler: clang - - name: OpenMW Components Coverity Scan - os: linux - dist: focal - if: branch = coverity_scan -# allow_failures: -# - name: OpenMW (openmw) on Ubuntu Focal with GCC-10 -# env: -# - MATRIX_EVAL="CC=gcc-10 && CXX=g++-10" - -before_install: - - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then eval "${MATRIX_EVAL}"; fi - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ]; then ./CI/before_install.${TRAVIS_OS_NAME}.sh; fi -before_script: - - ccache -z - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ]; then ./CI/before_script.${TRAVIS_OS_NAME}.sh; fi -script: - - cd ./build - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ]; then ${ANALYZE} make -j3; fi - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ] && [ "${TRAVIS_OS_NAME}" = "osx" ]; then make package; fi - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ] && [ "${TRAVIS_OS_NAME}" = "osx" ]; then ../CI/check_package.osx.sh; fi - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ] && [ "${TRAVIS_OS_NAME}" = "linux" ] && [ "${BUILD_TESTS_ONLY}" ]; then ./openmw_test_suite; fi - - if [ "${COVERITY_SCAN_BRANCH}" != 1 ] && [ "${TRAVIS_OS_NAME}" = "linux" ]; then cd .. && ./CI/check_tabs.sh; fi - - cd "${TRAVIS_BUILD_DIR}" - - ccache -s -deploy: - provider: script - script: ./CI/deploy.osx.sh - skip_cleanup: true - on: - branch: master - condition: "$TRAVIS_EVENT_TYPE = cron && $TRAVIS_OS_NAME = osx" - repo: OpenMW/openmw -notifications: - email: - recipients: - - corrmage+travis-ci@gmail.com - on_success: change - on_failure: always - irc: - channels: - - "chat.freenode.net#openmw" - on_success: change - on_failure: always - use_notice: true diff --git a/AUTHORS.md b/AUTHORS.md index 25004078e0..0939409926 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,7 +4,7 @@ Contributors The OpenMW project was started in 2008 by Nicolay Korslund. In the course of years many people have contributed to the project. -If you feel your name is missing from this list, please notify a developer. +If you feel your name is missing from this list, please add it to `AUTHORS.md`. Programmers @@ -24,11 +24,14 @@ Programmers Alex McKibben alexanderkjall Alexander Nadeau (wareya) - Alexander Olofsson (Ace) + Alexander Olofsson (Ananace) Alex Rice Alex S (docwest) + Alexey Yaryshev (skeevert) Allofich + Andreas Stöckel Andrei Kortunov (akortunov) + Andrew Appuhamy (andrew-app) AnyOldName3 Ardekantur Armin Preiml @@ -42,6 +45,7 @@ Programmers Austin Salgat (Salgat) Ben Shealy (bentsherman) Berulacks + Bo Svensson Britt Mathis (galdor557) Capostrophic Carl Maxwell @@ -49,6 +53,8 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Cody Glassman (Wazabear) + Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) crussell187 @@ -65,16 +71,19 @@ Programmers David Teviotdale (dteviot) Diggory Hardy Dmitry Marakasov (AMDmi3) + Duncan Frost (duncans_pumpkin) Edmondo Tommasina (edmondo) Eduard Cot (trombonecot) Eli2 Emanuel Guével (potatoesmaster) + Eris Caffee (eris) eroen escondida Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) Finbar Crago (finbar-crago) + Florent Teppe (Tetramir) Florian Weber (Florianjw) Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) @@ -86,16 +95,20 @@ Programmers Haoda Wang (h313) hristoast Internecine + Ivan Beloborodov (myrix) Jackerty Jacob Essex (Yacoby) + Jacob Turnbull (Tankinfrank) Jake Westrip (16bitint) James Carty (MrTopCat) James Moore (moore.work) James Stephens (james-h-stephens) Jan-Peter Nilsson (peppe) Jan Borsodi (am0s) + JanuarySnow Jason Hooks (jhooks) jeaye + jefetienne Jeffrey Haines (Jyby) Jengerer Jiří Kuneš (kunesj) @@ -104,6 +117,7 @@ Programmers John Blomberg (fstp) Jordan Ayers Jordan Milne + Josquin Frei Josua Grawitter Jules Blok (Armada651) julianko @@ -114,6 +128,7 @@ Programmers Kurnevsky Evgeny (kurnevsky) Lars Söderberg (Lazaroth) lazydev + Léo Peltier Leon Krieg (lkrieg) Leon Saunders (emoose) logzero @@ -121,7 +136,6 @@ Programmers Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) - Manuel Edelmann (vorenon) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) @@ -130,6 +144,7 @@ Programmers Martin Otto (MAtahualpa) Mateusz Kołaczek (PL_kolek) Mateusz Malisz (malice) + Max Henzerling (SaintMercury) megaton Michael Hogan (Xethik) Michael Mc Donnell @@ -148,9 +163,12 @@ Programmers Nathan Jeffords (blunted2night) NeveHanter Nialsy + Nick Crawford (nighthawk469) Nikolay Kasyanov (corristo) + Noah Gooder nobrakal Nolan Poe (nopoe) + Nurivan Gomez (Nuri-G) Oleg Chkan (mrcheko) Paul Cercueil (pcercuei) Paul McElroy (Greendogo) @@ -165,6 +183,7 @@ Programmers PlutonicOverkill Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) + Randy Davin (Kindi) rdimesio rexelion riothamus @@ -182,6 +201,7 @@ Programmers sergoz ShadowRadiance Siimacore + Simon Meulenbeek (simonmb) sir_herrbatka smbas Sophie Kirschner (pineapplemachine) @@ -195,21 +215,26 @@ Programmers Sylvain Thesnieres (Garvek) t6 terrorfisch + Tess (tescoShoppah) thegriglat Thomas Luppi (Digmaster) tlmullis tri4ng1e Thoronador + Tobias Tribble (zackogenic) + Tom Lowe (Vulpen) Tom Mason (wheybags) Torben Leif Carrington (TorbenC) unelsson uramer viadanna + Vidi_Aquam Vincent Heuken Vladimir Panteleev (CyberShadow) + vocollapse Wang Ryu (bzzt) Will Herrmann (Thunderforge) - vocollapse + Wolfgang Lieff xyzz Yohaulticetl Yuri Krupenin @@ -228,11 +253,13 @@ Documentation Joakim Berg (lysol90) Ryan Tucker (Ravenwing) sir_herrbatka + David Nagy (zuzaman) Packagers --------- - Alexander Olofsson (Ace) - Windows + Alexander Olofsson (Ananace) - Windows and Flatpak + Alexey Sokolov (DarthGandalf) - Gentoo Linux Bret Curtis (psi29a) - Debian and Ubuntu Linux Edmondo Tommasina (edmondo) - Gentoo Linux Julian Ospald (hasufell) - Gentoo Linux diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f19783ef5..97198823ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,262 @@ +0.48.0 +------ + + Bug #1751: Birthsign abilities increase modified attribute values instead of base ones + Bug #1930: Followers are still fighting if a target stops combat with a leader + Bug #2036: SetStat and ModStat instructions aren't implemented the same way as in Morrowind + Bug #3246: ESSImporter: Most NPCs are dead on save load + Bug #3488: AI combat aiming is too slow + Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear + Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) + Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change + Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes + Bug #3855: AI sometimes spams defensive spells + Bug #3867: All followers attack player when one follower enters combat with player + Bug #3905: Great House Dagoth issues + Bug #4203: Resurrecting an actor doesn't close the loot GUI + Bug #4376: Moved actors don't respawn in their original cells + Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node + Bug #4602: Robert's Bodies: crash inside createInstance() + Bug #4700: Editor: Incorrect command implementation + Bug #4744: Invisible particles aren't always processed + Bug #4949: Incorrect particle lighting + Bug #5054: Non-biped creatures don't use spellcast equip/unequip animations + Bug #5088: Sky abruptly changes direction during certain weather transitions + Bug #5100: Persuasion doesn't always clamp the resulting disposition + Bug #5120: Scripted object spawning updates physics system + Bug #5192: Actor turn rate is too slow + Bug #5207: Loose summons can be present in scene + Bug #5279: Ingame console stops auto-scrolling after clicking output + Bug #5318: Aiescort behaves differently from vanilla + Bug #5371: 'Dead' slaughterfish added by mod are animated/alive + Bug #5377: Console does not appear after using menutest in inventory + Bug #5379: Wandering NPCs falling through cantons + Bug #5394: Windows snapping no longer works + Bug #5434: Pinned windows shouldn't cover breath progress bar + Bug #5453: Magic effect VFX are offset for creatures + Bug #5483: AutoCalc flag is not used to calculate spells cost + Bug #5508: Engine binary links to Qt without using it + Bug #5592: Weapon idle animations do not work properly + Bug #5596: Effects in constant spells should not be merged + Bug #5621: Drained stats cannot be restored + Bug #5766: Active grid object paging - disappearing textures + Bug #5788: Texture editing parses the selected indexes wrongly + Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention + Bug #5842: GetDisposition adds temporary disposition change from different actors + Bug #5858: Visible modal windows and dropdowns crashing game on exit + Bug #5863: GetEffect should return true after the player has teleported + Bug #5913: Failed assertion during Ritual of Trees quest + Bug #5937: Lights always need to be rotated by 90 degrees + Bug #5989: Simple water isn't affected by texture filter settings + Bug #6037: Launcher: Morrowind content language cannot be set to English + Bug #6051: NaN water height in ESM file is not handled gracefully + Bug #6066: Addtopic "return" does not work from within script. No errors thrown + Bug #6067: ESP loader fails for certain subrecord orders + Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends + Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime + Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed + Bug #6115: Showmap overzealous matching + Bug #6118: Creature landing sound counts as a footstep + Bug #6123: NPC with broken script freezes the game on hello + Bug #6129: Player avatar not displayed correctly for large window sizes when GUI scaling active + Bug #6131: Item selection in the avatar window not working correctly for large window sizes + Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player + Bug #6142: Groundcover plugins change cells flags + Bug #6143: Capturing a screenshot renders the engine temporarily unresponsive + Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6168: Weather particles flicker for a frame at start of storms + Bug #6172: Some creatures can't open doors + Bug #6174: Spellmaking and Enchanting sliders differences from vanilla + Bug #6177: Followers of player follower stop following after waiting for a day + Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla + Bug #6191: Encumbrance messagebox timer works incorrectly + Bug #6197: Infinite Casting Loop + Bug #6253: Multiple instances of Reflect stack additively + Bug #6255: Reflect is different from vanilla + Bug #6256: Crash on exit with enabled shadows and statically linked OpenSceneGraph + Bug #6258: Barter menu glitches out when modifying prices + Bug #6273: Respawning NPCs rotation is inconsistent + Bug #6276: Deleted groundcover instances are not deleted in game + Bug #6282: Laura craft doesn't follow the player character + Bug #6283: Avis Dorsey follows you after her death + Bug #6285: Brush template drawing and terrain selection drawing performance is very bad + Bug #6289: Keyword search in dialogues expected the text to be all ASCII characters + Bug #6291: Can't pickup the dead mage's journal from the mysterious hunter mod + Bug #6302: Teleporting disabled actor breaks its disabled state + Bug #6303: After "go to jail" weapon can be stuck in the ready to attack state + Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken + Bug #6321: Arrow enchantments should always be applied to the target + Bug #6322: Total sold/cost should reset to 0 when there are no items offered + Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house + Bug #6324: Special Slave Companions: Can't buy the slave companions + Bug #6326: Detect Enchantment/Key should detect items in unresolved containers + Bug #6327: Blocking roots the character in place + Bug #6333: Werewolf stat changes should be implemented as damage/fortifications + Bug #6343: Magic projectile speed doesn't take race weight into account + Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures + Bug #6354: SFX abruptly cut off after crossing max distance + Bug #6358: Changeweather command does not report an error when entering non-existent region + Bug #6363: Some scripts in Morrowland fail to work + Bug #6376: Creatures should be able to use torches + Bug #6386: Artifacts in water reflection due to imprecise screen-space coordinate computation + Bug #6389: Maximum light distance setting doesn't affect water reflections + Bug #6395: Translations with longer tab titles may cause tabs to disappear from the options menu + Bug #6396: Inputting certain Unicode characters triggers an assertion + Bug #6416: Morphs are applied to the wrong target + Bug #6417: OpenMW doesn't always use the right node to accumulate movement + Bug #6429: Wyrmhaven: Can't add AI packages to player + Bug #6433: Items bound to Quick Keys sometimes do not appear until the Quick Key menu is opened + Bug #6451: Weapon summoned from Cast When Used item will have the name "None" + Bug #6473: Strings from NIF should be parsed only to first null terminator + Bug #6493: Unlocking owned but not locked or unlocked containers is considered a crime + Bug #6517: Rotations for KeyframeData in NIFs should be optional + Bug #6519: Effects tooltips for ingredients work incorrectly + Bug #6523: Disintegrate Weapon is resisted by Resist Magicka instead of Sanctuary + Bug #6544: Far from world origin objects jitter when camera is still + Bug #6559: Weapon condition inconsistency between melee and ranged critical / sneak / KO attacks + Bug #6579: OpenMW compilation error when using OSG doubles for BoundingSphere + Bug #6606: Quests with multiple IDs cannot always be restarted + Bug #6653: With default settings the in-game console doesn't fit into screen + Bug #6655: Constant effect absorb attribute causes the game to break + Bug #6667: Pressing the Esc key while resting or waiting causes black screen. + Bug #6670: Dialogue order is incorrect + Bug #6672: Garbage object refs in groundcover plugins like Vurt's grass plugins + Bug #6680: object.cpp handles nodetree unsafely, memory access with dangling pointer + Bug #6682: HitOnMe doesn't fire as intended + Bug #6697: Shaders vertex lighting incorrectly clamped + Bug #6711: Log time differs from real time + Bug #6717: Broken script causes interpreter stack corruption + Bug #6718: Throwable weapons cause arrow enchantment effect to be applied to the whole body + Bug #6730: LoopGroup stalls animation after playing :Stop frame until another animation is played + Bug #6753: Info records without a DATA subrecords are loaded incorrectly + Bug #6794: Light sources are attached to mesh bounds centers instead of mesh origins when AttachLight NiNode is missing + Bug #6799: Game crashes if an NPC has no Class attached + Bug #6849: ImageButton texture is not scaled properly + Feature #890: OpenMW-CS: Column filtering + Feature #1465: "Reset" argument for AI functions + Feature #2491: Ability to make OpenMW "portable" + Feature #2554: OpenMW-CS: Modifying an object in the cell view should trigger the instances table to scroll to the corresponding record + Feature #2766: Warn user if their version of Morrowind is not the latest. + Feature #2780: A way to see current OpenMW version in the console + Feature #2858: Add a tab to the launcher for handling datafolders + Feature #3245: Grid and angle snapping for the OpenMW-CS + Feature #3616: Allow Zoom levels on the World Map + Feature #4067: Post Processing + Feature #4297: Implement APPLIED_ONCE flag for magic effects + Feature #4414: Handle duration of EXTRA SPELL magic effect + Feature #4595: Unique object identifier + Feature #4974: Overridable MyGUI layout + Feature #4975: Built-in TrueType fonts + Feature #5198: Implement "Magic effect expired" event + Feature #5454: Clear active spells from actor when he disappears from scene + Feature #5489: MCP: Telekinesis fix for activators + Feature #5701: Convert osgAnimation::RigGeometry to double-buffered custom version + Feature #5737: Handle instance move from one cell to another + Feature #5928: Allow Glow in the Dahrk to be disabled + Feature #5996: Support Lua scripts in OpenMW + Feature #6017: Separate persistent and temporary cell references when saving + Feature #6019: Add antialias alpha test to the launcher or enable by default if possible + Feature #6032: Reverse-z depth buffer + Feature #6128: Soft Particles + Feature #6171: In-game log viewer + Feature #6189: Navigation mesh disk cache + Feature #6199: Support FBO Rendering + Feature #6248: Embedded error marker mesh + Feature #6249: Alpha testing support for Collada + Feature #6251: OpenMW-CS: Set instance movement based on camera zoom + Feature #6288: Preserve the "blocked" record flag for referenceable objects. + Feature #6360: More realistic raindrop ripples + Feature #6380: Treat commas as whitespace in scripts + Feature #6419: Don't grey out topics if they can produce another topic reference + Feature #6443: Support NiStencilProperty + Feature #6496: Handle NCC flag in NIF files + Feature #6534: Shader-based object texture blending + Feature #6541: Gloss-mapping + Feature #6557: Add support for controller gyroscope + Feature #6592: Support for NiTriShape particle emitters + Feature #6600: Support NiSortAdjustNode + Feature #6684: Support NiFltAnimationNode + Feature #6699: Support Ignored flag + 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 #6867: Add a way to localize hardcoded strings in GUI + Task #6078: First person should not clear depth buffer + Task #6161: Refactor Sky to use shaders and be GLES/GL3 friendly + Task #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly + Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings + Task #6264: Remove the old classes in animation.cpp + Task #6553: Simplify interpreter instruction registration + Task #6564: Remove predefined data paths `data="?global?data"`, `data=./data` + Task #6631: Fix ffmpeg avio API usage causing hangs in ffmpeg version 5 + Task #6709: Move KeyframeController transformation magic to NifOsg::MatrixTransform + Task #6763: gcc 12.1 multiple compiler warnings + 0.47.0 ------ Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path + Bug #1901: Actors colliding behaviour is different from vanilla Bug #1952: Incorrect particle lighting Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2473: Unable to overstock merchants - Bug #2798: Mutable ESM records - Bug #2976 [reopened]: Issues combining settings from the command line and both config files + Bug #2976: [reopened]: Issues combining settings from the command line and both config files + Bug #3137: Walking into a wall prevents jumping + Bug #3372: Projectiles and magic bolts go through moving targets Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects Bug #3789: Crash in visitEffectSources while in battle Bug #3862: Random container contents behave differently than vanilla Bug #3929: Leveled list merchant containers respawn on barter Bug #4021: Attributes and skills are not stored as floats + Bug #4039: Multiple followers should have the same following distance Bug #4055: Local scripts don't inherit variables from their base record Bug #4083: Door animation freezes when colliding with actors + Bug #4247: Cannot walk up stairs in Ebonheart docks + Bug #4357: OpenMW-CS: TopicInfos index sorting and rearranging isn't fully functional + Bug #4363: OpenMW-CS: Defect in Clone Function for Dialogue Info records + Bug #4447: Actor collision capsule shape allows looking through some walls + Bug #4465: Collision shape overlapping causes twitching + Bug #4476: Abot Gondoliers: player hangs in air during scenic travel + Bug #4568: Too many actors in one spot can push other actors out of bounds Bug #4623: Corprus implementation is incorrect Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem + Bug #4765: Data race in ChunkManager -> Array::setBinding Bug #4774: Guards are ignorant of an invisible player that tries to attack them + Bug #5026: Data races with rain intensity uniform set by sky and used by water Bug #5101: Hostile followers travel with the player Bug #5108: Savegame bloating due to inefficient fog textures format Bug #5165: Active spells should use real time intead of timestamps + Bug #5300: NPCs don't switch from torch to shield when starting combat Bug #5358: ForceGreeting always resets the dialogue window completely Bug #5363: Enchantment autocalc not always 0/1 Bug #5364: Script fails/stops if trying to startscript an unknown script Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures Bug #5370: Opening an unlocked but trapped door uses the key - Bug #5384: openmw-cs: deleting an instance requires reload of scene window to show in editor + Bug #5384: OpenMW-CS: Deleting an instance requires reload of scene window to show in editor Bug #5387: Move/MoveWorld don't update the object's cell properly + Bug #5391: Races Redone 1.2 bodies don't show on the inventory Bug #5397: NPC greeting does not reset if you leave + reenter area - Bug #5400: Editor: Verifier checks race of non-skin bodyparts + Bug #5400: OpenMW-CS: Verifier checks race of non-skin bodyparts Bug #5403: Enchantment effect doesn't show on an enemy during death animation Bug #5415: Environment maps in ebony cuirass and HiRez Armors Indoril cuirass don't work Bug #5416: Junk non-node records before the root node are not handled gracefully Bug #5422: The player loses all spells when resurrected + Bug #5423: Guar follows actors too closely Bug #5424: Creatures do not headtrack player Bug #5425: Poison effect only appears for one frame Bug #5427: GetDistance unknown ID error is misleading + Bug #5431: Physics performance degradation after a specific number of actors on a scene Bug #5435: Enemies can't hurt the player when collision is off Bug #5441: Enemies can't push a player character when in critical strike stance Bug #5451: Magic projectiles don't disappear with the caster Bug #5452: Autowalk is being included in savegames + Bug #5469: Local map is reset when re-entering certain cells Bug #5472: Mistify mod causes CTD in 0.46 on Mac + Bug #5473: OpenMW-CS: Cell border lines don't update properly on terrain change Bug #5479: NPCs who should be walking around town are standing around without walking Bug #5484: Zero value items shouldn't be able to be bought or sold for 1 gold Bug #5485: Intimidate doesn't increase disposition on marginal wins @@ -52,33 +264,98 @@ Bug #5499: Faction advance is available when requirements not met Bug #5502: Dead zone for analogue stick movement is too small Bug #5507: Sound volume is not clamped on ingame settings update + Bug #5525: Case-insensitive search in the inventory window does not work with non-ASCII characters Bug #5531: Actors flee using current rotation by axis x Bug #5539: Window resize breaks when going from a lower resolution to full screen resolution Bug #5548: Certain exhausted topics can be highlighted again even though there's no new dialogue Bug #5557: Diagonal movement is noticeably slower with analogue stick Bug #5588: Randomly clicking on the journal's right-side page when it's empty shows random topics Bug #5603: Setting constant effect cast style doesn't correct effects view + Bug #5604: Only one valid NIF root node is loaded from a single file Bug #5611: Usable items with "0 Uses" should be used only once + Bug #5619: Input events are queued during save loading Bug #5622: Can't properly interact with the console when in pause menu + Bug #5627: Bookart not shown if it isn't followed by
statement Bug #5633: Damage Spells in effect before god mode is enabled continue to hurt the player character and can kill them Bug #5639: Tooltips cover Messageboxes Bug #5644: Summon effects running on the player during game initialization cause crashes Bug #5656: Sneaking characters block hits while standing Bug #5661: Region sounds don't play at the right interval + Bug #5675: OpenMW-CS: FRMR subrecords are saved with the wrong MastIdx + Bug #5680: Bull Netches incorrectly aim over the player character's head and always miss + Bug #5681: Player character can clip or pass through bridges instead of colliding against them + Bug #5687: Bound items covering the same inventory slot expiring at the same time freezes the game + Bug #5688: Water shader broken indoors with enable indoor shadows = false + Bug #5695: ExplodeSpell for actors doesn't target the ground + Bug #5703: OpenMW-CS menu system crashing on XFCE + Bug #5706: AI sequences stop looping after the saved game is reloaded + Bug #5713: OpenMW-CS: Collada models are corrupted in Qt-based scene view + Bug #5731: OpenMW-CS: skirts are invisible on characters + Bug #5739: Saving and loading the save a second or two before hitting the ground doesn't count fall damage + Bug #5758: Paralyzed actors behavior is inconsistent with vanilla + Bug #5762: Movement solver is insufficiently robust + Bug #5800: Equipping a CE enchanted ring deselects an already equipped and selected enchanted ring from the spell menu + Bug #5807: Video decoding crash on ARM + Bug #5821: NPCs from mods getting removed if mod order was changed + Bug #5835: OpenMW doesn't accept negative values for NPC's hello, alarm, fight, and flee + Bug #5836: OpenMW dialogue/greeting/voice filter doesn't accept negative Ai values for NPC's hello, alarm, fight, and flee + Bug #5838: Local map and other menus become blank in some locations while playing Wizards' Islands mod. + Bug #5840: GetSoundPlaying "Health Damage" doesn't play when NPC hits target with shield effect ( vanilla engine behavior ) + Bug #5841: Can't Cast Zero Cost Spells When Magicka is < 0 + Bug #5869: Guards can initiate arrest dialogue behind locked doors + Bug #5871: The console appears if you type the Russian letter "Ё" in the name of the enchantment + Bug #5877: Effects appearing with empty icon + Bug #5899: Visible modal windows and dropdowns crashing game on exit + Bug #5902: NiZBufferProperty is unable to disable the depth test + Bug #5906: Sunglare doesn't work with Mesa drivers and AMD GPUs + Bug #5912: ImprovedBound mod doesn't work + Bug #5914: BM: The Swimmer can't reach destination + Bug #5923: Clicking on empty spaces between journal entries might show random topics + Bug #5934: AddItem command doesn't accept negative values + Bug #5975: NIF controllers from sheath meshes are used + Bug #5991: Activate should always be allowed for inventory items + Bug #5995: NiUVController doesn't calculate the UV offset properly + Bug #6007: Crash when ending cutscene is playing + Bug #6016: Greeting interrupts Fargoth's sneak-walk + Bug #6022: OpenMW-CS: Terrain selection is not updated when undoing/redoing terrain changes + Bug #6023: OpenMW-CS: Clicking on a reference in "Terrain land editing" mode discards corresponding select/edit action + Bug #6028: Particle system controller values are incorrectly used + Bug #6035: OpenMW-CS: Circle brush in "Terrain land editing" mode sometimes includes vertices outside its radius + Bug #6036: OpenMW-CS: Terrain selection at the border of cells omits certain corner vertices + Bug #6043: Actor can have torch missing when torch animation is played + Bug #6047: Mouse bindings can be triggered during save loading + Bug #6136: Game freezes when NPCs try to open doors that are about to be closed + Bug #6294: Game crashes with empty pathgrid Feature #390: 3rd person look "over the shoulder" + Feature #832: OpenMW-CS: Handle deleted references + Feature #1536: Show more information about level on menu + Feature #2159: "Graying out" exhausted dialogue topics Feature #2386: Distant Statics in the form of Object Paging Feature #2404: Levelled List can not be placed into a container + Feature #2686: Timestamps in openmw.log + Feature #2798: Mutable ESM records + Feature #3171: OpenMW-CS: Instance drag selection + Feature #3983: Wizard: Add link to buy Morrowind + Feature #4201: Projectile-projectile collision + Feature #4486: Handle crashes on Windows Feature #4894: Consider actors as obstacles for pathfinding + Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing + Feature #4917: Do not trigger NavMesh update when RecastMesh update should not change NavMesh + Feature #4977: Use the "default icon.tga" when an item's icon is not found Feature #5043: Head Bobbing + Feature #5199: OpenMW-CS: Improve scene view colors Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines + Feature #5456: Basic collada animation support Feature #5457: Realistic diagonal movement Feature #5486: Fixes trainers to choose their training skills based on their base skill points + Feature #5500: Prepare enough navmesh tiles before scene loading ends + Feature #5511: Add in game option to toggle HRTF support in OpenMW Feature #5519: Code Patch tab in launcher Feature #5524: Resume failed script execution after reload - Feature #5525: Search fields tweaks (utf-8) Feature #5545: Option to allow stealing from an unconscious NPC during combat + Feature #5551: Do not reboot PC after OpenMW installation on Windows Feature #5563: Run physics update in background thread Feature #5579: MCP SetAngle enhancement Feature #5580: Service refusal filtering @@ -86,6 +363,17 @@ Feature #5642: Ability to attach arrows to actor skeleton instead of bow mesh Feature #5649: Skyrim SE compressed BSA format support Feature #5672: Make stretch menu background configuration more accessible + Feature #5692: Improve spell/magic item search to factor in magic effect names + Feature #5730: Add graphic herbalism option to the launcher and documents + Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. + Feature #5813: Instanced groundcover support + Feature #5814: Bsatool should be able to create BSA archives, not only to extract it + Feature #5828: Support more than 8 lights + Feature #5910: Fall back to delta time when physics can't keep up + Feature #5980: Support Bullet with double precision instead of one with single precision + Feature #6024: OpenMW-CS: Selecting terrain in "Terrain land editing" should support "Add to selection" and "Remove from selection" modes + Feature #6033: Include pathgrid to navigation mesh + Feature #6034: Find path based on area cost depending on NPC stats Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation @@ -97,7 +385,7 @@ Bug #2395: Duplicated plugins in the launcher when multiple data directories provide the same plugin Bug #2679: Unable to map mouse wheel under control settings Bug #2969: Scripted items can stack - Bug #2976: Data lines in global openmw.cfg take priority over user openmw.cfg + Bug #2976: [reopened in 0.47] Data lines in global openmw.cfg take priority over user openmw.cfg Bug #2987: Editor: some chance and AI data fields can overflow Bug #3006: 'else if' operator breaks script compilation Bug #3109: SetPos/Position handles actors differently @@ -115,7 +403,6 @@ Bug #4009: Launcher does not show data files on the first run after installing Bug #4077: Enchanted items are not recharged if they are not in the player's inventory Bug #4141: PCSkipEquip isn't set to 1 when reading books/scrolls - Bug #4202: Open .omwaddon files without needing toopen openmw-cs first Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect Bug #4262: Rain settings are hardcoded Bug #4270: Closing doors while they are obstructed desyncs closing sfx @@ -205,7 +492,6 @@ Bug #4964: Multiple effect spell projectile sounds play louder than vanilla Bug #4965: Global light attenuation settings setup is lacking Bug #4969: "Miss" sound plays for any actor - Bug #4971: OpenMW-CS: Make rotations display as degrees instead of radians Bug #4972: Player is able to use quickkeys while disableplayerfighting is active Bug #4979: AiTravel maximum range depends on "actors processing range" setting Bug #4980: Drowning mechanics is applied for actors indifferently from distance to player @@ -303,7 +589,6 @@ Bug #5350: An attempt to launch magic bolt causes "AL error invalid value" error Bug #5352: Light source items' duration is decremented while they aren't visible Feature #1724: Handle AvoidNode - Feature #2159: "Graying out" exhausted dialogue topics Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls Feature #3442: Default values for fallbacks from ini file @@ -316,6 +601,7 @@ Feature #4001: Toggle sneak controller shortcut Feature #4068: OpenMW-CS: Add a button to reset key bindings to defaults Feature #4129: Beta Comment to File + Feature #4202: Open .omwaddon files without needing to open openmw-cs first Feature #4209: Editor: Faction rank sub-table Feature #4255: Handle broken RepairedOnMe script function Feature #4316: Implement RaiseRank/LowerRank functions properly @@ -338,6 +624,7 @@ Feature #4958: Support eight blood types Feature #4962: Add casting animations for magic items Feature #4968: Scalable UI widget skins + Feature #4971: OpenMW-CS: Make rotations display as degrees instead of radians Feature #4994: Persistent pinnable windows hiding Feature #5000: Compressed BSA format support Feature #5005: Editor: Instance window via Scene window @@ -1814,6 +2101,7 @@ Bug #2025: Missing mouse-over text for non affordable items Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall" Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding + Bug #3066: Editor doesn't check if IDs and other strings are longer than their hardcoded field length Feature #471: Editor: Special case implementation for top-level window with single sub-window Feature #472: Editor: Sub-Window re-use settings Feature #704: Font colors import from fallback settings diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md index 0a039ec3fa..100bc376ff 100644 --- a/CHANGELOG_PR.md +++ b/CHANGELOG_PR.md @@ -18,9 +18,10 @@ Known Issues: New Features: - Dialogue to split item stacks now displays the name of the trapped soul for stacks of soul gems (#5362) +- Basics of Collada animations are now supported via osgAnimation plugin (#5456) New Editor Features: -- ? +- Instance selection modes are now implemented (centred cube, corner-dragged cube, sphere) with four user-configurable actions (select only, add to selection, remove from selection, invert selection) (#3171) Bug Fixes: - NiParticleColorModifier in NIF files is now properly handled which solves issues regarding particle effects, e.g., smoke and fire (#1952, #3676) @@ -33,8 +34,20 @@ Bug Fixes: - Morrowind legacy madness: Using a key on a trapped door/container now only disarms the trap if the door/container is locked (#5370) Editor Bug Fixes: +- Deleted and moved objects within a cell are now saved properly (#832) +- Disabled record sorting in Topic and Journal Info tables, implemented drag-move for records (#4357) +- Topic and Journal Info records can now be cloned with a different parent Topic/Journal Id (#4363) - Verifier no longer checks for alleged 'race' entries in clothing body parts (#5400) +- Cell borders are now properly redrawn when undoing/redoing terrain changes (#5473) +- Loading mods now keeps the master index (#5675) +- Flicker and crashing on XFCE4 fixed (#5703) +- Collada models render properly in the Editor (#5713) +- Terrain-selection grid is now properly updated when undoing/redoing terrain changes (#6022) +- Tool outline and select/edit actions in "Terrain land editing" mode now ignore references (#6023) +- Primary-select and secondary-select actions in "Terrain land editing" mode now behave like in "Instance editing" mode (#6024) +- Using the circle brush to select terrain in the "Terrain land editing" mode no longer selects vertices outside the circle (#6035) +- Vertices at the NW and SE corners of a cell can now also be selected in "Terrain land editing" mode if the adjacent cells aren't loaded yet (#6036) Miscellaneous: - Prevent save-game bloating by using an appropriate fog texture format (#5108) -- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) \ No newline at end of file +- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) diff --git a/CI/activate_msvc.sh b/CI/activate_msvc.sh index 47f2c246f0..233f017433 100644 --- a/CI/activate_msvc.sh +++ b/CI/activate_msvc.sh @@ -30,55 +30,11 @@ command -v unixPathAsWindows >/dev/null 2>&1 || function unixPathAsWindows { fi } -function windowsSystemPathAsUnix { - if command -v cygpath >/dev/null 2>&1; then - cygpath -u -p $1 - else - IFS=';' read -r -a paths <<< "$1" - declare -a convertedPaths - for entry in paths; do - convertedPaths+=(windowsPathAsUnix $entry) - done - convertedPath=printf ":%s" ${convertedPaths[@]} - echo ${convertedPath:1} - fi -} - -# capture CMD environment so we know what's been changed -declare -A originalCmdEnv -originalIFS="$IFS" -IFS=$'\n\r' -for pair in $(cmd //c "set"); do - IFS='=' read -r -a separatedPair <<< "${pair}" - if [ ${#separatedPair[@]} -ne 2 ]; then - echo "Parsed '$pair' as ${#separatedPair[@]} parts, expected 2." - continue - fi - originalCmdEnv["${separatedPair[0]}"]="${separatedPair[1]}" -done # capture CMD environment in a shell with MSVC activated -cmdEnv="$(cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" set)" - -declare -A cmdEnvChanges -for pair in $cmdEnv; do - if [ -n "$pair" ]; then - IFS='=' read -r -a separatedPair <<< "${pair}" - if [ ${#separatedPair[@]} -ne 2 ]; then - echo "Parsed '$pair' as ${#separatedPair[@]} parts, expected 2." - continue - fi - key="${separatedPair[0]}" - value="${separatedPair[1]}" - if ! [ ${originalCmdEnv[$key]+_} ] || [ "${originalCmdEnv[$key]}" != "$value" ]; then - if [ $key != 'PATH' ] && [ $key != 'path' ] && [ $key != 'Path' ]; then - export "$key=$value" - else - export PATH=$(windowsSystemPathAsUnix $value) - fi - fi - fi -done +cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" "bash" "-c" "declare -px > declared_env.sh" +source ./declared_env.sh +rm declared_env.sh MISSINGTOOLS=0 @@ -93,6 +49,4 @@ if [ $MISSINGTOOLS -ne 0 ]; then return 1 fi -IFS="$originalIFS" - -restoreOldSettings \ No newline at end of file +restoreOldSettings diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 791377dd96..712ded2769 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201018.zip -o ~/openmw-android-deps.zip -unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20211114.zip -o ~/openmw-android-deps.zip +unzip -o ~/openmw-android-deps -d /android-ndk-r22/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 85434fa06d..87013df1ae 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,9 +1,27 @@ -#!/bin/sh -e +#!/bin/sh -ex + +export HOMEBREW_NO_EMOJI=1 +brew update --quiet + +# workaround python issue on travis +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies python@3.8 || true +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies python@3.9 || true +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies qt@6 || true # Some of these tools can come from places other than brew, so check before installing +[ -z "${TRAVIS}" ] && brew reinstall xquartz +[ -z "${TRAVIS}" ] && brew reinstall fontconfig 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 +command -v qmake >/dev/null 2>&1 || brew install qt@5 +brew install icu4c +brew install yaml-cpp +export PATH="/usr/local/opt/qt@5/bin:$PATH" # needed to use qmake in none default path as qt now points to qt6 -curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-f8918dd.zip -o ~/openmw-deps.zip +ccache --version +cmake --version +qmake --version + +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20220225.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null + diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh index 8f0ec77da0..cbf8d64905 100755 --- a/CI/before_script.android.sh +++ b/CI/before_script.android.sh @@ -3,13 +3,31 @@ # hack to work around: FFmpeg version is too old, 3.2 is required sed -i s/"NOT FFVER_OK"/"FALSE"/ CMakeLists.txt -mkdir build +# Silence a git warning +git config --global advice.detachedHead false + +mkdir -p build cd build +# Build a version of ICU for the host so that it can use the tools during the cross-compilation +mkdir -p icu-host-build +cd icu-host-build +if [ -r ../extern/fetched/icu/icu4c/source/configure ]; then + ICU_SOURCE_DIR=../extern/fetched/icu/icu4c/source +else + wget https://github.com/unicode-org/icu/archive/refs/tags/release-70-1.zip + unzip release-70-1.zip + ICU_SOURCE_DIR=./icu-release-70-1/icu4c/source +fi +${ICU_SOURCE_DIR}/configure --disable-tests --disable-samples --disable-icuio --disable-extras CC="ccache gcc" CXX="ccache g++" +make -j $(nproc) +cd .. + cmake \ --DCMAKE_TOOLCHAIN_FILE=/usr/lib/android-sdk/ndk-bundle/build/cmake/android.toolchain.cmake \ +-DCMAKE_TOOLCHAIN_FILE=/android-ndk-r22/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ +-DANDROID_LD=deprecated \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_INSTALL_PREFIX=install \ @@ -21,5 +39,11 @@ cmake \ -DBUILD_ESSIMPORTER=0 \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ --DMyGUI_LIBRARY="/usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/libMyGUIEngineStatic.a" \ +-DBUILD_NAVMESHTOOL=OFF \ +-DBUILD_BULLETOBJECTTOOL=OFF \ +-DOPENMW_USE_SYSTEM_MYGUI=OFF \ +-DOPENMW_USE_SYSTEM_SQLITE3=OFF \ +-DOPENMW_USE_SYSTEM_YAML_CPP=OFF \ +-DOPENMW_USE_SYSTEM_ICU=OFF \ +-DOPENMW_ICU_HOST_BUILD_DIR="$(pwd)/icu-host-build" \ .. diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 6df3dc32e0..9fce76b7e8 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -1,44 +1,109 @@ -#!/bin/bash -ex +#!/bin/bash + +set -xeo pipefail free -m +# Silence a git warning +git config --global advice.detachedHead false + +BUILD_UNITTESTS=OFF +BUILD_BENCHMARKS=OFF + if [[ "${BUILD_TESTS_ONLY}" ]]; then - export GOOGLETEST_DIR="$(pwd)/googletest/build/install" + export GOOGLETEST_DIR="${PWD}/googletest/build/install" env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_googletest.sh + 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}" + -DCMAKE_CXX_COMPILER="${CXX:-/usr/bin/c++}" + -DCMAKE_C_COMPILER_LAUNCHER=ccache + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + -DCMAKE_INSTALL_PREFIX=install + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DBUILD_SHARED_LIBS=OFF + -DUSE_SYSTEM_TINYXML=ON + -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=ON + -DOPENMW_CXX_FLAGS="-Werror -Werror=implicit-fallthrough" # flags specific to OpenMW project +) + +if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then + CMAKE_CONF_OPTS+=( + -DOPENMW_USE_SYSTEM_MYGUI=OFF + -DOPENMW_USE_SYSTEM_OSG=OFF + -DOPENMW_USE_SYSTEM_BULLET=OFF + -DOPENMW_USE_SYSTEM_SQLITE3=OFF + -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=OFF + ) +fi + +if [[ $CI_CLANG_TIDY ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" + ) fi -mkdir build + +if [[ "${CMAKE_BUILD_TYPE}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ) +fi + +if [[ "${CMAKE_CXX_FLAGS_DEBUG}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_FLAGS_DEBUG="${CMAKE_CXX_FLAGS_DEBUG}" + ) +fi + +if [[ "${CMAKE_EXE_LINKER_FLAGS}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" + ) +fi + +if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then + CMAKE_CONF_OPTS+=( + -DBUILD_WITH_CODE_COVERAGE="${BUILD_WITH_CODE_COVERAGE}" + ) +fi + +mkdir -p build cd build if [[ "${BUILD_TESTS_ONLY}" ]]; then + + # flags specific to our test suite + CXX_FLAGS="-Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" + if [[ "${CXX}" == 'clang++' ]]; then + CXX_FLAGS="${CXX_FLAGS} -Wno-error=unused-lambda-capture -Wno-error=gnu-zero-variadic-macro-arguments" + fi + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" + ) + ${ANALYZE} cmake \ - -D CMAKE_C_COMPILER="${CC}" \ - -D CMAKE_CXX_COMPILER="${CXX}" \ - -D CMAKE_C_COMPILER_LAUNCHER=ccache \ - -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -D CMAKE_INSTALL_PREFIX=install \ - -D CMAKE_BUILD_TYPE=RelWithDebInfo \ - -D USE_SYSTEM_TINYXML=TRUE \ - -D BUILD_OPENMW=OFF \ - -D BUILD_BSATOOL=OFF \ - -D BUILD_ESMTOOL=OFF \ - -D BUILD_LAUNCHER=OFF \ - -D BUILD_MWINIIMPORTER=OFF \ - -D BUILD_ESSIMPORTER=OFF \ - -D BUILD_OPENCS=OFF \ - -D BUILD_WIZARD=OFF \ - -D BUILD_UNITTESTS=ON \ - -D GTEST_ROOT="${GOOGLETEST_DIR}" \ - -D GMOCK_ROOT="${GOOGLETEST_DIR}" \ + "${CMAKE_CONF_OPTS[@]}" \ + -DBUILD_OPENMW=OFF \ + -DBUILD_BSATOOL=OFF \ + -DBUILD_ESMTOOL=OFF \ + -DBUILD_LAUNCHER=OFF \ + -DBUILD_MWINIIMPORTER=OFF \ + -DBUILD_ESSIMPORTER=OFF \ + -DBUILD_OPENCS=OFF \ + -DBUILD_WIZARD=OFF \ + -DBUILD_NAVMESHTOOL=OFF \ + -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \ + -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ + -DGTEST_ROOT="${GOOGLETEST_DIR}" \ + -DGMOCK_ROOT="${GOOGLETEST_DIR}" \ .. else ${ANALYZE} cmake \ - -D CMAKE_C_COMPILER="${CC}" \ - -D CMAKE_CXX_COMPILER="${CXX}" \ - -D CMAKE_C_COMPILER_LAUNCHER=ccache \ - -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -D USE_SYSTEM_TINYXML=TRUE \ - -D CMAKE_INSTALL_PREFIX=install \ - -D CMAKE_BUILD_TYPE=Debug \ + "${CMAKE_CONF_OPTS[@]}" \ .. fi diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 5d41691493..ec5e9a7da8 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -62,6 +62,7 @@ VERBOSE="" STRIP="" SKIP_DOWNLOAD="" SKIP_EXTRACT="" +USE_CCACHE="" KEEP="" UNITY_BUILD="" VS_VERSION="" @@ -73,9 +74,10 @@ CONFIGURATIONS=() TEST_FRAMEWORK="" GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." -BULLET_DOUBLE="" -BULLET_DBL="" -BULLET_DBL_DISPLAY="Single precision" +BUILD_BENCHMARKS="" +OSG_MULTIVIEW_BUILD="" +USE_WERROR="" +USE_CLANG_TIDY="" ACTIVATE_MSVC="" SINGLE_CONFIG="" @@ -99,12 +101,11 @@ while [ $# -gt 0 ]; do d ) SKIP_DOWNLOAD=true ;; - D ) - BULLET_DOUBLE=true ;; - e ) SKIP_EXTRACT=true ;; + C ) + USE_CCACHE=true ;; k ) KEEP=true ;; @@ -117,7 +118,7 @@ while [ $# -gt 0 ]; do n ) NMAKE=true ;; - + N ) NINJA=true ;; @@ -139,6 +140,18 @@ while [ $# -gt 0 ]; do INSTALL_PREFIX=$(echo "$1" | sed 's;\\;/;g' | sed -E 's;/+;/;g') shift ;; + b ) + BUILD_BENCHMARKS=true ;; + + M ) + OSG_MULTIVIEW_BUILD=true ;; + + E ) + USE_WERROR=true ;; + + T ) + USE_CLANG_TIDY=true ;; + h ) cat < /dev/null && \ grep "MYGUI_VERSION_MINOR 4" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ - grep "MYGUI_VERSION_PATCH 0" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null + grep "MYGUI_VERSION_PATCH 1" 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.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP - mv "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}" MyGUI + eval 7z x -y "${DEPS}/MyGUI-3.4.1-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP + [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.1-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP + mv "MyGUI-3.4.1-msvc${MSVC_REAL_YEAR}-win${BITS}" MyGUI fi export MYGUI_HOME="$(real_pwd)/MyGUI" for CONFIGURATION in ${CONFIGURATIONS[@]}; do @@ -762,8 +819,8 @@ printf "OpenAL-Soft 1.20.1... " } cd $DEPS echo -# OSG -printf "OSG 3.6.5... " +# OSGoS +printf "${OSG_ARCHIVE_NAME}... " { cd $DEPS_INSTALL if [ -d OSG ] && \ @@ -774,9 +831,9 @@ printf "OSG 3.6.5... " printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf OSG - eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP - mv "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}" 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" @@ -786,8 +843,13 @@ printf "OSG 3.6.5... " else SUFFIX="" fi - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ - "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll + if ! [ -z $OSG_MULTIVIEW_BUILD ]; then + add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{ot21-OpenThreads,zlib,libpng16}${SUFFIX}.dll \ + "$(pwd)/OSG/bin/osg162-osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll + else + add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ + "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll + fi add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,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 done @@ -796,113 +858,92 @@ printf "OSG 3.6.5... " cd $DEPS echo # Qt -if [ -z $APPVEYOR ]; then - printf "Qt 5.15.0... " -else - printf "Qt 5.13 AppVeyor... " -fi +printf "Qt 5.15.2... " { if [ $BITS -eq 64 ]; then SUFFIX="_64" else SUFFIX="" fi - if [ -z $APPVEYOR ]; then - cd $DEPS_INSTALL - qt_version="5.15.0" - if [ "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" == "win64_msvc2017_64" ]; then - echo "This combination of options is known not to work. Falling back to Qt 5.14.2." - qt_version="5.14.2" - fi + cd $DEPS_INSTALL - QT_SDK="$(real_pwd)/Qt/${qt_version}/msvc${MSVC_REAL_YEAR}${SUFFIX}" + qt_version="5.15.2" - if [ -d "Qt/${qt_version}" ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - if [ $MISSINGPYTHON -ne 0 ]; then - echo "Can't be automatically installed without Python." - wrappedExit 1 - fi + QT_SDK="$(real_pwd)/Qt/${qt_version}/msvc${MSVC_REAL_YEAR}${SUFFIX}" - 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 + if [ -d "Qt/${qt_version}" ]; then + printf "Exists. " + elif [ -z $SKIP_EXTRACT ]; then + if [ $MISSINGPYTHON -ne 0 ]; then + echo "Can't be automatically installed without Python." + wrappedExit 1 + fi - if ! [ -e "aqt-venv/${VENV_BIN_DIR}/aqt" ]; then - echo " Installing aqt wheel into virtualenv..." - run_cmd "aqt-venv/${VENV_BIN_DIR}/pip" install aqtinstall==0.9.2 - fi - popd > /dev/null + 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 - rm -rf Qt + # 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 + fi + popd > /dev/null - mkdir Qt - cd Qt + rm -rf Qt - run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install $qt_version windows desktop "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" + mkdir Qt + cd Qt - printf " Cleaning up extraneous data... " - rm -rf Qt/{aqtinstall.log,Tools} + run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install $qt_version windows desktop "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" - echo Done. - fi + printf " Cleaning up extraneous data... " + rm -rf Qt/{aqtinstall.log,Tools} - 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 - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll - add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" - done - echo Done. - else - QT_SDK="C:/Qt/5.13/msvc2017${SUFFIX}" - 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 - DIR=$(windowsPathAsUnix "${QT_SDK}") - add_runtime_dlls $CONFIGURATION "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll - add_qt_platform_dlls $CONFIGURATION "${DIR}/plugins/platforms/qwindows${DLLSUFFIX}.dll" - done echo Done. fi + + 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 + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll + 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 # SDL2 -printf "SDL 2.0.12... " +printf "SDL 2.0.22... " { - if [ -d SDL2-2.0.12 ]; then + if [ -d SDL2-2.0.22 ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then - rm -rf SDL2-2.0.12 - eval 7z x -y SDL2-2.0.12.zip $STRIP + rm -rf SDL2-2.0.22 + eval 7z x -y SDL2-2.0.22.zip $STRIP fi - export SDL2DIR="$(real_pwd)/SDL2-2.0.12" + export SDL2DIR="$(real_pwd)/SDL2-2.0.22" for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll" + add_runtime_dlls $config "$(pwd)/SDL2-2.0.22/lib/x${ARCHSUFFIX}/SDL2.dll" done echo Done. } @@ -915,7 +956,7 @@ printf "LZ4 1.9.2... " printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf LZ4_1.9.2 - eval 7z x -y lz4_win${BITS}_v1_9_2.7z -o./LZ4_1.9.2 $STRIP + eval 7z x -y lz4_win${BITS}_v1_9_2.7z -o$(real_pwd)/LZ4_1.9.2 $STRIP fi export LZ4DIR="$(real_pwd)/LZ4_1.9.2" add_cmake_opts -DLZ4_INCLUDE_DIR="${LZ4DIR}/include" \ @@ -933,9 +974,28 @@ printf "LZ4 1.9.2... " } cd $DEPS echo +# LuaJIT 2.1.0-beta3 +printf "LuaJIT 2.1.0-beta3... " +{ + if [ -d LuaJIT ]; then + printf "Exists. " + elif [ -z $SKIP_EXTRACT ]; then + rm -rf LuaJIT + eval 7z x -y LuaJIT-2.1.0-beta3-msvc${MSVC_REAL_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" + done + echo Done. +} +cd $DEPS +echo # Google Test and Google Mock -if [ ! -z $TEST_FRAMEWORK ]; then - printf "Google test 1.10.0 ..." +if [ -n "$TEST_FRAMEWORK" ]; then + printf "Google test 1.11.0 ..." cd googletest mkdir -p build${MSVC_REAL_YEAR} @@ -987,12 +1047,40 @@ if [ ! -z $TEST_FRAMEWORK ]; then fi +cd $DEPS +echo +# ICU +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. +} + echo cd $DEPS_INSTALL/.. echo echo "Setting up OpenMW build..." add_cmake_opts -DOPENMW_MP_BUILD=on add_cmake_opts -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" +add_cmake_opts -DOPENMW_USE_SYSTEM_SQLITE3=OFF +add_cmake_opts -DOPENMW_USE_SYSTEM_YAML_CPP=OFF if [ ! -z $CI ]; then case $STEP in components ) @@ -1062,14 +1150,31 @@ fi cp "$DLL" "${DLL_PREFIX}platforms" done echo + echo "- Qt Style DLLs..." + mkdir -p ${DLL_PREFIX}styles + for DLL in ${QT_STYLES[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}styles" + done + echo done #fi +if [ "${BUILD_BENCHMARKS}" ]; then + add_cmake_opts -DBUILD_BENCHMARKS=ON +fi + if [ -n "$ACTIVATE_MSVC" ]; then echo -n "- Activating MSVC in the current shell... " command -v vswhere >/dev/null 2>&1 || { echo "Error: vswhere is not on the path."; wrappedExit 1; } - MSVC_INSTALLATION_PATH=$(vswhere -products '*' -version "[$MSVC_REAL_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath) + # There are so many arguments now that I'm going to document them: + # * products: allow Visual Studio or standalone build tools + # * version: obvious. Awk helps make a version range by adding one. + # * property installationPath: only give the installation path. + # * latest: return only one result if several candidates exist. Prefer the last installed/updated + # * requires: make sure it's got the MSVC compiler instead of, for example, just the .NET compiler. The .x86.x64 suffix means it's for either, not that it's the x64 on x86 cross compiler as you always get both + MSVC_INSTALLATION_PATH=$(vswhere -products '*' -version "[$MSVC_REAL_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64) if [ -z "$MSVC_INSTALLATION_PATH" ]; then echo "vswhere was unable to find MSVC $MSVC_DISPLAY_YEAR" wrappedExit 1 diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 8f9be16e1a..cd293a9cc9 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -3,8 +3,11 @@ export CXX=clang++ export CC=clang +# Silence a git warning +git config --global advice.detachedHead false + DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps" -QT_PATH=$(brew --prefix qt) +QT_PATH=$(brew --prefix qt@5) CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache mkdir build cd build @@ -16,15 +19,16 @@ cmake \ -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="10.12" \ +-D CMAKE_OSX_DEPLOYMENT_TARGET="10.15" \ -D CMAKE_BUILD_TYPE=RELEASE \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ +-D OPENMW_USE_SYSTEM_SQLITE3=OFF \ -D BUILD_OPENMW=TRUE \ -D BUILD_OPENCS=TRUE \ -D BUILD_ESMTOOL=TRUE \ -D BUILD_BSATOOL=TRUE \ -D BUILD_ESSIMPORTER=TRUE \ -D BUILD_NIFTEST=TRUE \ --D BULLET_USE_DOUBLES=TRUE \ +-D ICU_ROOT="/usr/local/opt/icu4c" \ -G"Unix Makefiles" \ .. diff --git a/CI/build_googletest.sh b/CI/build_googletest.sh index a9a50fee7a..8da5b44232 100755 --- a/CI/build_googletest.sh +++ b/CI/build_googletest.sh @@ -1,6 +1,6 @@ #!/bin/sh -ex -git clone -b release-1.10.0 https://github.com/google/googletest.git +git clone -b release-1.11.0 https://github.com/google/googletest.git cd googletest mkdir build cd build diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh new file mode 100755 index 0000000000..8342d89722 --- /dev/null +++ b/CI/install_debian_deps.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +set -euo pipefail + +print_help() { + echo "usage: $0 [group]..." + echo + echo " available groups: "${!GROUPED_DEPS[@]}"" +} + +declare -rA GROUPED_DEPS=( + [gcc]="binutils gcc build-essential cmake ccache curl unzip git pkg-config" + [clang]="binutils clang make cmake ccache curl unzip git pkg-config" + + # Common dependencies for building OpenMW. + [openmw-deps]=" + libboost-filesystem-dev libboost-program-options-dev + 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 + " + + # These dependencies can alternatively be built and linked statically. + [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev libsqlite3-dev" + [clang-tidy]="clang-tidy" + + # Pre-requisites for building MyGUI and OSG for static linking. + # + # * MyGUI and OSG: libsdl2-dev liblz4-dev libfreetype6-dev + # * OSG: libgl-dev + # + # Plugins: + # * DAE: libcollada-dom-dev libboost-system-dev libboost-filesystem-dev + # * JPEG: libjpeg-dev + # * PNG: libpng-dev + [openmw-deps-static]=" + libcollada-dom-dev libfreetype6-dev libjpeg-dev libpng-dev + libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev + " + + [openmw-coverage]="gcovr" + + [openmw-integration-tests]=" + ca-certificates + git + git-lfs + libavcodec58 + libavformat58 + libavutil56 + libboost-filesystem1.71.0 + libboost-iostreams1.71.0 + libboost-program-options1.71.0 + libboost-system1.71.0 + libbullet2.88 + libcollada-dom2.4-dp0 + libicu66 + libjpeg8 + libluajit-5.1-2 + liblz4-1 + libmyguiengine3debian1v5 + libopenal1 + libopenscenegraph161 + libpng16-16 + libqt5opengl5 + librecast1 + libsdl2-2.0-0 + libsqlite3-0 + libswresample3 + libswscale5 + libtinyxml2.6.2v5 + libyaml-cpp0.6 + python3-pip + xvfb + " +) + +if [[ $# -eq 0 ]]; then + >&2 print_help + exit 1 +fi + +deps=() +for group in "$@"; do + if [[ ! -v GROUPED_DEPS[$group] ]]; then + >&2 echo "error: unknown group ${group}" + exit 1 + fi + deps+=(${GROUPED_DEPS[$group]}) +done + +export APT_CACHE_DIR="${PWD}/apt-cache" +set -x +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 >/dev/null +add-apt-repository -y ppa:openmw/openmw +apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh new file mode 100755 index 0000000000..d7b025df52 --- /dev/null +++ b/CI/run_integration_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash -ex + +git clone --depth=1 https://gitlab.com/OpenMW/example-suite.git + +xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ + scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ + +ls integration_tests_output/*.osg_stats.log | while read v; do + scripts/osg_stats.py --stats '.*' --regexp_match < "${v}" +done diff --git a/CMakeLists.txt b/CMakeLists.txt index 37696f44c4..96092a2f61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,37 @@ -project(OpenMW) cmake_minimum_required(VERSION 3.1.0) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) # for link time optimization, remove if cmake version is >= 3.9 -if(POLICY CMP0069) +if(POLICY CMP0069) # LTO cmake_policy(SET CMP0069 NEW) endif() +# for position-independent executable, remove if cmake version is >= 3.14 +if(POLICY CMP0083) + cmake_policy(SET CMP0083 NEW) +endif() + +# to link with freetype library +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + +# don't add /W3 flag by default for MSVC +if(POLICY CMP0092) + cmake_policy(SET CMP0092 NEW) +endif() + +project(OpenMW) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(GNUInstallDirs) + +option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF) +if(OPENMW_GL4ES_MANUAL_INIT) + add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) +endif() + # Apps and tools option(BUILD_OPENMW "Build OpenMW" ON) option(BUILD_LAUNCHER "Build Launcher" ON) @@ -21,7 +45,9 @@ 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(BULLET_USE_DOUBLES "Use double precision for Bullet" 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) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. @@ -49,14 +75,13 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) if (ANDROID) set(CMAKE_FIND_ROOT_PATH ${OPENMW_DEPENDENCIES_DIR} "${CMAKE_FIND_ROOT_PATH}") - set (OSG_PLUGINS_DIR CACHE STRING "") endif() # Version message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 47) +set(OPENMW_VERSION_MINOR 48) set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_COMMITHASH "") @@ -92,17 +117,53 @@ endif(EXISTS ${PROJECT_SOURCE_DIR}/.git) # Macros include(OpenMWMacros) +include(WholeArchive) # doxygen main page configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") -option(MYGUI_STATIC "Link static build of Mygui into the binaries" FALSE) 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(OSG_STATIC "Link static build of OpenSceneGraph 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) +if(OPENMW_USE_SYSTEM_BULLET) + set(_bullet_static_default OFF) +else() + set(_bullet_static_default ON) +endif() +option(BULLET_STATIC "Link static build of Bullet into the binaries" ${_bullet_static_default}) + +option(OPENMW_USE_SYSTEM_OSG "Use system provided OpenSceneGraph libraries" ON) +if(OPENMW_USE_SYSTEM_OSG) + set(_osg_static_default OFF) +else() + set(_osg_static_default ON) +endif() +option(OSG_STATIC "Link static build of OpenSceneGraph into the binaries" ${_osg_static_default}) + +option(OPENMW_USE_SYSTEM_MYGUI "Use system provided mygui library" ON) +if(OPENMW_USE_SYSTEM_MYGUI) + set(_mygui_static_default OFF) +else() + set(_mygui_static_default ON) +endif() +option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_static_default}) + +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) +else() + set(_recastnavigation_static_default ON) +endif() +option(RECASTNAVIGATION_STATIC "Build recastnavigation static libraries" ${_recastnavigation_static_default}) + +option(OPENMW_USE_SYSTEM_SQLITE3 "Use system provided SQLite3 library" ON) + +option(OPENMW_USE_SYSTEM_BENCHMARK "Use system Google Benchmark 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) @@ -118,18 +179,27 @@ option(OPENMW_OSX_DEPLOYMENT OFF) if (MSVC) option(OPENMW_MP_BUILD "Build OpenMW with /MP flag" OFF) + if (OPENMW_MP_BUILD) + add_compile_options(/MP) + endif() + + # \bigobj is required: + # 1) for OPENMW_UNITY_BUILD; + # 2) to compile lua bindings in components, openmw and tests, because sol3 is heavily templated. + # 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) endif() # Set up common paths if (APPLE) - set(MORROWIND_DATA_FILES "./data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "../Resources/resources" CACHE PATH "location of OpenMW resources files") elseif(UNIX) # Paths SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "Where to install libraries") - SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") - SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") + SET(DATAROOTDIR "${CMAKE_INSTALL_DATAROOTDIR}" CACHE PATH "Sets the root of data directories to a non-default location") + SET(GLOBAL_DATA_PATH "${CMAKE_INSTALL_PREFIX}/share/games/" CACHE PATH "Set data path prefix") SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") @@ -140,10 +210,8 @@ elseif(UNIX) ENDIF() SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") - set(MORROWIND_DATA_FILES "${DATADIR}/data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "${DATADIR}/resources" CACHE PATH "location of OpenMW resources files") else() - set(MORROWIND_DATA_FILES "data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") endif(APPLE) @@ -151,6 +219,10 @@ 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) endif() +if(MSVC) + add_compile_options("/utf-8") +endif() + # Dependencies find_package(OpenGL REQUIRED) @@ -161,10 +233,47 @@ if (USE_QT) find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5OpenGL REQUIRED) - # Instruct CMake to run moc automatically when needed. - #set(CMAKE_AUTOMOC ON) endif() +set(USED_OSG_COMPONENTS + osgDB + osgViewer + osgText + osgGA + osgParticle + osgUtil + osgFX + osgShadow + osgAnimation) +set(USED_OSG_PLUGINS + osgdb_bmp + osgdb_dds + osgdb_freetype + osgdb_jpeg + osgdb_osg + osgdb_png + osgdb_serializers_osg + osgdb_tga) + + +option(OPENMW_USE_SYSTEM_ICU "Use system ICU library instead of internal. If disabled, requires autotools" ON) +if(OPENMW_USE_SYSTEM_ICU) + find_package(ICU REQUIRED COMPONENTS uc i18n data) +endif() + +option(OPENMW_USE_SYSTEM_YAML_CPP "Use system yaml-cpp library instead of internal." ON) +if(OPENMW_USE_SYSTEM_YAML_CPP) + set(_yaml_cpp_static_default OFF) +else() + set(_yaml_cpp_static_default ON) +endif() +option(YAML_CPP_STATIC "Link static build of yaml-cpp into the binaries" ${_yaml_cpp_static_default}) +if (OPENMW_USE_SYSTEM_YAML_CPP) + find_package(yaml-cpp REQUIRED) +endif() + +add_subdirectory(extern) + # Sound setup # Require at least ffmpeg 3.2 for now @@ -198,16 +307,16 @@ if(FFmpeg_FOUND) set(FFVER_OK FALSE) endif() endif() + + if(NOT FFVER_OK AND NOT APPLE) # unable to detect on version on MacOS < 11.0 + message(FATAL_ERROR "FFmpeg version is too old, 3.2 is required" ) + endif() endif() if(NOT FFmpeg_FOUND) message(FATAL_ERROR "FFmpeg was not found" ) endif() -if(NOT FFVER_OK) - message(FATAL_ERROR "FFmpeg version is too old, 3.2 is required" ) -endif() - if(WIN32) message("Can not detect FFmpeg version, at least the 3.2 is required" ) endif() @@ -237,7 +346,45 @@ if (WIN32) add_definitions(-DSDL_MAIN_HANDLED) # Get rid of useless crud from windows.h - add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) + add_definitions( + -DNOMINMAX # name conflict with std::min, std::max + -DWIN32_LEAN_AND_MEAN + -DNOMB # name conflict with MWGui::MessageBox + -DNOGDI # name conflict with osgAnimation::MorphGeometry::RELATIVE + ) +endif() + +if(OPENMW_USE_SYSTEM_BULLET) + set(REQUIRED_BULLET_VERSION 286) # Bullet 286 required due to runtime bugfixes for btCapsuleShape + if (DEFINED ENV{TRAVIS_BRANCH} OR DEFINED ENV{APPVEYOR}) + set(REQUIRED_BULLET_VERSION 283) # but for build testing, 283 is fine + endif() + + # First, try BulletConfig-float64.cmake which comes with Debian derivatives. + # This file does not define the Bullet version in a CMake-friendly way. + find_package(Bullet CONFIGS BulletConfig-float64.cmake QUIET COMPONENTS BulletCollision LinearMath) + if (BULLET_FOUND) + string(REPLACE "." "" _bullet_version_num ${BULLET_VERSION_STRING}) + if (_bullet_version_num VERSION_LESS REQUIRED_BULLET_VERSION) + message(FATAL_ERROR "System bullet version too old, OpenMW requires at least ${REQUIRED_BULLET_VERSION}, got ${_bullet_version_num}") + endif() + # Fix the relative include: + set(BULLET_INCLUDE_DIRS "${BULLET_ROOT_DIR}/${BULLET_INCLUDE_DIRS}") + include(FindPackageMessage) + find_package_message(Bullet "Found Bullet: ${BULLET_LIBRARIES} ${BULLET_VERSION_STRING}" "${BULLET_VERSION_STRING}-float64") + else() + find_package(Bullet ${REQUIRED_BULLET_VERSION} REQUIRED COMPONENTS BulletCollision LinearMath) + endif() + + # Only link the Bullet libraries that we need: + string(REGEX MATCHALL "((optimized|debug);)?[^;]*(BulletCollision|LinearMath)[^;]*" BULLET_LIBRARIES "${BULLET_LIBRARIES}") + + include(cmake/CheckBulletPrecision.cmake) + if (HAS_DOUBLE_PRECISION_BULLET) + message(STATUS "Bullet uses double precision") + else() + message(FATAL_ERROR "Bullet does not uses double precision") + endif() endif() if (NOT WIN32 AND BUILD_WIZARD) # windows users can just run the morrowind installer @@ -258,42 +405,35 @@ if(NOT HAVE_STDINT_H) message(FATAL_ERROR "stdint.h was not found" ) endif() +if(OPENMW_USE_SYSTEM_OSG) + find_package(OpenSceneGraph 3.4.0 REQUIRED ${USED_OSG_COMPONENTS}) + if (${OPENSCENEGRAPH_VERSION} VERSION_GREATER 3.6.2 AND ${OPENSCENEGRAPH_VERSION} VERSION_LESS 3.6.5) + message(FATAL_ERROR "OpenSceneGraph version ${OPENSCENEGRAPH_VERSION} has critical regressions which cause crashes. Please upgrade to 3.6.5 or later. We strongly recommend using the tip of the official 'OpenSceneGraph-3.6' branch or the tip of '3.6' OpenMW/osg (OSGoS).") + endif() -find_package(OpenSceneGraph 3.3.4 REQUIRED osgDB osgViewer osgText osgGA osgParticle osgUtil osgFX osgShadow) -include_directories(SYSTEM ${OPENSCENEGRAPH_INCLUDE_DIRS}) + if(OSG_STATIC) + find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) + endif() +endif() -set(USED_OSG_PLUGINS - osgdb_bmp - osgdb_dds - osgdb_freetype - osgdb_jpeg - osgdb_osg - osgdb_png - osgdb_serializers_osg - osgdb_tga - ) - -set(OSGPlugins_LIB_DIR "") -foreach(OSGDB_LIB ${OSGDB_LIBRARY}) - # Skip library type names - if(EXISTS ${OSGDB_LIB} AND NOT IS_DIRECTORY ${OSGDB_LIB}) - get_filename_component(OSG_LIB_DIR ${OSGDB_LIB} DIRECTORY) - list(APPEND OSGPlugins_LIB_DIR "${OSG_LIB_DIR}/osgPlugins-${OPENSCENEGRAPH_VERSION}") - endif() -endforeach(OSGDB_LIB) +include_directories(BEFORE SYSTEM ${OPENSCENEGRAPH_INCLUDE_DIRS}) if(OSG_STATIC) add_definitions(-DOSG_LIBRARY_STATIC) - - find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) - list(APPEND OPENSCENEGRAPH_LIBRARIES ${OSGPlugins_LIBRARIES}) endif() +include(cmake/CheckOsgMultiview.cmake) +if(HAVE_MULTIVIEW) + add_definitions(-DOSG_HAS_MULTIVIEW) +endif(HAVE_MULTIVIEW) + set(BOOST_COMPONENTS system filesystem program_options iostreams) if(WIN32) set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) if(MSVC) - set(BOOST_COMPONENTS ${BOOST_COMPONENTS} zlib) + # 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) @@ -301,26 +441,47 @@ IF(BOOST_STATIC) set(Boost_USE_STATIC_LIBS ON) endif() -set(REQUIRED_BULLET_VERSION 286) # Bullet 286 required due to runtime bugfixes for btCapsuleShape -if (DEFINED ENV{TRAVIS_BRANCH} OR DEFINED ENV{APPVEYOR}) - set(REQUIRED_BULLET_VERSION 283) # but for build testing, 283 is fine -endif() - set(Boost_NO_BOOST_CMAKE ON) -find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS}) -find_package(MyGUI 3.2.2 REQUIRED) +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() + +if(OPENMW_USE_SYSTEM_MYGUI) + find_package(MyGUI 3.4.1 REQUIRED) +endif() find_package(SDL2 2.0.9 REQUIRED) find_package(OpenAL REQUIRED) -find_package(Bullet ${REQUIRED_BULLET_VERSION} REQUIRED COMPONENTS BulletCollision LinearMath) -include_directories("." - SYSTEM +option(USE_LUAJIT "Switch Lua/LuaJit (TRUE is highly recommended)" TRUE) +if(USE_LUAJIT) + find_package(LuaJit REQUIRED) + set(LUA_INCLUDE_DIR ${LuaJit_INCLUDE_DIR}) + set(LUA_LIBRARIES ${LuaJit_LIBRARIES}) +else(USE_LUAJIT) + find_package(Lua REQUIRED) + add_compile_definitions(NO_LUAJIT) +endif(USE_LUAJIT) + +# C++ library binding to Lua +set(SOL_INCLUDE_DIR ${OpenMW_SOURCE_DIR}/extern/sol3) +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} ${BULLET_INCLUDE_DIRS} + ${LUA_INCLUDE_DIR} + ${SOL_INCLUDE_DIR} + ${SOL_CONFIG_DIR} + ${ICU_INCLUDE_DIRS} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) @@ -337,9 +498,8 @@ if (APPLE) "${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY) endif (APPLE) -if (NOT APPLE) - set(OPENMW_MYGUI_FILES_ROOT ${OpenMW_BINARY_DIR}) - set(OPENMW_SHADERS_ROOT ${OpenMW_BINARY_DIR}) +if (NOT APPLE) # this is modified for macOS use later in "apps/open[mw|cs]/CMakeLists.txt" + set(OPENMW_RESOURCES_ROOT ${OpenMW_BINARY_DIR}) endif () add_subdirectory(files/) @@ -360,8 +520,8 @@ endif (APPLE) # Other files -configure_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg - "${OpenMW_BINARY_DIR}" "settings-default.cfg") +pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg + "${OpenMW_BINARY_DIR}" "defaults.bin") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}" "openmw.appdata.xml") @@ -376,8 +536,8 @@ else () "${OpenMW_BINARY_DIR}/openmw.cfg") endif () -configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg - "${OpenMW_BINARY_DIR}" "openmw-cs.cfg") +pack_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg + "${OpenMW_BINARY_DIR}" "defaults-cs.bin") # Needs the copy version because the configure version assumes the end of the file has been reached when a null character is reached and there are no CMake expressions to evaluate. copy_resource_file(${OpenMW_SOURCE_DIR}/files/opencs/defaultfilters @@ -415,7 +575,7 @@ endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long") + set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long ${CMAKE_CXX_FLAGS}") add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) if (APPLE) @@ -432,27 +592,23 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.6) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") endif() - - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 5.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 5.0) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override") - endif() -elseif (MSVC) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) # Extern -set(RECASTNAVIGATION_STATIC ON CACHE BOOL "Build recastnavigation static libraries") -add_subdirectory (extern/recastnavigation EXCLUDE_FROM_ALL) add_subdirectory (extern/osg-ffmpeg-videoplayer) add_subdirectory (extern/oics) +add_subdirectory (extern/Base64) if (BUILD_OPENCS) add_subdirectory (extern/osgQt) endif() +if (OPENMW_CXX_FLAGS) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OPENMW_CXX_FLAGS}") +endif() + # Components add_subdirectory (components) -target_compile_definitions(components PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") # Apps and tools if (BUILD_OPENMW) @@ -496,12 +652,20 @@ if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) endif() +if (BUILD_BENCHMARKS) + add_subdirectory(apps/benchmarks) +endif() + +if (BUILD_NAVMESHTOOL) + add_subdirectory(apps/navmeshtool) +endif() + +if (BUILD_BULLETOBJECTTOOL) + add_subdirectory( apps/bulletobjecttool ) +endif() + if (WIN32) if (MSVC) - if (OPENMW_MP_BUILD) - set( MT_BUILD "/MP") - endif() - foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) @@ -526,62 +690,24 @@ if (WIN32) # Play a bit with the warning levels - set(WARNINGS "/Wall") # Since windows can only disable specific warnings, not enable them + set(WARNINGS "/W4") set(WARNINGS_DISABLE - # Warnings that aren't enabled normally and don't need to be enabled - # They're unneeded and sometimes completely retarded warnings that /Wall enables - # Not going to bother commenting them as they tend to warn on every standard library file - 4061 4263 4264 4266 4350 4371 4435 4514 4548 4571 4610 4619 4623 4625 - 4626 4628 4640 4668 4710 4711 4768 4820 4826 4917 4946 5032 5039 5045 - - # Warnings that are thrown on standard libraries and not OpenMW - 4347 # Non-template function with same name and parameter count as template function - 4365 # Variable signed/unsigned mismatch - 4510 4512 # Unable to generate copy constructor/assignment operator as it's not public in the base - 4706 # Assignment in conditional expression - 4738 # Storing 32-bit float result in memory, possible loss of performance - 4774 # Format string expected in argument is not a string literal - 4986 # Undocumented warning that occurs in the crtdbg.h file - 4987 # nonstandard extension used (triggered by setjmp.h) - 4996 # Function was declared deprecated - - # caused by OSG - 4589 # Constructor of abstract class 'osg::Operation' ignores initializer for virtual base class 'osg::Referenced' (False warning) - - # caused by boost - 4191 # 'type cast' : unsafe conversion (1.56, thread_primitives.hpp, normally off) - 4643 # Forward declaring 'X' in namespace std is not permitted by the C++ Standard. (in *_std_fwd.h files) - 5204 # Class has virtual functions, but its trivial destructor is not virtual - - # caused by MyGUI - 4275 # non dll-interface class 'std::exception' used as base for dll-interface class 'MyGUI::Exception' - 4297 # function assumed not to throw an exception but does - - # OpenMW specific warnings - 4099 # Type mismatch, declared class or struct is defined with other type 4100 # Unreferenced formal parameter (-Wunused-parameter) - 4101 # Unreferenced local variable (-Wunused-variable) 4127 # Conditional expression is constant - 4242 # Storing value in a variable of a smaller type, possible loss of data - 4244 # Storing value of one type in variable of another (size_t in int, for example) - 4245 # Signed/unsigned mismatch - 4267 # Conversion from 'size_t' to 'int', possible loss of data - 4305 # Truncating value (double to float, for example) - 4309 # Variable overflow, trying to store 128 in a signed char for example - 4351 # New behavior: elements of array 'array' will be default initialized (desired behavior) - 4355 # Using 'this' in member initialization list - 4464 # relative include path contains '..' - 4505 # Unreferenced local function has been removed - 4701 # Potentially uninitialized local variable used - 4702 # Unreachable code - 4714 # function 'QString QString::trimmed(void) &&' marked as __forceinline not inlined - 4800 # Boolean optimization warning, e.g. myBool = (myInt != 0) instead of myBool = myInt + 4996 # Function was declared deprecated + 5054 # Deprecated operations between enumerations of different types caused by Qt headers + ) + + if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.0" ) + set(WARNINGS_DISABLE ${WARNINGS_DISABLE} + 4866 # compiler may not enforce left-to-right evaluation order for call ) + endif() - if (MSVC_VERSION GREATER 1800) - set(WARNINGS_DISABLE ${WARNINGS_DISABLE} 5026 5027 - 5031 # #pragma warning(pop): likely mismatch, popping warning state pushed in different file (config_begin.hpp, config_end.hpp) + if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.1" ) + set(WARNINGS_DISABLE ${WARNINGS_DISABLE} + 4275 # non dll-interface class 'MyGUI::delegates::IDelegateUnlink' used as base for dll-interface class 'MyGUI::Widget' ) endif() @@ -589,47 +715,64 @@ if (WIN32) set(WARNINGS "${WARNINGS} /wd${d}") endforeach(d) - set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + if(OPENMW_MSVC_WERROR) + set(WARNINGS "${WARNINGS} /WX") + endif() + + set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS}") + set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${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} ${MT_BUILD}") + set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_ESMTOOL) - set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_ESSIMPORTER) - set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_LAUNCHER) - set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_MWINIIMPORTER) - set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_OPENCS) - set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_OPENMW) - if (OPENMW_UNITY_BUILD) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") - else() - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() if (BUILD_WIZARD) - set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS}") + endif() + + if (BUILD_UNITTESTS) + set_target_properties(openmw_test_suite PROPERTIES COMPILE_FLAGS "${WARNINGS}") + endif() + + if (BUILD_BENCHMARKS) + set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS}") + endif() + + if (BUILD_NAVMESHTOOL) + set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS}") + endif() + + if (BUILD_BULLETOBJECTTOOL) + set(WARNINGS "${WARNINGS} ${MT_BUILD}") + set_target_properties(openmw-bulletobjecttool PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() endif(MSVC) @@ -638,6 +781,15 @@ if (WIN32) #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() +if (BUILD_OPENMW AND APPLE) + if (USE_LUAJIT) + # Without these flags LuaJit crashes on startup on OSX + set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") + endif(USE_LUAJIT) + target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1) + target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1) +endif() + # Apple bundling if (OPENMW_OSX_DEPLOYMENT AND APPLE) if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4) @@ -769,8 +921,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}/files/mygui/DejaVuFontLicense.txt" DESTINATION ".") - INSTALL(FILES "${INSTALL_SOURCE}/settings-default.cfg" DESTINATION ".") + INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION ".") @@ -812,13 +963,13 @@ elseif(NOT APPLE) SET(VCREDIST32 "${OpenMW_BINARY_DIR}/vcredist_x86.exe") if(EXISTS ${VCREDIST32}) INSTALL(FILES ${VCREDIST32} DESTINATION "redist") - SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x86.exe\\\" /q'" ) + SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x86.exe\\\" /q /norestart'" ) endif(EXISTS ${VCREDIST32}) SET(VCREDIST64 "${OpenMW_BINARY_DIR}/vcredist_x64.exe") if(EXISTS ${VCREDIST64}) INSTALL(FILES ${VCREDIST64} DESTINATION "redist") - SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x64.exe\\\" /q'" ) + SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x64.exe\\\" /q /norestart'" ) endif(EXISTS ${VCREDIST64}) SET(OALREDIST "${OpenMW_BINARY_DIR}/oalinst.exe") @@ -838,11 +989,7 @@ elseif(NOT APPLE) # Install binaries IF(BUILD_OPENMW) - IF(ANDROID) - INSTALL(PROGRAMS "${INSTALL_SOURCE}/libopenmw.so" DESTINATION "${BINDIR}" ) - ELSE(ANDROID) - INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw" DESTINATION "${BINDIR}" ) - ENDIF(ANDROID) + INSTALL(PROGRAMS "$" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENMW) IF(BUILD_LAUNCHER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-launcher" DESTINATION "${BINDIR}" ) @@ -868,9 +1015,12 @@ elseif(NOT APPLE) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) - - # Install licenses - INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" ) + if(BUILD_NAVMESHTOOL) + install(PROGRAMS "${INSTALL_SOURCE}/openmw-navmeshtool" DESTINATION "${BINDIR}" ) + endif() + IF(BUILD_BULLETOBJECTTOOL) + INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-bulletobjecttool" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_BULLETOBJECTTOOL) # Install icon and desktop file INSTALL(FILES "${OpenMW_BINARY_DIR}/org.openmw.launcher.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw") @@ -882,13 +1032,13 @@ elseif(NOT APPLE) ENDIF(BUILD_OPENCS) # Install global configuration files - INSTALL(FILES "${INSTALL_SOURCE}/settings-default.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") + 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) - INSTALL(FILES "${INSTALL_SOURCE}/openmw-cs.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") + INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install resources diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 264db49cc1..4cdb164a3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,9 +22,9 @@ Pull Request Guidelines To facilitate the review process, your pull 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. -* Summary of the changes made -* Reasoning / motivation behind the change -* What testing you have carried out to verify 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: @@ -51,9 +51,9 @@ 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 that were fixed in an official patch for Morrowind +* 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 that were fixed in an official patch for Morrowind. Feature additions policy ===================== @@ -99,7 +99,7 @@ Code Review Merging ======= -To be able to merge PRs, commit priviledges are required. If you do not have the priviledges, just ping someone that does have them with a short comment like "Looks good to me @user". +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". 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. diff --git a/README.md b/README.md index 6aa0a85156..b1ea6cbde3 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ OpenMW ====== -[![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) [![pipeline status](https://gitlab.com/OpenMW/openmw/badges/master/pipeline.svg)](https://gitlab.com/OpenMW/openmw/commits/master) - -OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. +OpenMW is an open-source open-world RPG game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. -* Version: 0.47.0 -* License: GPLv3 (see [LICENSE](https://github.com/OpenMW/openmw/blob/master/LICENSE) for more information) +* Version: 0.48.0 +* License: GPLv3 (see [LICENSE](https://gitlab.com/OpenMW/openmw/-/raw/master/LICENSE) for more information) * Website: https://www.openmw.org -* IRC: #openmw on irc.freenode.net +* IRC: #openmw on irc.libera.chat +* Discord: https://discord.gg/bWuqq2e + Font Licenses: -* DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVuFontLicense.txt](https://github.com/OpenMW/openmw/blob/master/files/mygui/DejaVuFontLicense.txt) for more information) +* DejaVuLGCSansMono.ttf: custom (see [files/data/fonts/DejaVuFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/DejaVuFontLicense.txt) for more information) +* OMWAyembedt.ttf: SIL Open Font License (see [files/data/fonts/OMWAyembedtFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/OMWAyembedtFontLicense.txt) for more information) +* Pelagiad.ttf: SIL Open Font License (see [files/data/fonts/PelagiadFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/PelagiadFontLicense.txt) for more information) 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?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. 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. @@ -26,7 +28,7 @@ Getting Started --------------- * [Official forums](https://forum.openmw.org/) -* [Installation instructions](https://wiki.openmw.org/index.php?title=Installation_Instructions) +* [Installation instructions](https://openmw.readthedocs.io/en/latest/manuals/installation/index.html) * [Build from source](https://wiki.openmw.org/index.php?title=Development_Environment_Setup) * [Testing the game](https://wiki.openmw.org/index.php?title=Testing) * [How to contribute](https://wiki.openmw.org/index.php?title=Contribution_Wanted) diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt new file mode 100644 index 0000000000..095a841742 --- /dev/null +++ b/apps/benchmarks/CMakeLists.txt @@ -0,0 +1,15 @@ +if(OPENMW_USE_SYSTEM_BENCHMARK) + find_package(benchmark REQUIRED) +endif() + +openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark detournavigator/navmeshtilescache.cpp) +target_compile_features(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE cxx_std_17) +target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ) +endif() diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp new file mode 100644 index 0000000000..49b079681e --- /dev/null +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -0,0 +1,290 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace DetourNavigator; + + struct Key + { + AgentBounds mAgentBounds; + TilePosition mTilePosition; + RecastMesh mRecastMesh; + }; + + struct Item + { + Key mKey; + PreparedNavMeshData mValue; + }; + + template + osg::Vec2i generateVec2i(int max, Random& 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) + { + 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) + { + 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) + { + std::uniform_int_distribution distribution(0, max); + std::generate_n(out, number - number % 3, [&] { return distribution(random); }); + } + + AreaType toAreaType(int index) + { + switch (index) + { + case 0: return AreaType_null; + case 1: return AreaType_water; + case 2: return AreaType_door; + case 3: return AreaType_pathgrid; + case 4: return AreaType_ground; + } + return AreaType_null; + } + + template + AreaType generateAreaType(Random& random) + { + std::uniform_int_distribution distribution(0, 4); + return toAreaType(distribution(random)); + } + + template + void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + { + std::generate_n(out, triangles, [&] { return generateAreaType(random); }); + } + + template + void generateWater(OutputIterator out, std::size_t count, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, count, [&] { + return CellWater {generateVec2i(1000, random), Water {ESM::Land::REAL_SIZE, distribution(random)}}; + }); + } + + template + Mesh generateMesh(std::size_t triangles, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::vector vertices; + std::vector indices; + std::vector areaTypes; + if (distribution(random) < 0.939) + { + generateVertices(std::back_inserter(vertices), triangles * 2.467, random); + generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.279, random); + generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); + } + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + + template + Heightfield generateHeightfield(Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + Heightfield result; + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; + result.mMinHeight = distribution(random); + result.mMaxHeight = result.mMinHeight + 1.0; + result.mLength = static_cast(ESM::Land::LAND_SIZE); + std::generate_n(std::back_inserter(result.mHeights), ESM::Land::LAND_NUM_VERTS, [&] + { + return distribution(random); + }); + result.mOriginalSize = ESM::Land::LAND_SIZE; + result.mMinX = 0; + result.mMinY = 0; + return result; + } + + template + FlatHeightfield generateFlatHeightfield(Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + FlatHeightfield result; + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; + result.mHeight = distribution(random); + return result; + } + + template + Key generateKey(std::size_t triangles, Random& random) + { + const CollisionShapeType agentShapeType = CollisionShapeType::Aabb; + const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); + const TilePosition tilePosition = generateVec2i(10000, random); + const std::size_t generation = std::uniform_int_distribution(0, 100)(random); + const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); + Mesh mesh = generateMesh(triangles, random); + std::vector water; + generateWater(std::back_inserter(water), 1, random); + RecastMesh recastMesh(generation, revision, std::move(mesh), std::move(water), + {generateHeightfield(random)}, {generateFlatHeightfield(random)}, {}); + return Key {AgentBounds {agentShapeType, agentHalfExtents}, tilePosition, std::move(recastMesh)}; + } + + constexpr std::size_t trianglesPerTile = 239; + + template + void generateKeys(OutputIterator out, std::size_t count, Random& random) + { + std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); + } + + template + void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + { + std::size_t size = cache.getStats().mNavMeshCacheSize; + + while (true) + { + Key key = generateKey(trianglesPerTile, random); + cache.set(key.mAgentBounds, key.mTilePosition, key.mRecastMesh, + std::make_unique()); + *out++ = std::move(key); + const std::size_t newSize = cache.getStats().mNavMeshCacheSize; + if (size >= newSize) + break; + size = newSize; + } + } + + template + void getFromFilledCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); + benchmark::DoNotOptimize(result); + } + } + + void getFromFilledCache_1m_100hit(benchmark::State& state) + { + getFromFilledCache<1 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_4m_100hit(benchmark::State& state) + { + getFromFilledCache<4 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_16m_100hit(benchmark::State& state) + { + getFromFilledCache<16 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_64m_100hit(benchmark::State& state) + { + getFromFilledCache<64 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_1m_70hit(benchmark::State& state) + { + getFromFilledCache<1 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_4m_70hit(benchmark::State& state) + { + getFromFilledCache<4 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_16m_70hit(benchmark::State& state) + { + getFromFilledCache<16 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_64m_70hit(benchmark::State& state) + { + getFromFilledCache<64 * 1024 * 1024, 70>(state); + } + + template + void setToBoundedNonEmptyCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * 2, random); + std::reverse(keys.begin(), keys.end()); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.set(key.mAgentBounds, key.mTilePosition, key.mRecastMesh, + std::make_unique()); + benchmark::DoNotOptimize(result); + } + } + + void setToBoundedNonEmptyCache_1m(benchmark::State& state) + { + setToBoundedNonEmptyCache<1 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_4m(benchmark::State& state) + { + setToBoundedNonEmptyCache<4 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_16m(benchmark::State& state) + { + setToBoundedNonEmptyCache<16 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_64m(benchmark::State& state) + { + setToBoundedNonEmptyCache<64 * 1024 * 1024>(state); + } +} // namespace + +BENCHMARK(getFromFilledCache_1m_100hit); +BENCHMARK(getFromFilledCache_4m_100hit); +BENCHMARK(getFromFilledCache_16m_100hit); +BENCHMARK(getFromFilledCache_64m_100hit); +BENCHMARK(getFromFilledCache_1m_70hit); +BENCHMARK(getFromFilledCache_4m_70hit); +BENCHMARK(getFromFilledCache_16m_70hit); +BENCHMARK(getFromFilledCache_64m_70hit); +BENCHMARK(setToBoundedNonEmptyCache_1m); +BENCHMARK(setToBoundedNonEmptyCache_4m); +BENCHMARK(setToBoundedNonEmptyCache_16m); +BENCHMARK(setToBoundedNonEmptyCache_64m); + +BENCHMARK_MAIN(); diff --git a/apps/bsatool/CMakeLists.txt b/apps/bsatool/CMakeLists.txt index ec0615ff9c..6312c33aaf 100644 --- a/apps/bsatool/CMakeLists.txt +++ b/apps/bsatool/CMakeLists.txt @@ -10,7 +10,6 @@ openmw_add_executable(bsatool target_link_libraries(bsatool ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} components ) @@ -18,3 +17,11 @@ if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(bsatool gcov) endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(bsatool PRIVATE + + + + ) +endif() diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 3afbd777f8..a8e28fcfb3 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -1,10 +1,10 @@ +#include +#include #include #include #include #include -#include -#include #include #include @@ -13,13 +13,13 @@ // Create local aliases for brevity namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; struct Arguments { std::string mode; std::string filename; std::string extractfile; + std::string addfile; std::string outdir; bool longformat; @@ -36,6 +36,10 @@ bool parseOptions (int argc, char** argv, Arguments &info) " Extract a file from the input archive.\n\n" " bsatool extractall archivefile [output_directory]\n" " Extract all files from the input archive.\n\n" + " bsatool add [-a] archivefile file_to_add\n" + " Add a file to the input archive.\n\n" + " bsatool create [-c] archivefile\n" + " Create an archive.\n\n" "Allowed options"); desc.add_options() @@ -95,7 +99,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) } info.mode = variables["mode"].as(); - if (!(info.mode == "list" || info.mode == "extract" || info.mode == "extractall")) + if (!(info.mode == "list" || info.mode == "extract" || info.mode == "extractall" || info.mode == "add" || info.mode == "create")) { std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"\n\n" << desc << std::endl; @@ -126,6 +130,17 @@ bool parseOptions (int argc, char** argv, Arguments &info) if (variables["input-file"].as< std::vector >().size() > 2) info.outdir = variables["input-file"].as< std::vector >()[2]; } + else if (info.mode == "add") + { + if (variables["input-file"].as< std::vector >().size() < 1) + { + std::cout << "\nERROR: file to add unspecified\n\n" + << desc << std::endl; + return false; + } + if (variables["input-file"].as< std::vector >().size() > 1) + info.addfile = variables["input-file"].as< std::vector >()[1]; + } else if (variables["input-file"].as< std::vector >().size() > 1) info.outdir = variables["input-file"].as< std::vector >()[1]; @@ -135,72 +150,31 @@ bool parseOptions (int argc, char** argv, Arguments &info) return true; } -int list(std::unique_ptr& bsa, Arguments& info); -int extract(std::unique_ptr& bsa, Arguments& info); -int extractAll(std::unique_ptr& bsa, Arguments& info); - -int main(int argc, char** argv) -{ - try - { - Arguments info; - if(!parseOptions (argc, argv, info)) - return 1; - - // Open file - std::unique_ptr bsa; - - Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(info.filename); - - if (bsaVersion == Bsa::BSAVER_COMPRESSED) - bsa = std::make_unique(Bsa::CompressedBSAFile()); - else - bsa = std::make_unique(Bsa::BSAFile()); - - bsa->open(info.filename); - - if (info.mode == "list") - return list(bsa, info); - else if (info.mode == "extract") - return extract(bsa, info); - else if (info.mode == "extractall") - return extractAll(bsa, info); - else - { - std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; - return 1; - } - } - catch (std::exception& e) - { - std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; - return 2; - } -} - -int list(std::unique_ptr& bsa, Arguments& info) +template +int list(std::unique_ptr& bsa, Arguments& info) { // List all files - const Bsa::BSAFile::FileList &files = bsa->getList(); + const auto &files = bsa->getList(); for (const auto& file : files) { if(info.longformat) { // Long format std::ios::fmtflags f(std::cout.flags()); - std::cout << std::setw(50) << std::left << file.name; + std::cout << std::setw(50) << std::left << file.name(); std::cout << std::setw(8) << std::left << std::dec << file.fileSize; std::cout << "@ 0x" << std::hex << file.offset << std::endl; std::cout.flags(f); } else - std::cout << file.name << std::endl; + std::cout << file.name() << std::endl; } return 0; } -int extract(std::unique_ptr& bsa, Arguments& info) +template +int extract(std::unique_ptr& bsa, Arguments& info) { std::string archivePath = info.extractfile; Misc::StringUtils::replaceAll(archivePath, "/", "\\"); @@ -208,7 +182,17 @@ int extract(std::unique_ptr& bsa, Arguments& info) std::string extractPath = info.extractfile; Misc::StringUtils::replaceAll(extractPath, "\\", "/"); - if (!bsa->exists(archivePath.c_str())) + Files::IStreamPtr stream; + // Get a stream for the file to extract + for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) + { + if (Misc::StringUtils::ciEqual(std::string(it->name()), archivePath)) + { + stream = bsa->getFile(&*it); + break; + } + } + if (!stream) { std::cout << "ERROR: file '" << archivePath << "' not found\n"; std::cout << "In archive: " << info.filename << std::endl; @@ -216,29 +200,26 @@ int extract(std::unique_ptr& bsa, Arguments& info) } // Get the target path (the path the file will be extracted to) - bfs::path relPath (extractPath); - bfs::path outdir (info.outdir); + std::filesystem::path relPath (extractPath); + std::filesystem::path outdir (info.outdir); - bfs::path target; + std::filesystem::path target; if (info.fullpath) target = outdir / relPath; else target = outdir / relPath.filename(); // Create the directory hierarchy - bfs::create_directories(target.parent_path()); + std::filesystem::create_directories(target.parent_path()); - bfs::file_status s = bfs::status(target.parent_path()); - if (!bfs::is_directory(s)) + std::filesystem::file_status s = std::filesystem::status(target.parent_path()); + if (!std::filesystem::is_directory(s)) { std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; return 3; } - // Get a stream for the file to extract - Files::IStreamPtr stream = bsa->getFile(archivePath.c_str()); - - bfs::ofstream out(target, std::ios::binary); + std::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << info.extractfile << " to " << target << std::endl; @@ -249,31 +230,31 @@ int extract(std::unique_ptr& bsa, Arguments& info) return 0; } -int extractAll(std::unique_ptr& bsa, Arguments& info) +template +int extractAll(std::unique_ptr& bsa, Arguments& info) { for (const auto &file : bsa->getList()) { - std::string extractPath(file.name); + std::string extractPath(file.name()); Misc::StringUtils::replaceAll(extractPath, "\\", "/"); // Get the target path (the path the file will be extracted to) - bfs::path target (info.outdir); + std::filesystem::path target (info.outdir); target /= extractPath; // Create the directory hierarchy - bfs::create_directories(target.parent_path()); + std::filesystem::create_directories(target.parent_path()); - bfs::file_status s = bfs::status(target.parent_path()); - if (!bfs::is_directory(s)) + std::filesystem::file_status s = std::filesystem::status(target.parent_path()); + if (!std::filesystem::is_directory(s)) { std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; return 3; } // Get a stream for the file to extract - // (inefficient because getFile iter on the list again) - Files::IStreamPtr data = bsa->getFile(file.name); - bfs::ofstream out(target, std::ios::binary); + Files::IStreamPtr data = bsa->getFile(&file); + std::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << target << std::endl; @@ -283,3 +264,63 @@ int extractAll(std::unique_ptr& bsa, Arguments& info) return 0; } + +template +int add(std::unique_ptr& bsa, Arguments& info) +{ + std::fstream stream(info.addfile, std::ios_base::binary | std::ios_base::out | std::ios_base::in); + bsa->addFile(info.addfile, stream); + + return 0; +} + +template +int call(Arguments& info) +{ + std::unique_ptr bsa = std::make_unique(); + if (info.mode == "create") + { + bsa->open(info.filename); + return 0; + } + + bsa->open(info.filename); + + if (info.mode == "list") + return list(bsa, info); + else if (info.mode == "extract") + return extract(bsa, info); + else if (info.mode == "extractall") + return extractAll(bsa, info); + else if (info.mode == "add") + return add(bsa, info); + else + { + std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; + return 1; + } +} + +int main(int argc, char** argv) +{ + try + { + Arguments info; + if (!parseOptions(argc, argv, info)) + return 1; + + // Open file + + Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(info.filename); + + if (bsaVersion == Bsa::BSAVER_COMPRESSED) + return call(info); + else + return call(info); + } + catch (std::exception& e) + { + std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; + return 2; + } +} diff --git a/apps/bulletobjecttool/CMakeLists.txt b/apps/bulletobjecttool/CMakeLists.txt new file mode 100644 index 0000000000..bc19a29e21 --- /dev/null +++ b/apps/bulletobjecttool/CMakeLists.txt @@ -0,0 +1,27 @@ +set(BULLETMESHTOOL + main.cpp +) +source_group(apps\\bulletobjecttool FILES ${BULLETMESHTOOL}) + +openmw_add_executable(openmw-bulletobjecttool ${BULLETMESHTOOL}) + +target_link_libraries(openmw-bulletobjecttool + ${Boost_PROGRAM_OPTIONS_LIBRARY} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions(--coverage) + target_link_libraries(openmw-bulletobjecttool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-bulletobjecttool RUNTIME DESTINATION ".") +endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-bulletobjecttool PRIVATE + + + ) +endif() diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp new file mode 100644 index 0000000000..aad8c5e082 --- /dev/null +++ b/apps/bulletobjecttool/main.cpp @@ -0,0 +1,203 @@ +#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 + + +namespace +{ + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + + result.add_options() + ("help", "print help message") + + ("version", "print version information and quit") + + ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), + "set resources directory") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + + ("fallback", bpo::value()->default_value(FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + ; + + Files::ConfigurationManager::addCommonOptions(result); + + return result; + } + + struct WriteArray + { + const float (&mValue)[3]; + + friend std::ostream& operator <<(std::ostream& stream, const WriteArray& value) + { + for (std::size_t i = 0; i < 2; ++i) + stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[i] << ", "; + return stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[2]; + } + }; + + std::string toHex(std::string_view value) + { + std::string buffer(value.size() * 2, '0'); + char* out = buffer.data(); + for (const char v : value) + { + const std::ptrdiff_t space = static_cast(static_cast(v) <= 0xf); + const auto [ptr, ec] = std::to_chars(out + space, out + space + 2, static_cast(v), 16); + if (ec != std::errc()) + throw std::system_error(std::make_error_code(ec)); + out += 2; + } + return buffer; + } + + int runBulletObjectTool(int argc, char *argv[]) + { + Platform::init(); + + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv) + .options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + + bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); + config.readConfiguration(variables, desc); + Files::mergeComposingVariables(variables, composingVariables, desc); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.filterOutNonExistingPaths(dataDirs); + + const auto fsStrict = variables["fs-strict"].as(); + const auto resDir = variables["resources"].as(); + Version::Version v = Version::getOpenmwVersion(resDir.string()); + Log(Debug::Info) << v.describe(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const auto fileCollections = Files::Collections(dataDirs, !fsStrict); + const auto archives = variables["fallback-archive"].as(); + const auto contentFiles = variables["content"].as(); + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs(fsStrict); + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager settings; + settings.load(config); + + ESM::ReadersCache readers; + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + 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); + + Resource::forEachBulletObject(readers, vfs, bulletShapeManager, esmData, + [] (const ESM::Cell& cell, const Resource::BulletObject& object) + { + Log(Debug::Verbose) << "Found bullet object in " << (cell.isExterior() ? "exterior" : "interior") + << " cell \"" << cell.getDescription() << "\":" + << " fileName=\"" << object.mShape->mFileName << '"' + << " fileHash=" << toHex(object.mShape->mFileHash) + << " collisionShape=" << std::boolalpha << (object.mShape->mCollisionShape == nullptr) + << " avoidCollisionShape=" << std::boolalpha << (object.mShape->mAvoidCollisionShape == nullptr) + << " position=(" << WriteArray {object.mPosition.pos} << ')' + << " rotation=(" << WriteArray {object.mPosition.rot} << ')' + << " scale=" << std::setprecision(std::numeric_limits::max_exponent10) << object.mScale; + }); + + Log(Debug::Info) << "Done"; + + return 0; + } +} + +int main(int argc, char *argv[]) +{ + return wrapApplication(runBulletObjectTool, argc, argv, "BulletObjectTool"); +} diff --git a/apps/esmtool/CMakeLists.txt b/apps/esmtool/CMakeLists.txt index 122ca2f3af..841fb4b219 100644 --- a/apps/esmtool/CMakeLists.txt +++ b/apps/esmtool/CMakeLists.txt @@ -4,6 +4,9 @@ set(ESMTOOL labels.cpp record.hpp record.cpp + arguments.hpp + tes4.hpp + tes4.cpp ) source_group(apps\\esmtool FILES ${ESMTOOL}) @@ -21,3 +24,11 @@ if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(esmtool gcov) endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(esmtool PRIVATE + + + + ) +endif() diff --git a/apps/esmtool/arguments.hpp b/apps/esmtool/arguments.hpp new file mode 100644 index 0000000000..edd8c9b06c --- /dev/null +++ b/apps/esmtool/arguments.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_ESMTOOL_ARGUMENTS_H +#define OPENMW_ESMTOOL_ARGUMENTS_H + +#include +#include + +#include + +namespace EsmTool +{ + struct Arguments + { + std::optional mRawFormat; + bool quiet_given = false; + bool loadcells_given = false; + bool plain_given = false; + + std::string mode; + std::string encoding; + std::string filename; + std::string outname; + + std::vector types; + std::string name; + }; +} + +#endif diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 7cea574c83..9d90a542be 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -2,71 +2,46 @@ #include #include #include +#include #include -#include #include #include +#include +#include +#include #include -#include -#include +#include +#include #include +#include +#include #include "record.hpp" +#include "labels.hpp" +#include "arguments.hpp" +#include "tes4.hpp" + +namespace +{ -#define ESMTOOL_VERSION 1.2 +using namespace EsmTool; + +constexpr unsigned majorVersion = 1; +constexpr unsigned minorVersion = 3; // Create a local alias for brevity namespace bpo = boost::program_options; struct ESMData { - std::string author; - std::string description; - unsigned int version; - std::vector masters; - - std::deque mRecords; + ESM::Header mHeader; + std::deque> mRecords; // Value: (Reference, Deleted flag) std::map > > mCellRefs; std::map mRecordStats; - static const std::set sLabeledRec; -}; - -static const int sLabeledRecIds[] = { - ESM::REC_GLOB, ESM::REC_CLAS, ESM::REC_FACT, ESM::REC_RACE, ESM::REC_SOUN, - ESM::REC_REGN, ESM::REC_BSGN, ESM::REC_LTEX, ESM::REC_STAT, ESM::REC_DOOR, - ESM::REC_MISC, ESM::REC_WEAP, ESM::REC_CONT, ESM::REC_SPEL, ESM::REC_CREA, - ESM::REC_BODY, ESM::REC_LIGH, ESM::REC_ENCH, ESM::REC_NPC_, ESM::REC_ARMO, - ESM::REC_CLOT, ESM::REC_REPA, ESM::REC_ACTI, ESM::REC_APPA, ESM::REC_LOCK, - ESM::REC_PROB, ESM::REC_INGR, ESM::REC_BOOK, ESM::REC_ALCH, ESM::REC_LEVI, - ESM::REC_LEVC, ESM::REC_SNDG, ESM::REC_CELL, ESM::REC_DIAL -}; - -const std::set ESMData::sLabeledRec = - std::set(sLabeledRecIds, sLabeledRecIds + 34); - -// Based on the legacy struct -struct Arguments -{ - bool raw_given; - bool quiet_given; - bool loadcells_given; - bool plain_given; - - std::string mode; - std::string encoding; - std::string filename; - std::string outname; - - std::vector types; - std::string name; - - ESMData data; - ESM::ESMReader reader; - ESM::ESMWriter writer; }; bool parseOptions (int argc, char** argv, Arguments &info) @@ -76,7 +51,10 @@ bool parseOptions (int argc, char** argv, Arguments &info) desc.add_options() ("help,h", "print help message.") ("version,v", "print version information and quit.") - ("raw,r", "Show an unformatted list of all records and subrecords.") + ("raw,r", bpo::value(), + "Show an unformatted list of all records and subrecords of given format:\n" + "\n\tTES3" + "\n\tTES4") // The intention is that this option would interact better // with other modes including clone, dump, and raw. ("type,t", bpo::value< std::vector >(), @@ -138,12 +116,12 @@ bool parseOptions (int argc, char** argv, Arguments &info) } if (variables.count ("version")) { - std::cout << "ESMTool version " << ESMTOOL_VERSION << std::endl; + std::cout << "ESMTool version " << majorVersion << '.' << minorVersion << std::endl; return false; } if (!variables.count("mode")) { - std::cout << "No mode specified!" << std::endl << std::endl + std::cout << "No mode specified!\n\n" << desc << finalText << std::endl; return false; } @@ -156,7 +134,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) info.mode = variables["mode"].as(); if (!(info.mode == "dump" || info.mode == "clone" || info.mode == "comp")) { - std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"" << std::endl << std::endl + std::cout << "\nERROR: invalid mode \"" << info.mode << "\"\n\n" << desc << finalText << std::endl; return false; } @@ -180,7 +158,9 @@ bool parseOptions (int argc, char** argv, Arguments &info) if (variables["input-file"].as< std::vector >().size() > 1) info.outname = variables["input-file"].as< std::vector >()[1]; - info.raw_given = variables.count ("raw") != 0; + if (const auto it = variables.find("raw"); it != variables.end()) + info.mRawFormat = ESM::parseFormat(it->second.as()); + info.quiet_given = variables.count ("quiet") != 0; info.loadcells_given = variables.count ("loadcells") != 0; info.plain_given = variables.count("plain") != 0; @@ -189,7 +169,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) info.encoding = variables["encoding"].as(); if(info.encoding != "win1250" && info.encoding != "win1251" && info.encoding != "win1252") { - std::cout << info.encoding << " is not a valid encoding option." << std::endl; + std::cout << info.encoding << " is not a valid encoding option.\n"; info.encoding = "win1252"; } std::cout << ToUTF8::encodingUsingMessage(info.encoding) << std::endl; @@ -197,12 +177,13 @@ bool parseOptions (int argc, char** argv, Arguments &info) return true; } -void printRaw(ESM::ESMReader &esm); -void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info); +void loadCell(const Arguments& info, ESM::Cell &cell, ESM::ESMReader &esm, ESMData* data); + +int load(const Arguments& info, ESMData* data); +int clone(const Arguments& info); +int comp(const Arguments& info); -int load(Arguments& info); -int clone(Arguments& info); -int comp(Arguments& info); +} int main(int argc, char**argv) { @@ -213,7 +194,7 @@ int main(int argc, char**argv) return 1; if (info.mode == "dump") - return load(info); + return load(info, nullptr); else if (info.mode == "clone") return clone(info); else if (info.mode == "comp") @@ -233,7 +214,10 @@ int main(int argc, char**argv) return 0; } -void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) +namespace +{ + +void loadCell(const Arguments& info, ESM::Cell &cell, ESM::ESMReader &esm, ESMData* data) { bool quiet = (info.quiet_given || info.mode == "clone"); bool save = (info.mode == "clone"); @@ -249,55 +233,66 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) if(!quiet) std::cout << " References:\n"; bool deleted = false; - while(cell.getNextRef(esm, ref, deleted)) + ESM::MovedCellRef movedCellRef; + bool moved = false; + while(cell.getNextRef(esm, ref, deleted, movedCellRef, moved)) { - if (save) { - info.data.mCellRefs[&cell].push_back(std::make_pair(ref, deleted)); - } + if (data != nullptr && save) + data->mCellRefs[&cell].push_back(std::make_pair(ref, deleted)); if(quiet) continue; - std::cout << " Refnum: " << ref.mRefNum.mIndex << std::endl; - std::cout << " ID: " << ref.mRefID << std::endl; - std::cout << " Position: (" << ref.mPos.pos[0] << ", " << ref.mPos.pos[1] << ", " << ref.mPos.pos[2] << ")" << std::endl; + std::cout << " - Refnum: " << ref.mRefNum.mIndex << '\n'; + std::cout << " ID: " << ref.mRefID << '\n'; + std::cout << " Position: (" << ref.mPos.pos[0] << ", " << ref.mPos.pos[1] << ", " << ref.mPos.pos[2] << ")\n"; if (ref.mScale != 1.f) - std::cout << " Scale: " << ref.mScale << std::endl; + std::cout << " Scale: " << ref.mScale << '\n'; if (!ref.mOwner.empty()) - std::cout << " Owner: " << ref.mOwner << std::endl; + std::cout << " Owner: " << ref.mOwner << '\n'; if (!ref.mGlobalVariable.empty()) - std::cout << " Global: " << ref.mGlobalVariable << std::endl; + std::cout << " Global: " << ref.mGlobalVariable << '\n'; if (!ref.mFaction.empty()) - std::cout << " Faction: " << ref.mFaction << std::endl; + std::cout << " Faction: " << ref.mFaction << '\n'; if (!ref.mFaction.empty() || ref.mFactionRank != -2) - std::cout << " Faction rank: " << ref.mFactionRank << std::endl; - std::cout << " Enchantment charge: " << ref.mEnchantmentCharge << std::endl; - std::cout << " Uses/health: " << ref.mChargeInt << std::endl; - std::cout << " Gold value: " << ref.mGoldValue << std::endl; - std::cout << " Blocked: " << static_cast(ref.mReferenceBlocked) << std::endl; - std::cout << " Deleted: " << deleted << std::endl; + 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 << " Blocked: " << static_cast(ref.mReferenceBlocked) << '\n'; + std::cout << " Deleted: " << deleted << '\n'; if (!ref.mKey.empty()) - std::cout << " Key: " << ref.mKey << std::endl; - std::cout << " Lock level: " << ref.mLockLevel << std::endl; + std::cout << " Key: " << ref.mKey << '\n'; + std::cout << " Lock level: " << ref.mLockLevel << '\n'; if (!ref.mTrap.empty()) - std::cout << " Trap: " << ref.mTrap << std::endl; + std::cout << " Trap: " << ref.mTrap << '\n'; if (!ref.mSoul.empty()) - std::cout << " Soul: " << ref.mSoul << std::endl; + std::cout << " Soul: " << ref.mSoul << '\n'; if (ref.mTeleport) { std::cout << " Destination position: (" << ref.mDoorDest.pos[0] << ", " - << ref.mDoorDest.pos[1] << ", " << ref.mDoorDest.pos[2] << ")" << std::endl; + << ref.mDoorDest.pos[1] << ", " << ref.mDoorDest.pos[2] << ")\n"; if (!ref.mDestCell.empty()) - std::cout << " Destination cell: " << ref.mDestCell << std::endl; + std::cout << " Destination cell: " << ref.mDestCell << '\n'; + } + std::cout << " Moved: " << std::boolalpha << moved << std::noboolalpha << '\n'; + if (moved) + { + std::cout << " Moved refnum: " << movedCellRef.mRefNum.mIndex << '\n'; + std::cout << " Moved content file: " << movedCellRef.mRefNum.mContentFile << '\n'; + std::cout << " Target: " << movedCellRef.mTarget[0] << ", " << movedCellRef.mTarget[1] << '\n'; } } } -void printRaw(ESM::ESMReader &esm) +void printRawTes3(std::string_view path) { + std::cout << "TES3 RAW file listing: " << path << '\n'; + ESM::ESMReader esm; + esm.openRaw(path); while(esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); - std::cout << "Record: " << n.toString() << std::endl; + std::cout << "Record: " << n.toStringView() << '\n'; esm.getRecHeader(); while(esm.hasMoreSubs()) { @@ -306,75 +301,62 @@ void printRaw(ESM::ESMReader &esm) esm.skipHSub(); n = esm.retSubName(); std::ios::fmtflags f(std::cout.flags()); - std::cout << " " << n.toString() << " - " << esm.getSubSize() - << " bytes @ 0x" << std::hex << offs << "\n"; + std::cout << " " << n.toStringView() << " - " << esm.getSubSize() + << " bytes @ 0x" << std::hex << offs << '\n'; std::cout.flags(f); } } } -int load(Arguments& info) +int loadTes3(const Arguments& info, std::unique_ptr&& stream, ESMData* data) { - ESM::ESMReader& esm = info.reader; + std::cout << "Loading TES3 file: " << info.filename << '\n'; + + ESM::ESMReader esm; ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding)); esm.setEncoder(&encoder); - std::string filename = info.filename; - std::cout << "Loading file: " << filename << std::endl; - - std::list skipped; - - try { - - if(info.raw_given && info.mode == "dump") - { - std::cout << "RAW file listing:\n"; - - esm.openRaw(filename); - - printRaw(esm); - - return 0; - } + std::unordered_set skipped; + try + { bool quiet = (info.quiet_given || info.mode == "clone"); bool loadCells = (info.loadcells_given || info.mode == "clone"); bool save = (info.mode == "clone"); - esm.open(filename); + esm.open(std::move(stream), info.filename); - info.data.author = esm.getAuthor(); - info.data.description = esm.getDesc(); - info.data.masters = esm.getGameFiles(); + if (data != nullptr) + data->mHeader = esm.getHeader(); if (!quiet) { - std::cout << "Author: " << esm.getAuthor() << std::endl - << "Description: " << esm.getDesc() << std::endl - << "File format version: " << esm.getFVer() << std::endl; + std::cout << "Author: " << esm.getAuthor() << '\n' + << "Description: " << esm.getDesc() << '\n' + << "File format version: " << esm.getFVer() << '\n'; std::vector masterData = esm.getGameFiles(); if (!masterData.empty()) { - std::cout << "Masters:" << std::endl; + std::cout << "Masters:" << '\n'; for(const auto& master : masterData) - std::cout << " " << master.name << ", " << master.size << " bytes" << std::endl; + std::cout << " " << master.name << ", " << master.size << " bytes\n"; } } // Loop through all records while(esm.hasMoreRecs()) { - ESM::NAME n = esm.getRecName(); + const ESM::NAME n = esm.getRecName(); uint32_t flags; esm.getRecHeader(flags); - EsmTool::RecordBase *record = EsmTool::RecordBase::create(n); + auto record = EsmTool::RecordBase::create(n); if (record == nullptr) { - if (std::find(skipped.begin(), skipped.end(), n.intval) == skipped.end()) + if (!quiet && skipped.count(n.toInt()) == 0) { - std::cout << "Skipping " << n.toString() << " records." << std::endl; - skipped.push_back(n.intval); + std::cout << "Skipping " << n.toStringView() << " records.\n"; + skipped.emplace(n.toInt()); } esm.skipRecord(); @@ -390,54 +372,88 @@ int load(Arguments& info) // Is the user interested in this record type? bool interested = true; - if (!info.types.empty()) - { - std::vector::iterator match; - match = std::find(info.types.begin(), info.types.end(), n.toString()); - if (match == info.types.end()) interested = false; - } + if (!info.types.empty() && std::find(info.types.begin(), info.types.end(), n.toStringView()) == info.types.end()) + interested = false; if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, record->getId())) interested = false; if(!quiet && interested) { - std::cout << "\nRecord: " << n.toString() << " '" << record->getId() << "'\n"; + std::cout << "\nRecord: " << n.toStringView() << " '" << record->getId() << "'\n" + << "Record flags: " << recordFlags(record->getFlags()) << '\n'; record->print(); } - if (record->getType().intval == ESM::REC_CELL && loadCells && interested) + if (record->getType().toInt() == ESM::REC_CELL && loadCells && interested) { - loadCell(record->cast()->get(), esm, info); + loadCell(info, record->cast()->get(), esm, data); } - if (save) - { - info.data.mRecords.push_back(record); - } - else + if (data != nullptr) { - delete record; + if (save) + data->mRecords.push_back(std::move(record)); + ++data->mRecordStats[n.toInt()]; } - ++info.data.mRecordStats[n.intval]; } - - } catch(std::exception &e) { + } + catch (const std::exception &e) + { std::cout << "\nERROR:\n\n " << e.what() << std::endl; - - for (const EsmTool::RecordBase* record : info.data.mRecords) - delete record; - - info.data.mRecords.clear(); + if (data != nullptr) + data->mRecords.clear(); return 1; } return 0; } -#include +int load(const Arguments& info, ESMData* data) +{ + if (info.mRawFormat.has_value() && info.mode == "dump") + { + switch (*info.mRawFormat) + { + case ESM::Format::Tes3: + printRawTes3(info.filename); + break; + case ESM::Format::Tes4: + std::cout << "Printing raw TES4 file is not supported: " << info.filename << "\n"; + break; + } + return 0; + } -int clone(Arguments& info) + auto stream = Files::openBinaryInputFileStream(info.filename); + if (!stream->is_open()) + { + std::cout << "Failed to open file: " << std::strerror(errno) << '\n'; + return -1; + } + + const ESM::Format format = ESM::readFormat(*stream); + stream->seekg(0); + + switch (format) + { + case ESM::Format::Tes3: + return loadTes3(info, std::move(stream), data); + case ESM::Format::Tes4: + if (data != nullptr) + { + std::cout << "Collecting data from esm file is not supported for TES4\n"; + return -1; + } + return loadTes4(info, std::move(stream)); + } + + std::cout << "Unsupported ESM format: " << ESM::NAME(format).toStringView() << '\n'; + + return -1; +} + +int clone(const Arguments& info) { if (info.outname.empty()) { @@ -445,71 +461,68 @@ int clone(Arguments& info) return 1; } - if (load(info) != 0) + ESMData data; + if (load(info, &data) != 0) { std::cout << "Failed to load, aborting." << std::endl; return 1; } - size_t recordCount = info.data.mRecords.size(); + size_t recordCount = data.mRecords.size(); int digitCount = 1; // For a nicer output if (recordCount > 0) digitCount = (int)std::log10(recordCount) + 1; - std::cout << "Loaded " << recordCount << " records:" << std::endl << std::endl; + std::cout << "Loaded " << recordCount << " records:\n\n"; int i = 0; - for (std::pair stat : info.data.mRecordStats) + for (std::pair stat : data.mRecordStats) { ESM::NAME name; - name.intval = stat.first; + name = stat.first; int amount = stat.second; - std::cout << std::setw(digitCount) << amount << " " << name.toString() << " "; + std::cout << std::setw(digitCount) << amount << " " << name.toStringView() << " "; if (++i % 3 == 0) - std::cout << std::endl; + std::cout << '\n'; } if (i % 3 != 0) - std::cout << std::endl; + std::cout << '\n'; - std::cout << std::endl << "Saving records to: " << info.outname << "..." << std::endl; + std::cout << "\nSaving records to: " << info.outname << "...\n"; - ESM::ESMWriter& esm = info.writer; + ESM::ESMWriter esm; ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding)); esm.setEncoder(&encoder); - esm.setAuthor(info.data.author); - esm.setDescription(info.data.description); - esm.setVersion(info.data.version); + esm.setHeader(data.mHeader); + esm.setVersion(ESM::VER_13); esm.setRecordCount (recordCount); - for (const ESM::Header::MasterData &master : info.data.masters) - esm.addMaster(master.name, master.size); - std::fstream save(info.outname.c_str(), std::fstream::out | std::fstream::binary); esm.save(save); int saved = 0; - for (EsmTool::RecordBase* record : info.data.mRecords) + for (auto& record : data.mRecords) { if (i <= 0) break; - const ESM::NAME& typeName = record->getType(); + const ESM::NAME typeName = record->getType(); - esm.startRecord(typeName.toString(), record->getFlags()); + esm.startRecord(typeName, record->getFlags()); record->save(esm); - if (typeName.intval == ESM::REC_CELL) { + if (typeName.toInt() == ESM::REC_CELL) { ESM::Cell *ptr = &record->cast()->get(); - if (!info.data.mCellRefs[ptr].empty()) + if (!data.mCellRefs[ptr].empty()) { - for (std::pair &ref : info.data.mCellRefs[ptr]) + for (std::pair &ref : data.mCellRefs[ptr]) ref.first.save(esm, ref.second); } } - esm.endRecord(typeName.toString()); + esm.endRecord(typeName); saved++; int perc = recordCount == 0 ? 100 : (int)((saved / (float)recordCount)*100); @@ -527,7 +540,7 @@ int clone(Arguments& info) return 0; } -int comp(Arguments& info) +int comp(const Arguments& info) { if (info.filename.empty() || info.outname.empty()) { @@ -538,9 +551,6 @@ int comp(Arguments& info) Arguments fileOne; Arguments fileTwo; - fileOne.raw_given = false; - fileTwo.raw_given = false; - fileOne.mode = "clone"; fileTwo.mode = "clone"; @@ -550,19 +560,21 @@ int comp(Arguments& info) fileOne.filename = info.filename; fileTwo.filename = info.outname; - if (load(fileOne) != 0) + ESMData dataOne; + if (load(fileOne, &dataOne) != 0) { std::cout << "Failed to load " << info.filename << ", aborting comparison." << std::endl; return 1; } - if (load(fileTwo) != 0) + ESMData dataTwo; + if (load(fileTwo, &dataTwo) != 0) { std::cout << "Failed to load " << info.outname << ", aborting comparison." << std::endl; return 1; } - if (fileOne.data.mRecords.size() != fileTwo.data.mRecords.size()) + if (dataOne.mRecords.size() != dataTwo.mRecords.size()) { std::cout << "Not equal, different amount of records." << std::endl; return 1; @@ -570,3 +582,5 @@ int comp(Arguments& info) return 0; } + +} diff --git a/apps/esmtool/labels.cpp b/apps/esmtool/labels.cpp index 24e3605eb2..405aeb9454 100644 --- a/apps/esmtool/labels.cpp +++ b/apps/esmtool/labels.cpp @@ -1,17 +1,17 @@ #include "labels.hpp" -#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 @@ -902,3 +902,17 @@ std::string weaponFlags(int flags) properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } + +std::string recordFlags(uint32_t flags) +{ + std::string properties; + if (flags == 0) properties += "[None] "; + if (flags & ESM::FLAG_Deleted) properties += "Deleted "; + if (flags & ESM::FLAG_Persistent) properties += "Persistent "; + if (flags & ESM::FLAG_Ignored) properties += "Ignored "; + if (flags & ESM::FLAG_Blocked) properties += "Blocked "; + int unused = ~(ESM::FLAG_Deleted | ESM::FLAG_Persistent | ESM::FLAG_Ignored | ESM::FLAG_Blocked); + if (flags & unused) properties += "Invalid "; + properties += Misc::StringUtils::format("(0x%08X)", flags); + return properties; +} \ No newline at end of file diff --git a/apps/esmtool/labels.hpp b/apps/esmtool/labels.hpp index b06480a97b..8450ccfa03 100644 --- a/apps/esmtool/labels.hpp +++ b/apps/esmtool/labels.hpp @@ -60,6 +60,8 @@ std::string raceFlags(int flags); std::string spellFlags(int flags); std::string weaponFlags(int flags); +std::string recordFlags(uint32_t flags); + // Missing flags functions: // aiServicesFlags, possibly more diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 3679184a6f..fbda7eee6b 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -30,7 +30,7 @@ void printAIPackage(const ESM::AIPackage& p) { std::cout << " Travel Coordinates: (" << p.mTravel.mX << "," << p.mTravel.mY << "," << p.mTravel.mZ << ")" << std::endl; - std::cout << " Travel Unknown: " << p.mTravel.mUnk << std::endl; + std::cout << " Should repeat: " << p.mTravel.mShouldRepeat << std::endl; } else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort) { @@ -38,12 +38,12 @@ void printAIPackage(const ESM::AIPackage& p) << p.mTarget.mY << "," << p.mTarget.mZ << ")" << std::endl; std::cout << " Duration: " << p.mTarget.mDuration << std::endl; std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl; - std::cout << " Unknown: " << p.mTarget.mUnk << std::endl; + std::cout << " Should repeat: " << p.mTarget.mShouldRepeat << std::endl; } else if (p.mType == ESM::AI_Activate) { std::cout << " Name: " << p.mActivate.mName.toString() << std::endl; - std::cout << " Activate Unknown: " << p.mActivate.mUnk << std::endl; + std::cout << " Should repeat: " << p.mActivate.mShouldRepeat << std::endl; } else { std::cout << " BadPackage: " << Misc::StringUtils::format("0x%08X", p.mType) << std::endl; @@ -171,226 +171,227 @@ void printTransport(const std::vector& transport) namespace EsmTool { -RecordBase * -RecordBase::create(ESM::NAME type) +std::unique_ptr RecordBase::create(const ESM::NAME type) { - RecordBase *record = nullptr; + std::unique_ptr record; - switch (type.intval) { + switch (type.toInt()) + { case ESM::REC_ACTI: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_ALCH: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_APPA: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_ARMO: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_BODY: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_BOOK: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_BSGN: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_CELL: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_CLAS: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_CLOT: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_CONT: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_CREA: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_DIAL: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_DOOR: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_ENCH: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_FACT: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_GLOB: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_GMST: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_INFO: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_INGR: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LAND: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LEVI: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LEVC: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LIGH: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LOCK: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_LTEX: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_MISC: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_MGEF: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_NPC_: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_PGRD: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_PROB: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_RACE: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_REGN: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_REPA: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SCPT: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SKIL: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SNDG: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SOUN: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SPEL: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_STAT: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_WEAP: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } case ESM::REC_SSCR: { - record = new EsmTool::Record; + record = std::make_unique>(); break; } default: - record = nullptr; + break; } - if (record) { + if (record) + { record->mType = type; } return record; @@ -1183,8 +1184,8 @@ void Record::print() 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 << " UnknownA: " << (int)mData.mData.mA << std::endl; - std::cout << " UnknownB: " << (int)mData.mData.mB << std::endl; + std::cout << " Snow: " << (int)mData.mData.mSnow << std::endl; + std::cout << " Blizzard: " << (int)mData.mData.mBlizzard << std::endl; std::cout << " Map Color: " << mData.mMapColor << std::endl; if (!mData.mSleepList.empty()) std::cout << " Sleep List: " << mData.mSleepList << std::endl; diff --git a/apps/esmtool/record.hpp b/apps/esmtool/record.hpp index bbb3dd0988..ef90dd1310 100644 --- a/apps/esmtool/record.hpp +++ b/apps/esmtool/record.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESMTOOL_RECORD_H #include +#include #include @@ -54,7 +55,7 @@ namespace EsmTool virtual void save(ESM::ESMWriter &esm) = 0; virtual void print() = 0; - static RecordBase *create(ESM::NAME type); + static std::unique_ptr create(ESM::NAME type); // just make it a bit shorter template diff --git a/apps/esmtool/tes4.cpp b/apps/esmtool/tes4.cpp new file mode 100644 index 0000000000..3f213ff0b7 --- /dev/null +++ b/apps/esmtool/tes4.cpp @@ -0,0 +1,329 @@ +#include "tes4.hpp" +#include "arguments.hpp" +#include "labels.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace EsmTool +{ + namespace + { + struct Params + { + const bool mQuite; + + explicit Params(const Arguments& info) + : mQuite(info.quiet_given || info.mode == "clone") + {} + }; + + std::string toString(ESM4::GroupType type) + { + switch (type) + { + case ESM4::Grp_RecordType: return "RecordType"; + case ESM4::Grp_WorldChild: return "WorldChild"; + case ESM4::Grp_InteriorCell: return "InteriorCell"; + case ESM4::Grp_InteriorSubCell: return "InteriorSubCell"; + case ESM4::Grp_ExteriorCell: return "ExteriorCell"; + case ESM4::Grp_ExteriorSubCell: return "ExteriorSubCell"; + case ESM4::Grp_CellChild: return "CellChild"; + case ESM4::Grp_TopicChild: return "TopicChild"; + case ESM4::Grp_CellPersistentChild: return "CellPersistentChild"; + case ESM4::Grp_CellTemporaryChild: return "CellTemporaryChild"; + case ESM4::Grp_CellVisibleDistChild: return "CellVisibleDistChild"; + } + + return "Unknown (" + std::to_string(type) + ")"; + } + + template > + struct HasFormId : std::false_type {}; + + template + struct HasFormId> : std::true_type {}; + + template + constexpr bool hasFormId = HasFormId::value; + + template > + struct HasFlags : std::false_type {}; + + template + struct HasFlags> : std::true_type {}; + + template + constexpr bool hasFlags = HasFlags::value; + + template + void readTypedRecord(const Params& params, ESM4::Reader& reader) + { + reader.getRecordData(); + + T value; + value.load(reader); + + if (params.mQuite) + return; + + std::cout << "\n Record: " << ESM::NAME(reader.hdr().record.typeId).toStringView(); + if constexpr (hasFormId) + std::cout << ' ' << value.mFormId; + if constexpr (hasFlags) + std::cout << "\n Record flags: " << recordFlags(value.mFlags); + std::cout << '\n'; + } + + void readRecord(const Params& params, ESM4::Reader& reader) + { + switch (static_cast(reader.hdr().record.typeId)) + { + case ESM4::REC_AACT: break; + case ESM4::REC_ACHR: return readTypedRecord(params, reader); + case ESM4::REC_ACRE: return readTypedRecord(params, reader); + case ESM4::REC_ACTI: return readTypedRecord(params, reader); + case ESM4::REC_ADDN: break; + case ESM4::REC_ALCH: return readTypedRecord(params, reader); + case ESM4::REC_ALOC: return readTypedRecord(params, reader); + case ESM4::REC_AMMO: return readTypedRecord(params, reader); + case ESM4::REC_ANIO: return readTypedRecord(params, reader); + case ESM4::REC_APPA: return readTypedRecord(params, reader); + case ESM4::REC_ARMA: return readTypedRecord(params, reader); + case ESM4::REC_ARMO: return readTypedRecord(params, reader); + case ESM4::REC_ARTO: break; + case ESM4::REC_ASPC: return readTypedRecord(params, reader); + case ESM4::REC_ASTP: break; + case ESM4::REC_AVIF: break; + case ESM4::REC_BOOK: return readTypedRecord(params, reader); + case ESM4::REC_BPTD: return readTypedRecord(params, reader); + case ESM4::REC_CAMS: break; + case ESM4::REC_CCRD: break; + case ESM4::REC_CELL: return readTypedRecord(params, reader); + case ESM4::REC_CLAS: return readTypedRecord(params, reader); + case ESM4::REC_CLFM: return readTypedRecord(params, reader); + case ESM4::REC_CLMT: break; + case ESM4::REC_CLOT: return readTypedRecord(params, reader); + case ESM4::REC_CMNY: break; + case ESM4::REC_COBJ: break; + case ESM4::REC_COLL: break; + case ESM4::REC_CONT: return readTypedRecord(params, reader); + case ESM4::REC_CPTH: break; + case ESM4::REC_CREA: return readTypedRecord(params, reader); + case ESM4::REC_CSTY: break; + case ESM4::REC_DEBR: break; + case ESM4::REC_DIAL: return readTypedRecord(params, reader); + case ESM4::REC_DLBR: break; + case ESM4::REC_DLVW: break; + case ESM4::REC_DOBJ: return readTypedRecord(params, reader); + case ESM4::REC_DOOR: return readTypedRecord(params, reader); + case ESM4::REC_DUAL: break; + case ESM4::REC_ECZN: break; + case ESM4::REC_EFSH: break; + case ESM4::REC_ENCH: break; + case ESM4::REC_EQUP: break; + case ESM4::REC_EXPL: break; + case ESM4::REC_EYES: return readTypedRecord(params, reader); + case ESM4::REC_FACT: break; + case ESM4::REC_FLOR: return readTypedRecord(params, reader); + case ESM4::REC_FLST: return readTypedRecord(params, reader); + case ESM4::REC_FSTP: break; + case ESM4::REC_FSTS: break; + case ESM4::REC_FURN: return readTypedRecord(params, reader); + case ESM4::REC_GLOB: return readTypedRecord(params, reader); + case ESM4::REC_GMST: break; + case ESM4::REC_GRAS: return readTypedRecord(params, reader); + case ESM4::REC_GRUP: break; + case ESM4::REC_HAIR: return readTypedRecord(params, reader); + case ESM4::REC_HAZD: break; + case ESM4::REC_HDPT: return readTypedRecord(params, reader); + case ESM4::REC_IDLE: + // FIXME: ESM4::IdleAnimation::load does not work with Oblivion.esm + // return readTypedRecord(params, reader); + break; + case ESM4::REC_IDLM: return readTypedRecord(params, reader); + case ESM4::REC_IMAD: break; + case ESM4::REC_IMGS: break; + case ESM4::REC_IMOD: return readTypedRecord(params, reader); + case ESM4::REC_INFO: return readTypedRecord(params, reader); + case ESM4::REC_INGR: return readTypedRecord(params, reader); + case ESM4::REC_IPCT: break; + case ESM4::REC_IPDS: break; + case ESM4::REC_KEYM: return readTypedRecord(params, reader); + case ESM4::REC_KYWD: break; + case ESM4::REC_LAND: return readTypedRecord(params, reader); + case ESM4::REC_LCRT: break; + case ESM4::REC_LCTN: break; + case ESM4::REC_LGTM: return readTypedRecord(params, reader); + case ESM4::REC_LIGH: return readTypedRecord(params, reader); + case ESM4::REC_LSCR: break; + case ESM4::REC_LTEX: return readTypedRecord(params, reader); + case ESM4::REC_LVLC: return readTypedRecord(params, reader); + case ESM4::REC_LVLI: return readTypedRecord(params, reader); + case ESM4::REC_LVLN: return readTypedRecord(params, reader); + case ESM4::REC_LVSP: break; + case ESM4::REC_MATO: return readTypedRecord(params, reader); + case ESM4::REC_MATT: break; + case ESM4::REC_MESG: break; + case ESM4::REC_MGEF: break; + case ESM4::REC_MISC: return readTypedRecord(params, reader); + case ESM4::REC_MOVT: break; + case ESM4::REC_MSET: return readTypedRecord(params, reader); + case ESM4::REC_MSTT: return readTypedRecord(params, reader); + case ESM4::REC_MUSC: return readTypedRecord(params, reader); + case ESM4::REC_MUST: break; + case ESM4::REC_NAVI: return readTypedRecord(params, reader); + case ESM4::REC_NAVM: return readTypedRecord(params, reader); + case ESM4::REC_NOTE: return readTypedRecord(params, reader); + case ESM4::REC_NPC_: return readTypedRecord(params, reader); + case ESM4::REC_OTFT: return readTypedRecord(params, reader); + case ESM4::REC_PACK: return readTypedRecord(params, reader); + case ESM4::REC_PERK: break; + case ESM4::REC_PGRD: return readTypedRecord(params, reader); + case ESM4::REC_PGRE: return readTypedRecord(params, reader); + case ESM4::REC_PHZD: break; + case ESM4::REC_PROJ: break; + case ESM4::REC_PWAT: return readTypedRecord(params, reader); + case ESM4::REC_QUST: return readTypedRecord(params, reader); + case ESM4::REC_RACE: return readTypedRecord(params, reader); + case ESM4::REC_REFR: return readTypedRecord(params, reader); + case ESM4::REC_REGN: return readTypedRecord(params, reader); + case ESM4::REC_RELA: break; + case ESM4::REC_REVB: break; + case ESM4::REC_RFCT: break; + case ESM4::REC_ROAD: return readTypedRecord(params, reader); + case ESM4::REC_SBSP: return readTypedRecord(params, reader); + case ESM4::REC_SCEN: break; + case ESM4::REC_SCOL: return readTypedRecord(params, reader); + case ESM4::REC_SCPT: return readTypedRecord(params, reader); + case ESM4::REC_SCRL: return readTypedRecord(params, reader); + case ESM4::REC_SGST: return readTypedRecord(params, reader); + case ESM4::REC_SHOU: break; + case ESM4::REC_SLGM: return readTypedRecord(params, reader); + case ESM4::REC_SMBN: break; + case ESM4::REC_SMEN: break; + case ESM4::REC_SMQN: break; + case ESM4::REC_SNCT: break; + case ESM4::REC_SNDR: return readTypedRecord(params, reader); + case ESM4::REC_SOPM: break; + case ESM4::REC_SOUN: return readTypedRecord(params, reader); + case ESM4::REC_SPEL: break; + case ESM4::REC_SPGD: break; + case ESM4::REC_STAT: return readTypedRecord(params, reader); + case ESM4::REC_TACT: return readTypedRecord(params, reader); + case ESM4::REC_TERM: return readTypedRecord(params, reader); + case ESM4::REC_TES4: return readTypedRecord(params, reader); + case ESM4::REC_TREE: return readTypedRecord(params, reader); + case ESM4::REC_TXST: return readTypedRecord(params, reader); + case ESM4::REC_VTYP: break; + case ESM4::REC_WATR: break; + case ESM4::REC_WEAP: return readTypedRecord(params, reader); + case ESM4::REC_WOOP: break; + case ESM4::REC_WRLD: return readTypedRecord(params, reader); + case ESM4::REC_WTHR: break; + } + + if (!params.mQuite) + std::cout << "\n Unsupported record: " << ESM::NAME(reader.hdr().record.typeId).toStringView() << '\n'; + + reader.skipRecordData(); + } + + bool readItem(const Params& params, ESM4::Reader& reader); + + bool readGroup(const Params& params, ESM4::Reader& reader) + { + const ESM4::RecordHeader& header = reader.hdr(); + + if (!params.mQuite) + std::cout << "\nGroup: " << toString(static_cast(header.group.type)) + << " " << ESM::NAME(header.group.typeId).toStringView() << '\n'; + + switch (static_cast(header.group.type)) + { + case ESM4::Grp_RecordType: + case ESM4::Grp_InteriorCell: + case ESM4::Grp_InteriorSubCell: + case ESM4::Grp_ExteriorCell: + case ESM4::Grp_ExteriorSubCell: + reader.enterGroup(); + return readItem(params, reader); + case ESM4::Grp_WorldChild: + case ESM4::Grp_CellChild: + case ESM4::Grp_TopicChild: + case ESM4::Grp_CellPersistentChild: + case ESM4::Grp_CellTemporaryChild: + case ESM4::Grp_CellVisibleDistChild: + reader.adjustGRUPFormId(); + reader.enterGroup(); + if (!reader.hasMoreRecs()) + return false; + return readItem(params, reader); + } + + reader.skipGroup(); + + return true; + } + + bool readItem(const Params& params, ESM4::Reader& reader) + { + if (!reader.getRecordHeader() || !reader.hasMoreRecs()) + return false; + + const ESM4::RecordHeader& header = reader.hdr(); + + if (header.record.typeId == ESM4::REC_GRUP) + return readGroup(params, reader); + + readRecord(params, reader); + return true; + } + } + + int loadTes4(const Arguments& info, std::unique_ptr&& stream) + { + std::cout << "Loading TES4 file: " << info.filename << '\n'; + + try + { + const ToUTF8::StatelessUtf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); + ESM4::Reader reader(std::move(stream), info.filename); + reader.setEncoder(&encoder); + const Params params(info); + + if (!params.mQuite) + { + std::cout << "Author: " << reader.getAuthor() << '\n' + << "Description: " << reader.getDesc() << '\n' + << "File format version: " << reader.esmVersion() << '\n'; + + if (const std::vector& masterData = reader.getGameFiles(); !masterData.empty()) + { + std::cout << "Masters:" << '\n'; + for (const auto& master : masterData) + std::cout << " " << master.name << ", " << master.size << " bytes\n"; + } + } + + while (reader.hasMoreRecs()) + { + reader.exitGroupCheck(); + if (!readItem(params, reader)) + break; + } + } + catch (const std::exception& e) + { + std::cout << "\nERROR:\n\n " << e.what() << std::endl; + return -1; + } + + return 0; + } +} diff --git a/apps/esmtool/tes4.hpp b/apps/esmtool/tes4.hpp new file mode 100644 index 0000000000..8149b26049 --- /dev/null +++ b/apps/esmtool/tes4.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESMTOOL_TES4_H +#define OPENMW_ESMTOOL_TES4_H + +#include +#include +#include + +namespace EsmTool +{ + struct Arguments; + + int loadTes4(const Arguments& info, std::unique_ptr&& stream); +} + +#endif diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt index 0e742ff548..add5ad6d05 100644 --- a/apps/essimporter/CMakeLists.txt +++ b/apps/essimporter/CMakeLists.txt @@ -36,7 +36,6 @@ openmw_add_executable(openmw-essimporter target_link_libraries(openmw-essimporter ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} components ) @@ -48,3 +47,13 @@ endif() if (WIN32) INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") endif(WIN32) + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-essimporter PRIVATE + + + + + + ) +endif() diff --git a/apps/essimporter/convertacdt.hpp b/apps/essimporter/convertacdt.hpp index 4059dd1af8..00e90ababf 100644 --- a/apps/essimporter/convertacdt.hpp +++ b/apps/essimporter/convertacdt.hpp @@ -1,10 +1,10 @@ #ifndef OPENMW_ESSIMPORT_CONVERTACDT_H #define OPENMW_ESSIMPORT_CONVERTACDT_H -#include -#include -#include -#include +#include +#include +#include +#include #include "importacdt.hpp" diff --git a/apps/essimporter/convertcntc.hpp b/apps/essimporter/convertcntc.hpp index c299d87a1e..2dc51949b1 100644 --- a/apps/essimporter/convertcntc.hpp +++ b/apps/essimporter/convertcntc.hpp @@ -3,7 +3,7 @@ #include "importcntc.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/convertcrec.hpp b/apps/essimporter/convertcrec.hpp index 7d317f03e8..fa2e7e807f 100644 --- a/apps/essimporter/convertcrec.hpp +++ b/apps/essimporter/convertcrec.hpp @@ -3,7 +3,7 @@ #include "importcrec.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index e0756602dd..3c08084ca9 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -5,8 +5,8 @@ #include -#include -#include +#include +#include #include @@ -68,7 +68,7 @@ namespace { if (isIndexedRefId(indexedRefId)) { - int refIndex; + int refIndex = 0; std::string refId; splitIndexedRefId(indexedRefId, refIndex, refId); @@ -278,7 +278,7 @@ namespace ESSImport while (esm.isNextSub("MPCD")) { float notepos[3]; - esm.getHT(notepos, 3*sizeof(float)); + esm.getHTSized<3 * sizeof(float)>(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, @@ -320,6 +320,8 @@ namespace ESSImport esm.startRecord(ESM::REC_CSTA); ESM::CellState csta; csta.mHasFogOfWar = 0; + csta.mLastRespawn.mDay = 0; + csta.mLastRespawn.mHour = 0; csta.mId = esmcell.getCellId(); csta.mId.save(esm); // TODO csta.mLastRespawn; @@ -352,12 +354,12 @@ namespace ESSImport } else { - int refIndex; + int refIndex = 0; splitIndexedRefId(cellref.mIndexedRefId, refIndex, out.mRefID); std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); - std::map, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( + auto npccIt = mContext->mNpcChanges.find( std::make_pair(refIndex, out.mRefID)); if (npccIt != mContext->mNpcChanges.end()) { @@ -369,6 +371,8 @@ namespace ESSImport // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mMissingACDT = true; if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertNpcData(cellref, objstate.mNpcStats); @@ -383,7 +387,7 @@ namespace ESSImport continue; } - std::map, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( + auto cntcIt = mContext->mContainerChanges.find( std::make_pair(refIndex, out.mRefID)); if (cntcIt != mContext->mContainerChanges.end()) { @@ -398,7 +402,7 @@ namespace ESSImport continue; } - std::map, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( + auto crecIt = mContext->mCreatureChanges.find( std::make_pair(refIndex, out.mRefID)); if (crecIt != mContext->mCreatureChanges.end()) { @@ -410,6 +414,8 @@ namespace ESSImport // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mMissingACDT = true; if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertCREC(crecIt->second, objstate); @@ -486,6 +492,7 @@ namespace ESSImport out.mSpellId = it->mSPDT.mId.toString(); out.mSpeed = pnam.mSpeed * 0.001f; // not sure where this factor comes from + out.mSlot = 0; esm.startRecord(ESM::REC_MPRJ); out.save(esm); diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 9a1923c2b6..4ae39b0e65 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -6,23 +6,23 @@ #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 "importcrec.hpp" #include "importcntc.hpp" @@ -124,11 +124,9 @@ public: { mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; mContext->mPlayerBase = npc; - ESM::SpellState::SpellParams empty; // FIXME: player start spells and birthsign spells aren't listed here, // need to fix openmw to account for this - for (const auto & spell : npc.mSpells.mList) - mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList; // Clear the list now that we've written it, this prevents issues cropping up with // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. @@ -374,7 +372,7 @@ public: void write(ESM::ESMWriter &esm) override { esm.startRecord(ESM::REC_DCOU); - for (std::map::const_iterator it = mKillCounter.begin(); it != mKillCounter.end(); ++it) + for (auto it = mKillCounter.begin(); it != mKillCounter.end(); ++it) { esm.writeHNString("ID__", it->first); esm.writeHNT ("COUN", it->second); @@ -397,7 +395,7 @@ public: faction.load(esm, isDeleted); std::string id = Misc::StringUtils::lowerCase(faction.mId); - for (std::map::const_iterator it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) + for (auto it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) { std::string faction2 = Misc::StringUtils::lowerCase(it->first); mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); @@ -431,7 +429,7 @@ public: void write(ESM::ESMWriter &esm) override { ESM::StolenItems items; - for (std::map >::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) + for (auto it = mStolenItems.begin(); it != mStolenItems.end(); ++it) { std::map, int> owners; for (const auto & ownerIt : it->second) @@ -487,7 +485,7 @@ public: } void write(ESM::ESMWriter &esm) override { - for (std::map::const_iterator it = mDials.begin(); it != mDials.end(); ++it) + for (auto it = mDials.begin(); it != mDials.end(); ++it) { esm.startRecord(ESM::REC_QUES); ESM::QuestState state; @@ -545,9 +543,7 @@ public: } else { - std::stringstream error; - error << "Invalid weather ID:" << weatherID << std::endl; - throw std::runtime_error(error.str()); + throw std::runtime_error("Invalid weather ID: " + std::to_string(weatherID)); } } diff --git a/apps/essimporter/convertinventory.hpp b/apps/essimporter/convertinventory.hpp index 8abe85a44a..95d134d4fc 100644 --- a/apps/essimporter/convertinventory.hpp +++ b/apps/essimporter/convertinventory.hpp @@ -3,7 +3,7 @@ #include "importinventory.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/convertnpcc.hpp b/apps/essimporter/convertnpcc.hpp index eb12d8f3bc..d0a395e33e 100644 --- a/apps/essimporter/convertnpcc.hpp +++ b/apps/essimporter/convertnpcc.hpp @@ -3,7 +3,7 @@ #include "importnpcc.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/convertplayer.hpp b/apps/essimporter/convertplayer.hpp index 1d2fdc87a6..73c98309f8 100644 --- a/apps/essimporter/convertplayer.hpp +++ b/apps/essimporter/convertplayer.hpp @@ -3,8 +3,8 @@ #include "importplayer.hpp" -#include -#include +#include +#include namespace ESSImport { diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp index ca81ebbbf2..cb7947e400 100644 --- a/apps/essimporter/convertscpt.cpp +++ b/apps/essimporter/convertscpt.cpp @@ -11,6 +11,7 @@ namespace ESSImport { out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); out.mRunning = scpt.mRunning; + out.mTargetRef.unset(); // TODO: convert target reference of global script convertSCRI(scpt.mSCRI, out.mLocals); } diff --git a/apps/essimporter/convertscpt.hpp b/apps/essimporter/convertscpt.hpp index 3390bd6070..f4a4e34fe4 100644 --- a/apps/essimporter/convertscpt.hpp +++ b/apps/essimporter/convertscpt.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_ESSIMPORT_CONVERTSCPT_H #define OPENMW_ESSIMPORT_CONVERTSCPT_H -#include +#include #include "importscpt.hpp" diff --git a/apps/essimporter/convertscri.hpp b/apps/essimporter/convertscri.hpp index 2d89456662..3908dbacf2 100644 --- a/apps/essimporter/convertscri.hpp +++ b/apps/essimporter/convertscri.hpp @@ -3,7 +3,7 @@ #include "importscri.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importacdt.cpp b/apps/essimporter/importacdt.cpp index 0ddd2eb64c..bab9b7b6b9 100644 --- a/apps/essimporter/importacdt.cpp +++ b/apps/essimporter/importacdt.cpp @@ -1,14 +1,16 @@ #include "importacdt.hpp" -#include +#include -#include +#include namespace ESSImport { void ActorData::load(ESM::ESMReader &esm) { + blank(); + if (esm.isNextSub("ACTN")) { /* diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp index 354eca32d8..6ecb72e334 100644 --- a/apps/essimporter/importacdt.hpp +++ b/apps/essimporter/importacdt.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "importscri.hpp" diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp index 442a7781c7..22af1af5fa 100644 --- a/apps/essimporter/importcellref.cpp +++ b/apps/essimporter/importcellref.cpp @@ -1,6 +1,6 @@ #include "importcellref.hpp" -#include +#include namespace ESSImport { @@ -35,8 +35,8 @@ 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.getHNOT(mPos, "DATA", 24); - esm.getHNOT(mPos, "DATA", 24); + esm.getHNOTSized<24>(mPos, "DATA"); + esm.getHNOTSized<24>(mPos, "DATA"); mDeleted = 0; if (esm.isNextSub("DELE")) diff --git a/apps/essimporter/importcellref.hpp b/apps/essimporter/importcellref.hpp index b115628d5e..d69a0a829d 100644 --- a/apps/essimporter/importcellref.hpp +++ b/apps/essimporter/importcellref.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "importacdt.hpp" @@ -27,7 +27,7 @@ namespace ESSImport void load(ESM::ESMReader& esm) override; - virtual ~CellRef() = default; + ~CellRef() override = default; }; } diff --git a/apps/essimporter/importcntc.cpp b/apps/essimporter/importcntc.cpp index a492aef5aa..a4b54ca7b4 100644 --- a/apps/essimporter/importcntc.cpp +++ b/apps/essimporter/importcntc.cpp @@ -1,6 +1,6 @@ #include "importcntc.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importcrec.cpp b/apps/essimporter/importcrec.cpp index 64879f2afc..6cef13333b 100644 --- a/apps/essimporter/importcrec.cpp +++ b/apps/essimporter/importcrec.cpp @@ -1,6 +1,6 @@ #include "importcrec.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp index 5110fbc689..77933eafe8 100644 --- a/apps/essimporter/importcrec.hpp +++ b/apps/essimporter/importcrec.hpp @@ -2,7 +2,7 @@ #define OPENMW_ESSIMPORT_CREC_H #include "importinventory.hpp" -#include +#include namespace ESM { diff --git a/apps/essimporter/importdial.cpp b/apps/essimporter/importdial.cpp index 5797a708a1..1467e43365 100644 --- a/apps/essimporter/importdial.cpp +++ b/apps/essimporter/importdial.cpp @@ -1,6 +1,6 @@ #include "importdial.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 706512263e..f0c74a7333 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -1,27 +1,26 @@ #include "importer.hpp" #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 @@ -264,48 +263,48 @@ namespace ESSImport const ESM::Header& header = esm.getHeader(); context.mPlayerCellName = header.mGameData.mCurrentCell.toString(); - const unsigned int recREFR = ESM::FourCC<'R','E','F','R'>::value; - const unsigned int recPCDT = ESM::FourCC<'P','C','D','T'>::value; - const unsigned int recFMAP = ESM::FourCC<'F','M','A','P'>::value; - const unsigned int recKLST = ESM::FourCC<'K','L','S','T'>::value; - const unsigned int recSTLN = ESM::FourCC<'S','T','L','N'>::value; - const unsigned int recGAME = ESM::FourCC<'G','A','M','E'>::value; - const unsigned int recJOUR = ESM::FourCC<'J','O','U','R'>::value; - const unsigned int recSPLM = ESM::FourCC<'S','P','L','M'>::value; - - std::map > converters; - converters[ESM::REC_GLOB] = std::shared_ptr(new ConvertGlobal()); - converters[ESM::REC_BOOK] = std::shared_ptr(new ConvertBook()); - converters[ESM::REC_NPC_] = std::shared_ptr(new ConvertNPC()); - converters[ESM::REC_CREA] = std::shared_ptr(new ConvertCREA()); - converters[ESM::REC_NPCC] = std::shared_ptr(new ConvertNPCC()); - converters[ESM::REC_CREC] = std::shared_ptr(new ConvertCREC()); - converters[recREFR ] = std::shared_ptr(new ConvertREFR()); - converters[recPCDT ] = std::shared_ptr(new ConvertPCDT()); - converters[recFMAP ] = std::shared_ptr(new ConvertFMAP()); - converters[recKLST ] = std::shared_ptr(new ConvertKLST()); - converters[recSTLN ] = std::shared_ptr(new ConvertSTLN()); - converters[recGAME ] = std::shared_ptr(new ConvertGAME()); - converters[ESM::REC_CELL] = std::shared_ptr(new ConvertCell()); - converters[ESM::REC_ALCH] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CLAS] = std::shared_ptr(new ConvertClass()); - converters[ESM::REC_SPEL] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_ARMO] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CLOT] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_ENCH] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_LEVC] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_LEVI] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CNTC] = std::shared_ptr(new ConvertCNTC()); - converters[ESM::REC_FACT] = std::shared_ptr(new ConvertFACT()); - converters[ESM::REC_INFO] = std::shared_ptr(new ConvertINFO()); - converters[ESM::REC_DIAL] = std::shared_ptr(new ConvertDIAL()); - converters[ESM::REC_QUES] = std::shared_ptr(new ConvertQUES()); - converters[recJOUR ] = std::shared_ptr(new ConvertJOUR()); - converters[ESM::REC_SCPT] = std::shared_ptr(new ConvertSCPT()); - converters[ESM::REC_PROJ] = std::shared_ptr(new ConvertPROJ()); - converters[recSPLM] = std::shared_ptr(new ConvertSPLM()); + const unsigned int recREFR = ESM::fourCC("REFR"); + const unsigned int recPCDT = ESM::fourCC("PCDT"); + const unsigned int recFMAP = ESM::fourCC("FMAP"); + const unsigned int recKLST = ESM::fourCC("KLST"); + const unsigned int recSTLN = ESM::fourCC("STLN"); + const unsigned int recGAME = ESM::fourCC("GAME"); + const unsigned int recJOUR = ESM::fourCC("JOUR"); + const unsigned int recSPLM = ESM::fourCC("SPLM"); + + std::map> converters; + converters[ESM::REC_GLOB] = std::make_unique(); + converters[ESM::REC_BOOK] = std::make_unique(); + converters[ESM::REC_NPC_] = std::make_unique(); + converters[ESM::REC_CREA] = std::make_unique(); + converters[ESM::REC_NPCC] = std::make_unique(); + converters[ESM::REC_CREC] = std::make_unique(); + converters[recREFR ] = std::make_unique(); + converters[recPCDT ] = std::make_unique(); + converters[recFMAP ] = std::make_unique(); + converters[recKLST ] = std::make_unique(); + converters[recSTLN ] = std::make_unique(); + converters[recGAME ] = std::make_unique(); + converters[ESM::REC_CELL] = std::make_unique(); + converters[ESM::REC_ALCH] = std::make_unique>(); + converters[ESM::REC_CLAS] = std::make_unique(); + converters[ESM::REC_SPEL] = std::make_unique>(); + converters[ESM::REC_ARMO] = std::make_unique>(); + converters[ESM::REC_WEAP] = std::make_unique>(); + converters[ESM::REC_CLOT] = std::make_unique>(); + converters[ESM::REC_ENCH] = std::make_unique>(); + converters[ESM::REC_WEAP] = std::make_unique>(); + converters[ESM::REC_LEVC] = std::make_unique>(); + converters[ESM::REC_LEVI] = std::make_unique>(); + converters[ESM::REC_CNTC] = std::make_unique(); + converters[ESM::REC_FACT] = std::make_unique(); + converters[ESM::REC_INFO] = std::make_unique(); + converters[ESM::REC_DIAL] = std::make_unique(); + converters[ESM::REC_QUES] = std::make_unique(); + converters[recJOUR ] = std::make_unique(); + converters[ESM::REC_SCPT] = std::make_unique(); + converters[ESM::REC_PROJ] = std::make_unique(); + converters[recSPLM] = std::make_unique(); // TODO: // - REGN (weather in certain regions?) @@ -324,14 +323,14 @@ namespace ESSImport ESM::NAME n = esm.getRecName(); esm.getRecHeader(); - auto it = converters.find(n.intval); + auto it = converters.find(n.toInt()); if (it != converters.end()) { it->second->read(esm); } else { - if (unknownRecords.insert(n.intval).second) + if (unknownRecords.insert(n.toInt()).second) { std::ios::fmtflags f(std::cerr.flags()); std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() << ")" << std::endl; @@ -346,7 +345,7 @@ namespace ESSImport writer.setFormat (ESM::SavedGame::sCurrentFormat); - boost::filesystem::ofstream stream(boost::filesystem::path(mOutFile), std::ios::out | std::ios::binary); + std::ofstream stream(std::filesystem::path(mOutFile), std::ios::out | std::ios::binary); // all unused writer.setVersion(0); writer.setType(0); @@ -369,6 +368,7 @@ namespace ESSImport profile.mInGameTime.mGameHour = context.mHour; profile.mInGameTime.mMonth = context.mMonth; profile.mInGameTime.mYear = context.mYear; + profile.mTimePlayed = 0; profile.mPlayerCell = header.mGameData.mCurrentCell.toString(); if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") profile.mPlayerClassName = context.mCustomPlayerClassName; @@ -385,7 +385,7 @@ namespace ESSImport // Writing order should be Dynamic Store -> Cells -> Player, // so that references to dynamic records can be recognized when loading - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 0) @@ -398,7 +398,7 @@ namespace ESSImport context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_); - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 1) @@ -423,7 +423,7 @@ namespace ESSImport writer.endRecord(ESM::REC_ACTC); // Stage 2 requires cell references to be written / actors IDs assigned - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 2) diff --git a/apps/essimporter/importercontext.hpp b/apps/essimporter/importercontext.hpp index 179e00f087..149059219c 100644 --- a/apps/essimporter/importercontext.hpp +++ b/apps/essimporter/importercontext.hpp @@ -3,13 +3,13 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include "importnpcc.hpp" #include "importcrec.hpp" diff --git a/apps/essimporter/importgame.cpp b/apps/essimporter/importgame.cpp index 1012541b49..df36afe784 100644 --- a/apps/essimporter/importgame.cpp +++ b/apps/essimporter/importgame.cpp @@ -1,6 +1,6 @@ #include "importgame.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importinfo.cpp b/apps/essimporter/importinfo.cpp index 1131553709..49a0c745f6 100644 --- a/apps/essimporter/importinfo.cpp +++ b/apps/essimporter/importinfo.cpp @@ -1,6 +1,6 @@ #include "importinfo.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index dbf9ce0bd8..c4d11763f5 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -2,7 +2,7 @@ #include -#include +#include namespace ESSImport { @@ -19,6 +19,7 @@ namespace ESSImport item.mCount = contItem.mCount; item.mRelativeEquipmentSlot = -1; item.mLockLevel = 0; + item.mRefNum.unset(); unsigned int itemCount = std::abs(item.mCount); bool separateStacks = false; diff --git a/apps/essimporter/importinventory.hpp b/apps/essimporter/importinventory.hpp index a1324a6960..6f757ac9a0 100644 --- a/apps/essimporter/importinventory.hpp +++ b/apps/essimporter/importinventory.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include "importscri.hpp" diff --git a/apps/essimporter/importjour.cpp b/apps/essimporter/importjour.cpp index e5d24e113c..1c46b3159c 100644 --- a/apps/essimporter/importjour.cpp +++ b/apps/essimporter/importjour.cpp @@ -1,6 +1,6 @@ #include "importjour.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importklst.cpp b/apps/essimporter/importklst.cpp index daa1ab0774..5d9f22a31c 100644 --- a/apps/essimporter/importklst.cpp +++ b/apps/essimporter/importklst.cpp @@ -1,6 +1,6 @@ #include "importklst.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp index 3cbd749ce8..4d8da66f0f 100644 --- a/apps/essimporter/importnpcc.cpp +++ b/apps/essimporter/importnpcc.cpp @@ -1,6 +1,6 @@ #include "importnpcc.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp index a23ab1e50b..d525c00743 100644 --- a/apps/essimporter/importnpcc.hpp +++ b/apps/essimporter/importnpcc.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_ESSIMPORT_NPCC_H #define OPENMW_ESSIMPORT_NPCC_H -#include +#include -#include +#include #include "importinventory.hpp" diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp index 8c275a2868..f2ec57ab3f 100644 --- a/apps/essimporter/importplayer.cpp +++ b/apps/essimporter/importplayer.cpp @@ -1,6 +1,6 @@ #include "importplayer.hpp" -#include +#include namespace ESSImport { @@ -13,7 +13,7 @@ namespace ESSImport mActorData.load(esm); - esm.getHNOT(mPos, "DATA", 24); + esm.getHNOTSized<24>(mPos, "DATA"); } void PCDT::load(ESM::ESMReader &esm) diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp index 924522383b..6c8f211c57 100644 --- a/apps/essimporter/importplayer.hpp +++ b/apps/essimporter/importplayer.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include "importacdt.hpp" diff --git a/apps/essimporter/importproj.cpp b/apps/essimporter/importproj.cpp index b2dcf4e7da..aada41a778 100644 --- a/apps/essimporter/importproj.cpp +++ b/apps/essimporter/importproj.cpp @@ -1,6 +1,6 @@ #include "importproj.h" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importques.cpp b/apps/essimporter/importques.cpp index 78b779e439..b57083b0b3 100644 --- a/apps/essimporter/importques.cpp +++ b/apps/essimporter/importques.cpp @@ -1,6 +1,6 @@ #include "importques.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importscpt.cpp b/apps/essimporter/importscpt.cpp index 652383cdaa..5760d8b438 100644 --- a/apps/essimporter/importscpt.cpp +++ b/apps/essimporter/importscpt.cpp @@ -1,7 +1,6 @@ #include "importscpt.hpp" -#include - +#include namespace ESSImport diff --git a/apps/essimporter/importscpt.hpp b/apps/essimporter/importscpt.hpp index 6bfd2603a2..15f4fde598 100644 --- a/apps/essimporter/importscpt.hpp +++ b/apps/essimporter/importscpt.hpp @@ -3,7 +3,7 @@ #include "importscri.hpp" -#include +#include namespace ESM { diff --git a/apps/essimporter/importscri.cpp b/apps/essimporter/importscri.cpp index de0b35c86c..40a5516147 100644 --- a/apps/essimporter/importscri.cpp +++ b/apps/essimporter/importscri.cpp @@ -1,6 +1,7 @@ #include "importscri.hpp" -#include +#include + namespace ESSImport { diff --git a/apps/essimporter/importscri.hpp b/apps/essimporter/importscri.hpp index fe68e50515..73d8942f81 100644 --- a/apps/essimporter/importscri.hpp +++ b/apps/essimporter/importscri.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_ESSIMPORT_IMPORTSCRI_H #define OPENMW_ESSIMPORT_IMPORTSCRI_H -#include +#include #include diff --git a/apps/essimporter/importsplm.cpp b/apps/essimporter/importsplm.cpp index 9fdba4ddb5..f635a8fdbb 100644 --- a/apps/essimporter/importsplm.cpp +++ b/apps/essimporter/importsplm.cpp @@ -1,6 +1,6 @@ #include "importsplm.h" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/importsplm.h b/apps/essimporter/importsplm.h index 8fd5c2bb52..1fc118a8f5 100644 --- a/apps/essimporter/importsplm.h +++ b/apps/essimporter/importsplm.h @@ -41,7 +41,7 @@ struct SPLM { int mUnknown; unsigned char mUnknown2; - ESM::FIXED_STRING<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration + ESM::FixedString<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration }; struct CNAM // 36 bytes diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index d593669c33..9517df2d2a 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -1,15 +1,13 @@ #include +#include #include -#include #include #include "importer.hpp" namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; - int main(int argc, char** argv) @@ -26,6 +24,7 @@ int main(int argc, char** argv) ("encoding", boost::program_options::value()->default_value("win1252"), "encoding of the save file") ; p_desc.add("mwsave", 1).add("output", 1); + Files::ConfigurationManager::addCommonOptions(desc); bpo::variables_map variables; @@ -57,7 +56,7 @@ int main(int argc, char** argv) else { const std::string& ext = ".omwsave"; - if (boost::filesystem::exists(boost::filesystem::path(outputFile)) + if (std::filesystem::exists(std::filesystem::path(outputFile)) && (outputFile.size() < ext.size() || outputFile.substr(outputFile.size()-ext.size()) != ext)) { throw std::runtime_error("Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 329d06a570..79a27094f2 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -13,6 +13,7 @@ set(LAUNCHER utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp + utils/openalutil.cpp ${CMAKE_SOURCE_DIR}/files/windows/launcher.rc ) @@ -31,25 +32,10 @@ set(LAUNCHER_HEADER utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp + utils/openalutil.hpp ) # Headers that must be pre-processed -set(LAUNCHER_HEADER_MOC - datafilespage.hpp - graphicspage.hpp - maindialog.hpp - playpage.hpp - textslotmsgbox.hpp - settingspage.hpp - advancedpage.hpp - - utils/cellnameloader.hpp - utils/textinputdialog.hpp - utils/profilescombobox.hpp - utils/lineedit.hpp - -) - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -58,6 +44,7 @@ set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/directorypicker.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) @@ -71,7 +58,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) -QT5_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -95,7 +81,8 @@ endif (WIN32) target_link_libraries(openmw-launcher ${SDL2_LIBRARY_ONLY} - components + ${OPENAL_LIBRARY} + components_qt ) target_link_libraries(openmw-launcher Qt5::Widgets Qt5::Core) @@ -105,4 +92,16 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw-launcher gcov) endif() +if(USE_QT) + set_property(TARGET openmw-launcher PROPERTY AUTOMOC ON) +endif(USE_QT) +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-launcher PRIVATE + + + + + + ) +endif() diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index b35e786395..eb1a88ea59 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,27 +1,37 @@ #include "advancedpage.hpp" -#include -#include +#include +#include +#include + #include #include -#include +#include + +#include #include #include -#include +#include "utils/openalutil.hpp" -Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, - Config::GameSettings &gameSettings, - Settings::Manager &engineSettings, QWidget *parent) +Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent) : QWidget(parent) - , mCfgMgr(cfg) , mGameSettings(gameSettings) - , mEngineSettings(engineSettings) { setObjectName ("AdvancedPage"); setupUi(this); + for(const std::string& name : Launcher::enumerateOpenALDevices()) + { + audioDeviceSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name)); + } + for(const std::string& name : Launcher::enumerateOpenALDevicesHrtf()) + { + hrtfProfileSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name)); + } + loadSettings(); + mCellNameCompleter.setModel(&mCellNameCompleterModel); startDefaultCharacterAtField->setCompleter(&mCellNameCompleter); } @@ -64,12 +74,12 @@ namespace double convertToCells(double unitRadius) { - return std::round((unitRadius / 0.93 + 1024) / CellSizeInUnits); + return unitRadius / CellSizeInUnits; } - double convertToUnits(double CellGridRadius) + int convertToUnits(double CellGridRadius) { - return (CellSizeInUnits * CellGridRadius - 1024) * 0.93; + return static_cast(CellSizeInUnits * CellGridRadius); } } @@ -89,14 +99,15 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); - int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game"); + int unarmedFactorsStrengthIndex = Settings::Manager::getInt("strength influences hand to hand", "Game"); if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); loadSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); loadSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); - int numPhysicsThreads = mEngineSettings.getInt("async num threads", "Physics"); + int numPhysicsThreads = Settings::Manager::getInt("async num threads", "Physics"); if (numPhysicsThreads >= 0) physicsThreadsSpinBox->setValue(numPhysicsThreads); + loadSettingBool(allowNPCToFollowOverWaterSurfaceCheckBox, "allow actors to follow over water surface", "Game"); } // Visuals @@ -106,11 +117,15 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); loadSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); loadSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); - loadSettingBool(radialFogCheckBox, "radial fog", "Shaders"); + loadSettingBool(softParticlesCheckBox, "soft particles", "Shaders"); + loadSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders"); + if (Settings::Manager::getInt("antialiasing", "Video") == 0) { + antialiasAlphaTestCheckBox->setCheckState(Qt::Unchecked); + } loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool))); loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); - if (animSourcesCheckBox->checkState()) + if (animSourcesCheckBox->checkState() != Qt::Unchecked) { loadSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); @@ -118,27 +133,56 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); - const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); - const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); + const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); + const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); if (distantTerrain && objectPaging) { distantLandCheckBox->setCheckState(Qt::Checked); } loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); - viewingDistanceComboBox->setValue(convertToCells(mEngineSettings.getInt("viewing distance", "Camera"))); + viewingDistanceComboBox->setValue(convertToCells(Settings::Manager::getInt("viewing distance", "Camera"))); + objectPagingMinSizeComboBox->setValue(Settings::Manager::getDouble("object paging min size", "Terrain")); + + loadSettingBool(nightDaySwitchesCheckBox, "day night switches", "Game"); + + connect(postprocessEnabledCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotPostProcessToggled(bool))); + loadSettingBool(postprocessEnabledCheckBox, "enabled", "Post Processing"); + loadSettingBool(postprocessLiveReloadCheckBox, "live reload", "Post Processing"); + loadSettingBool(postprocessTransparentPostpassCheckBox, "transparent postpass", "Post Processing"); + postprocessHDRTimeComboBox->setValue(Settings::Manager::getDouble("hdr exposure time", "Post Processing")); + + connect(skyBlendingCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotSkyBlendingToggled(bool))); + loadSettingBool(radialFogCheckBox, "radial fog", "Fog"); + loadSettingBool(exponentialFogCheckBox, "exponential fog", "Fog"); + loadSettingBool(skyBlendingCheckBox, "sky blending", "Fog"); + skyBlendingStartComboBox->setValue(Settings::Manager::getDouble("sky blending start", "Fog")); } - // Camera + // Audio { - loadSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); - connect(viewOverShoulderCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotViewOverShoulderToggled(bool))); - viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); - loadSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); - loadSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); - loadSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); - loadSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); - defaultShoulderComboBox->setCurrentIndex( - mEngineSettings.getVector2("view over shoulder offset", "Camera").x() >= 0 ? 0 : 1); + std::string selectedAudioDevice = Settings::Manager::getString("device", "Sound"); + if (selectedAudioDevice.empty() == false) + { + int audioDeviceIndex = audioDeviceSelectorComboBox->findData(QString::fromStdString(selectedAudioDevice)); + if (audioDeviceIndex != -1) + { + audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); + } + } + int hrtfEnabledIndex = Settings::Manager::getInt("hrtf enable", "Sound"); + if (hrtfEnabledIndex >= -1 && hrtfEnabledIndex <= 1) + { + enableHRTFComboBox->setCurrentIndex(hrtfEnabledIndex + 1); + } + std::string selectedHRTFProfile = Settings::Manager::getString("hrtf", "Sound"); + if (selectedHRTFProfile.empty() == false) + { + int hrtfProfileIndex = hrtfProfileSelectorComboBox->findData(QString::fromStdString(selectedHRTFProfile)); + if (hrtfProfileIndex != -1) + { + hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); + } + } } // Interface Changes @@ -148,11 +192,14 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); loadSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); - int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); + int showOwnedIndex = Settings::Manager::getInt("show owned", "Game"); // Match the index with the option (only 0, 1, 2, or 3 are valid). Will default to 0 if invalid. if (showOwnedIndex >= 0 && showOwnedIndex <= 3) showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + loadSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map"); + loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + scalingSpinBox->setValue(Settings::Manager::getFloat("scaling factor", "GUI")); } // Bug fixes @@ -165,13 +212,15 @@ bool Launcher::AdvancedPage::loadSettings() { // Saves loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - maximumQuicksavesComboBox->setValue(mEngineSettings.getInt("max quicksaves", "Saves")); + loadSettingInt(maximumQuicksavesComboBox,"max quicksaves", "Saves"); // Other Settings - QString screenshotFormatString = QString::fromStdString(mEngineSettings.getString("screenshot format", "General")).toUpper(); + QString screenshotFormatString = QString::fromStdString(Settings::Manager::getString("screenshot format", "General")).toUpper(); if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) screenshotFormatComboBox->addItem(screenshotFormatString); screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString)); + + loadSettingBool(notifyOnSavedScreenshotCheckBox, "notify on saved screenshot", "General"); } // Testing @@ -208,14 +257,11 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); - int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); - if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game")) - mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); + saveSettingInt(unarmedFactorsStrengthComboBox, "strength influences hand to hand", "Game"); saveSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); saveSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); - int numPhysicsThreads = physicsThreadsSpinBox->value(); - if (numPhysicsThreads != mEngineSettings.getInt("async num threads", "Physics")) - mEngineSettings.setInt("async num threads", "Physics", numPhysicsThreads); + saveSettingInt(physicsThreadsSpinBox, "async num threads", "Physics"); + saveSettingBool(allowNPCToFollowOverWaterSurfaceCheckBox, "allow actors to follow over water surface", "Game"); } // Visuals @@ -225,7 +271,9 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); saveSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); saveSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); - saveSettingBool(radialFogCheckBox, "radial fog", "Shaders"); + saveSettingBool(radialFogCheckBox, "radial fog", "Fog"); + saveSettingBool(softParticlesCheckBox, "soft particles", "Shaders"); + saveSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders"); saveSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); saveSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); @@ -233,38 +281,69 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); - const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); - const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); + const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); + const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); const bool wantDistantLand = distantLandCheckBox->checkState(); if (wantDistantLand != (distantTerrain && objectPaging)) { - mEngineSettings.setBool("distant terrain", "Terrain", wantDistantLand); - mEngineSettings.setBool("object paging", "Terrain", wantDistantLand); + Settings::Manager::setBool("distant terrain", "Terrain", wantDistantLand); + Settings::Manager::setBool("object paging", "Terrain", wantDistantLand); } saveSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); - double viewingDistance = viewingDistanceComboBox->value(); - if (viewingDistance != convertToCells(mEngineSettings.getInt("viewing distance", "Camera"))) + int viewingDistance = convertToUnits(viewingDistanceComboBox->value()); + if (viewingDistance != Settings::Manager::getInt("viewing distance", "Camera")) { - mEngineSettings.setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); + Settings::Manager::setInt("viewing distance", "Camera", viewingDistance); } + double objectPagingMinSize = objectPagingMinSizeComboBox->value(); + if (objectPagingMinSize != Settings::Manager::getDouble("object paging min size", "Terrain")) + Settings::Manager::setDouble("object paging min size", "Terrain", objectPagingMinSize); + + saveSettingBool(nightDaySwitchesCheckBox, "day night switches", "Game"); + + saveSettingBool(postprocessEnabledCheckBox, "enabled", "Post Processing"); + saveSettingBool(postprocessLiveReloadCheckBox, "live reload", "Post Processing"); + saveSettingBool(postprocessTransparentPostpassCheckBox, "transparent postpass", "Post Processing"); + double hdrExposureTime = postprocessHDRTimeComboBox->value(); + if (hdrExposureTime != Settings::Manager::getDouble("hdr exposure time", "Post Processing")) + Settings::Manager::setDouble("hdr exposure time", "Post Processing", hdrExposureTime); + + saveSettingBool(radialFogCheckBox, "radial fog", "Fog"); + saveSettingBool(exponentialFogCheckBox, "exponential fog", "Fog"); + saveSettingBool(skyBlendingCheckBox, "sky blending", "Fog"); + Settings::Manager::setDouble("sky blending start", "Fog", skyBlendingStartComboBox->value()); } - - // Camera + + // Audio { - saveSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); - saveSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); - saveSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); - saveSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); - saveSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); - - osg::Vec2f shoulderOffset = mEngineSettings.getVector2("view over shoulder offset", "Camera"); - if (defaultShoulderComboBox->currentIndex() != (shoulderOffset.x() >= 0 ? 0 : 1)) + int audioDeviceIndex = audioDeviceSelectorComboBox->currentIndex(); + std::string prevAudioDevice = Settings::Manager::getString("device", "Sound"); + if (audioDeviceIndex != 0) { - if (defaultShoulderComboBox->currentIndex() == 0) - shoulderOffset.x() = std::abs(shoulderOffset.x()); - else - shoulderOffset.x() = -std::abs(shoulderOffset.x()); - mEngineSettings.setVector2("view over shoulder offset", "Camera", shoulderOffset); + const std::string& newAudioDevice = audioDeviceSelectorComboBox->currentText().toUtf8().constData(); + if (newAudioDevice != prevAudioDevice) + Settings::Manager::setString("device", "Sound", newAudioDevice); + } + else if (!prevAudioDevice.empty()) + { + Settings::Manager::setString("device", "Sound", {}); + } + int hrtfEnabledIndex = enableHRTFComboBox->currentIndex() - 1; + if (hrtfEnabledIndex != Settings::Manager::getInt("hrtf enable", "Sound")) + { + Settings::Manager::setInt("hrtf enable", "Sound", hrtfEnabledIndex); + } + int selectedHRTFProfileIndex = hrtfProfileSelectorComboBox->currentIndex(); + std::string prevHRTFProfile = Settings::Manager::getString("hrtf", "Sound"); + if (selectedHRTFProfileIndex != 0) + { + const std::string& newHRTFProfile = hrtfProfileSelectorComboBox->currentText().toUtf8().constData(); + if (newHRTFProfile != prevHRTFProfile) + Settings::Manager::setString("hrtf", "Sound", newHRTFProfile); + } + else if (!prevHRTFProfile.empty()) + { + Settings::Manager::setString("hrtf", "Sound", {}); } } @@ -275,10 +354,13 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); saveSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); - int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); - if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) - mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); + saveSettingInt(showOwnedComboBox,"show owned", "Game"); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + saveSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map"); + saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + float uiScalingFactor = scalingSpinBox->value(); + if (uiScalingFactor != Settings::Manager::getFloat("scaling factor", "GUI")) + Settings::Manager::setFloat("scaling factor", "GUI", uiScalingFactor); } // Bug fixes @@ -291,16 +373,14 @@ void Launcher::AdvancedPage::saveSettings() { // Saves Settings saveSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - int maximumQuicksaves = maximumQuicksavesComboBox->value(); - if (maximumQuicksaves != mEngineSettings.getInt("max quicksaves", "Saves")) - { - mEngineSettings.setInt("max quicksaves", "Saves", maximumQuicksaves); - } + saveSettingInt(maximumQuicksavesComboBox, "max quicksaves", "Saves"); // Other Settings std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); - if (screenshotFormatString != mEngineSettings.getString("screenshot format", "General")) - mEngineSettings.setString("screenshot format", "General", screenshotFormatString); + if (screenshotFormatString != Settings::Manager::getString("screenshot format", "General")) + Settings::Manager::setString("screenshot format", "General", screenshotFormatString); + + saveSettingBool(notifyOnSavedScreenshotCheckBox, "notify on saved screenshot", "General"); } // Testing @@ -324,15 +404,41 @@ void Launcher::AdvancedPage::saveSettings() void Launcher::AdvancedPage::loadSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) { - if (mEngineSettings.getBool(setting, group)) + if (Settings::Manager::getBool(setting, group)) checkbox->setCheckState(Qt::Checked); } void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) { bool cValue = checkbox->checkState(); - if (cValue != mEngineSettings.getBool(setting, group)) - mEngineSettings.setBool(setting, group, cValue); + if (cValue != Settings::Manager::getBool(setting, group)) + Settings::Manager::setBool(setting, group, cValue); +} + +void Launcher::AdvancedPage::loadSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group) +{ + int currentIndex = Settings::Manager::getInt(setting, group); + comboBox->setCurrentIndex(currentIndex); +} + +void Launcher::AdvancedPage::saveSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group) +{ + int currentIndex = comboBox->currentIndex(); + if (currentIndex != Settings::Manager::getInt(setting, group)) + Settings::Manager::setInt(setting, group, currentIndex); +} + +void Launcher::AdvancedPage::loadSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group) +{ + int value = Settings::Manager::getInt(setting, group); + spinBox->setValue(value); +} + +void Launcher::AdvancedPage::saveSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group) +{ + int value = spinBox->value(); + if (value != Settings::Manager::getInt(setting, group)) + Settings::Manager::setInt(setting, group, value); } void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) @@ -351,7 +457,16 @@ void Launcher::AdvancedPage::slotAnimSourcesToggled(bool checked) } } -void Launcher::AdvancedPage::slotViewOverShoulderToggled(bool checked) +void Launcher::AdvancedPage::slotPostProcessToggled(bool checked) +{ + postprocessLiveReloadCheckBox->setEnabled(checked); + postprocessTransparentPostpassCheckBox->setEnabled(checked); + postprocessHDRTimeComboBox->setEnabled(checked); + postprocessHDRTimeLabel->setEnabled(checked); +} + +void Launcher::AdvancedPage::slotSkyBlendingToggled(bool checked) { - viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); + skyBlendingStartComboBox->setEnabled(checked); + skyBlendingStartLabel->setEnabled(checked); } diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index bdf5af0c85..a011602da8 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -1,7 +1,6 @@ #ifndef ADVANCEDPAGE_H #define ADVANCEDPAGE_H -#include #include #include @@ -9,7 +8,6 @@ #include -namespace Files { struct ConfigurationManager; } namespace Config { class GameSettings; } namespace Launcher @@ -19,8 +17,7 @@ namespace Launcher Q_OBJECT public: - AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, - Settings::Manager &engineSettings, QWidget *parent = 0); + explicit AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent = nullptr); bool loadSettings(); void saveSettings(); @@ -32,12 +29,11 @@ namespace Launcher void on_skipMenuCheckBox_stateChanged(int state); void on_runScriptAfterStartupBrowseButton_clicked(); void slotAnimSourcesToggled(bool checked); - void slotViewOverShoulderToggled(bool checked); + void slotPostProcessToggled(bool checked); + void slotSkyBlendingToggled(bool checked); private: - Files::ConfigurationManager &mCfgMgr; Config::GameSettings &mGameSettings; - Settings::Manager &mEngineSettings; QCompleter mCellNameCompleter; QStringListModel mCellNameCompleterModel; @@ -46,8 +42,12 @@ namespace Launcher * @param filePaths the file paths of the content files to be examined */ void loadCellsForAutocomplete(QStringList filePaths); - void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); - void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void loadSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group); + static void saveSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group); + static void loadSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group); + static void saveSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group); }; } #endif diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 81544b0945..e1b34a8034 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,41 +1,131 @@ #include "datafilespage.hpp" +#include "maindialog.hpp" #include - #include #include -#include -#include -#include +#include + #include #include +#include #include #include #include -#include #include #include #include -#include + +#include +#include +#include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" +#include "ui_directorypicker.h" const char *Launcher::DataFilesPage::mDefaultContentListName = "Default"; -Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) +namespace +{ + void contentSubdirs(const QString& path, QStringList& dirs) + { + QStringList fileFilter {"*.esm", "*.esp", "*.omwaddon", "*.bsa"}; + QStringList dirFilter {"bookart", "icons", "meshes", "music", "sound", "textures"}; + + QDir currentDir(path); + if (!currentDir.entryInfoList(fileFilter, QDir::Files).empty() + || !currentDir.entryInfoList(dirFilter, QDir::Dirs | QDir::NoDotAndDotDot).empty()) + dirs.push_back(currentDir.canonicalPath()); + + for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + contentSubdirs(subdir.canonicalFilePath(), dirs); + } +} + +namespace Launcher +{ + namespace + { + struct HandleNavMeshToolMessage + { + int mCellsCount; + int mExpectedMaxProgress; + int mMaxProgress; + int mProgress; + + HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedCells&& message) const + { + return HandleNavMeshToolMessage { + static_cast(message.mCount), + mExpectedMaxProgress, + static_cast(message.mCount) * 100, + mProgress + }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::ProcessedCells&& message) const + { + return HandleNavMeshToolMessage { + mCellsCount, + mExpectedMaxProgress, + mMaxProgress, + std::max(mProgress, static_cast(message.mCount)) + }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedTiles&& message) const + { + const int expectedMaxProgress = mCellsCount + static_cast(message.mCount); + return HandleNavMeshToolMessage { + mCellsCount, + expectedMaxProgress, + std::max(mMaxProgress, expectedMaxProgress), + mProgress + }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::GeneratedTiles&& message) const + { + int progress = mCellsCount + static_cast(message.mCount); + if (mExpectedMaxProgress < mMaxProgress) + progress += static_cast(std::round( + (mMaxProgress - mExpectedMaxProgress) + * (static_cast(progress) / static_cast(mExpectedMaxProgress)) + )); + return HandleNavMeshToolMessage { + mCellsCount, + mExpectedMaxProgress, + mMaxProgress, + std::max(mProgress, progress) + }; + } + }; + + int getMaxNavMeshDbFileSizeMiB() + { + return static_cast(Settings::Manager::getInt64("max navmeshdb file size", "Navigator") / (1024 * 1024)); + } + } +} + +Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) : QWidget(parent) + , mMainDialog(parent) , mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) + , mNavMeshToolInvoker(new Process::ProcessInvoker(this)) { ui.setupUi (this); setObjectName ("DataFilesPage"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/true); const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); @@ -46,6 +136,14 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: this, SLOT(updateNewProfileOkButton(QString))); connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateCloneProfileOkButton(QString))); + 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.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); }); buildView(); loadSettings(); @@ -60,15 +158,12 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: void Launcher::DataFilesPage::buildView() { - ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); - - QToolButton * refreshButton = mSelector->refreshButton(); + QToolButton * refreshButton = mSelector->refreshButton(); //tool buttons ui.newProfileButton->setToolTip ("Create a new Content List"); ui.cloneProfileButton->setToolTip ("Clone the current Content List"); ui.deleteProfileButton->setToolTip ("Delete an existing Content List"); - refreshButton->setToolTip("Refresh Data Files"); //combo box ui.profilesComboBox->addItem(mDefaultContentListName); @@ -92,10 +187,19 @@ void Launcher::DataFilesPage::buildView() this, SLOT (slotProfileChangedByUser(QString, QString))); connect(ui.refreshDataFilesAction, SIGNAL(triggered()),this, SLOT(slotRefreshButtonClicked())); + + connect(ui.updateNavMeshButton, SIGNAL(clicked()), this, SLOT(startNavMeshTool())); + connect(ui.cancelNavMeshButton, SIGNAL(clicked()), this, SLOT(killNavMeshTool())); + + connect(mNavMeshToolInvoker->getProcess(), SIGNAL(readyReadStandardOutput()), this, SLOT(readNavMeshToolStdout())); + connect(mNavMeshToolInvoker->getProcess(), SIGNAL(readyReadStandardError()), this, SLOT(readNavMeshToolStderr())); + connect(mNavMeshToolInvoker->getProcess(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(navMeshToolFinished(int, QProcess::ExitStatus))); } bool Launcher::DataFilesPage::loadSettings() { + ui.navMeshMaxSizeSpinBox->setValue(getMaxNavMeshDbFileSizeMiB()); + QStringList profiles = mLauncherSettings.getContentLists(); QString currentProfile = mLauncherSettings.getCurrentContentListName(); @@ -113,19 +217,96 @@ bool Launcher::DataFilesPage::loadSettings() void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) { - QStringList paths = mGameSettings.getDataDirs(); + mSelector->clearFiles(); + ui.archiveListWidget->clear(); + ui.directoryListWidget->clear(); - mDataLocal = mGameSettings.getDataLocal(); + QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName); + if (directories.isEmpty()) + directories = mGameSettings.getDataDirs(); + mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) - paths.insert(0, mDataLocal); + directories.insert(0, mDataLocal); - mSelector->clearFiles(); + const auto globalDataDir = QString(mGameSettings.getGlobalDataDir().c_str()); + if (!globalDataDir.isEmpty()) + directories.insert(0, globalDataDir); + + // normalize user supplied directories: resolve symlink, convert to native separator, make absolute + for (auto& currentDir : directories) + currentDir = QDir(QDir::cleanPath(currentDir)).canonicalPath(); + + // add directories, archives and content files + directories.removeDuplicates(); + for (const auto& currentDir : directories) + { + // add new achives files presents in current directory + addArchivesFromDir(currentDir); + + QString tooltip; + + // add content files presents in current directory + mSelector->addFiles(currentDir, mNewDataDirs.contains(currentDir)); + + // add current directory to list + ui.directoryListWidget->addItem(currentDir); + auto row = ui.directoryListWidget->count() - 1; + auto* item = ui.directoryListWidget->item(row); + + // Display new content with green background + if (mNewDataDirs.contains(currentDir)) + { + tooltip += "Will be added to the current profile\n"; + item->setBackground(Qt::green); + item->setForeground(Qt::black); + } + + // deactivate data-local and global data directory: they are always included + if (currentDir == mDataLocal || currentDir == globalDataDir) + { + auto flags = item->flags(); + item->setFlags(flags & ~(Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled|Qt::ItemIsEnabled)); + } + + // Add a "data file" icon if the directory contains a content file + if (mSelector->containsDataFiles(currentDir)) + { + item->setIcon(QIcon(":/images/openmw-plugin.png")); + tooltip += "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()); + auto emptyIcon = QIcon(pixmap); + item->setIcon(emptyIcon); + } + item->setToolTip(tooltip); + } + mSelector->sortFiles(); - for (const QString &path : paths) - mSelector->addFiles(path); + QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName); + if (selectedArchives.isEmpty()) + selectedArchives = mGameSettings.getArchiveList(); - PathIterator pathIterator(paths); + // sort and tick BSA according to profile + int row = 0; + for (const auto& archive : selectedArchives) + { + const auto match = ui.archiveListWidget->findItems(archive, Qt::MatchExactly); + if (match.isEmpty()) + continue; + const auto name = match[0]->text(); + const auto oldrow = ui.archiveListWidget->row(match[0]); + ui.archiveListWidget->takeItem(oldrow); + ui.archiveListWidget->insertItem(row, name); + ui.archiveListWidget->item(row)->setCheckState(Qt::Checked); + row++; + } + + PathIterator pathIterator(directories); mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator)); } @@ -148,13 +329,19 @@ QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, void Launcher::DataFilesPage::saveSettings(const QString &profile) { - QString profileName = profile; + if (const int value = ui.navMeshMaxSizeSpinBox->value(); value != getMaxNavMeshDbFileSizeMiB()) + Settings::Manager::setInt64("max navmeshdb file size", "Navigator", static_cast(value) * 1024 * 1024); + + QString profileName = profile; + + if (profileName.isEmpty()) + profileName = ui.profilesComboBox->currentText(); - if (profileName.isEmpty()) - profileName = ui.profilesComboBox->currentText(); + //retrieve the data paths + auto dirList = selectedDirectoriesPaths(); - //retrieve the files selected for the profile - ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); + //retrieve the files selected for the profile + ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); //set the value of the current profile (not necessarily the profile being saved!) mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText()); @@ -164,11 +351,36 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) { fileNames.append(item->fileName()); } - mLauncherSettings.setContentList(profileName, fileNames); - mGameSettings.setContentList(fileNames); + mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); + mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); } -QStringList Launcher::DataFilesPage::selectedFilePaths() +QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const +{ + QStringList dirList; + for (int i = 0; i < ui.directoryListWidget->count(); ++i) + { + if (ui.directoryListWidget->item(i)->flags() & Qt::ItemIsEnabled) + dirList.append(ui.directoryListWidget->item(i)->text()); + } + return dirList; +} + +QStringList Launcher::DataFilesPage::selectedArchivePaths(bool all) const +{ + QStringList archiveList; + for (int i = 0; i < ui.archiveListWidget->count(); ++i) + { + const auto* item = ui.archiveListWidget->item(i); + const auto archive = ui.archiveListWidget->item(i)->text(); + + if (all ||item->checkState() == Qt::Checked) + archiveList.append(item->text()); + } + return archiveList; +} + +QStringList Launcher::DataFilesPage::selectedFilePaths() const { //retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); @@ -176,15 +388,8 @@ QStringList Launcher::DataFilesPage::selectedFilePaths() for (const ContentSelectorModel::EsmFile *item : items) { QFile file(item->filePath()); - if(file.exists()) - { filePaths.append(item->filePath()); - } - else - { - slotRefreshButtonClicked(); - } } return filePaths; } @@ -228,8 +433,17 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current)); + mNewDataDirs.clear(); + mKnownArchives.clear(); populateFileViews(current); + // save list of "old" bsa to be able to display "new" bsa in a different colour + for (int i = 0; i < ui.archiveListWidget->count(); ++i) + { + auto* item = ui.archiveListWidget->item(i); + mKnownArchives.push_back(item->text()); + } + checkForDefaultProfile(); } @@ -318,7 +532,7 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered() if (profile.isEmpty()) return; - mLauncherSettings.setContentList(profile, selectedFilePaths()); + mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths()); addProfile(profile, true); } @@ -356,6 +570,158 @@ 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(); + + if (selectedRow == -1) + return; + + const auto rootDir = selectDirectory(); + if (rootDir.isEmpty()) + return; + + QStringList subdirs; + contentSubdirs(rootDir, subdirs); + + if (subdirs.empty()) + { + // 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(); + } + return; + } + + QDialog dialog; + Ui::SelectSubdirs select; + + select.setupUi(&dialog); + + for (const auto& dir : subdirs) + { + if (!ui.directoryListWidget->findItems(dir, Qt::MatchFixedString).isEmpty()) + continue; + const auto lastRow = select.dirListWidget->count(); + select.dirListWidget->addItem(dir); + select.dirListWidget->item(lastRow)->setCheckState(Qt::Unchecked); + } + + dialog.show(); + + if (dialog.exec() == QDialog::Rejected) + return; + + for (int i = 0; i < select.dirListWidget->count(); ++i) + { + const auto* dir = select.dirListWidget->item(i); + if (dir->checkState() == Qt::Checked) + { + ui.directoryListWidget->insertItem(selectedRow++, dir->text()); + mNewDataDirs.push_back(dir->text()); + } + } + + refreshDataFilesView(); +} + +void Launcher::DataFilesPage::sortDirectories() +{ + // Ensure disabled entries (aka default directories) are always at the top. + for (auto i = 1; i < ui.directoryListWidget->count(); ++i) + { + if (!(ui.directoryListWidget->item(i)->flags() & Qt::ItemIsEnabled) && + (ui.directoryListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled)) + { + const auto item = ui.directoryListWidget->takeItem(i); + ui.directoryListWidget->insertItem(i - 1, item); + ui.directoryListWidget->setCurrentRow(i); + } + } +} + +void Launcher::DataFilesPage::moveDirectory(int step) +{ + 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); +} + +void Launcher::DataFilesPage::removeDirectory() +{ + for (const auto& path : ui.directoryListWidget->selectedItems()) + ui.directoryListWidget->takeItem(ui.directoryListWidget->row(path)); + refreshDataFilesView(); +} + +void Launcher::DataFilesPage::moveArchive(int step) +{ + int selectedRow = ui.archiveListWidget->currentRow(); + int newRow = selectedRow + step; + if (selectedRow == -1 || newRow < 0 || newRow > ui.archiveListWidget->count() - 1) + return; + + const auto* item = ui.archiveListWidget->takeItem(selectedRow); + + addArchive(item->text(), item->checkState(), newRow); + ui.archiveListWidget->setCurrentRow(newRow); +} + +void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState selected, int row) +{ + if (row == -1) + row = ui.archiveListWidget->count(); + ui.archiveListWidget->insertItem(row, name); + ui.archiveListWidget->item(row)->setCheckState(selected); + 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); + } +} + +void Launcher::DataFilesPage::addArchivesFromDir(const QString& path) +{ + QDir dir(path, "*.bsa"); + + for (const auto& fileinfo : dir.entryInfoList()) + { + const auto absPath = fileinfo.absoluteFilePath(); + if (Bsa::CompressedBSAFile::detectVersion(absPath.toStdString()) == Bsa::BSAVER_UNKNOWN) + continue; + + const auto fileName = fileinfo.fileName(); + const auto currentList = selectedArchivePaths(true); + + if (!currentList.contains(fileName, Qt::CaseInsensitive)) + addArchive(fileName, Qt::Unchecked); + } +} + void Launcher::DataFilesPage::checkForDefaultProfile() { //don't allow deleting "Default" profile @@ -394,13 +760,13 @@ void Launcher::DataFilesPage::slotAddonDataChanged() } // Mutex lock to run reloadCells synchronously. -std::mutex _reloadCellsMutex; +static std::mutex reloadCellsMutex; void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) { // Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time // Based on https://stackoverflow.com/a/5429695/531762 - std::unique_lock lock(_reloadCellsMutex); + std::unique_lock lock(reloadCellsMutex); // The following code will run only if there is not another thread currently running it CellNameLoader cellNameLoader; @@ -413,3 +779,89 @@ void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) std::sort(cellNamesList.begin(), cellNamesList.end()); emit signalLoadedCellsChanged(cellNamesList); } + +void Launcher::DataFilesPage::startNavMeshTool() +{ + mMainDialog->writeSettings(); + + ui.navMeshLogPlainTextEdit->clear(); + ui.navMeshProgressBar->setValue(0); + ui.navMeshProgressBar->setMaximum(1); + + mNavMeshToolProgress = NavMeshToolProgress {}; + + QStringList arguments({"--write-binary-log"}); + if (ui.navMeshRemoveUnusedTilesCheckBox->checkState() == Qt::Checked) + arguments.append("--remove-unused-tiles"); + + if (!mNavMeshToolInvoker->startProcess(QLatin1String("openmw-navmeshtool"), arguments)) + return; + + ui.cancelNavMeshButton->setEnabled(true); + ui.navMeshProgressBar->setEnabled(true); +} + +void Launcher::DataFilesPage::killNavMeshTool() +{ + mNavMeshToolInvoker->killProcess(); +} + +void Launcher::DataFilesPage::readNavMeshToolStderr() +{ + updateNavMeshProgress(4096); +} + +void Launcher::DataFilesPage::updateNavMeshProgress(int minDataSize) +{ + QProcess& process = *mNavMeshToolInvoker->getProcess(); + mNavMeshToolProgress.mMessagesData.append(process.readAllStandardError()); + if (mNavMeshToolProgress.mMessagesData.size() < minDataSize) + return; + const std::byte* const begin = reinterpret_cast(mNavMeshToolProgress.mMessagesData.constData()); + const std::byte* const end = begin + mNavMeshToolProgress.mMessagesData.size(); + const std::byte* position = begin; + HandleNavMeshToolMessage handle { + mNavMeshToolProgress.mCellsCount, + mNavMeshToolProgress.mExpectedMaxProgress, + ui.navMeshProgressBar->maximum(), + ui.navMeshProgressBar->value(), + }; + while (true) + { + NavMeshTool::Message message; + const std::byte* const nextPosition = NavMeshTool::deserialize(position, end, message); + if (nextPosition == position) + break; + position = nextPosition; + handle = std::visit(handle, NavMeshTool::decode(message)); + } + if (position != begin) + mNavMeshToolProgress.mMessagesData = mNavMeshToolProgress.mMessagesData.mid(position - begin); + mNavMeshToolProgress.mCellsCount = handle.mCellsCount; + mNavMeshToolProgress.mExpectedMaxProgress = handle.mExpectedMaxProgress; + ui.navMeshProgressBar->setMaximum(handle.mMaxProgress); + ui.navMeshProgressBar->setValue(handle.mProgress); +} + +void Launcher::DataFilesPage::readNavMeshToolStdout() +{ + QProcess& process = *mNavMeshToolInvoker->getProcess(); + QByteArray& logData = mNavMeshToolProgress.mLogData; + logData.append(process.readAllStandardOutput()); + const int lineEnd = logData.lastIndexOf('\n'); + if (lineEnd == -1) + return; + const int size = logData.size() >= lineEnd && logData[lineEnd - 1] == '\r' ? lineEnd - 1 : lineEnd; + ui.navMeshLogPlainTextEdit->appendPlainText(QString::fromUtf8(logData.data(), size)); + logData = logData.mid(lineEnd + 1); +} + +void Launcher::DataFilesPage::navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + updateNavMeshProgress(0); + ui.navMeshLogPlainTextEdit->appendPlainText(QString::fromUtf8(mNavMeshToolInvoker->getProcess()->readAllStandardOutput())); + if (exitCode == 0 && exitStatus == QProcess::ExitStatus::NormalExit) + ui.navMeshProgressBar->setValue(ui.navMeshProgressBar->maximum()); + ui.cancelNavMeshButton->setEnabled(false); + ui.navMeshProgressBar->setEnabled(false); +} diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index af54fe75e4..0a235209f3 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -2,11 +2,11 @@ #define DATAFILESPAGE_H #include "ui_datafilespage.h" -#include +#include +#include #include -#include #include class QSortFilterProxyModel; @@ -20,6 +20,7 @@ namespace Config { class GameSettings; namespace Launcher { + class MainDialog; class TextInputDialog; class ProfilesComboBox; @@ -32,7 +33,7 @@ namespace Launcher public: explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, QWidget *parent = 0); + Config::LauncherSettings &launcherSettings, MainDialog *parent = nullptr); QAbstractItemModel* profilesModel() const; @@ -42,12 +43,6 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); - /** - * Returns the file paths of all selected content files - * @return the file paths of all selected content files - */ - QStringList selectedFilePaths(); - signals: void signalProfileChanged (int index); void signalLoadedCellsChanged(QStringList selectedFiles); @@ -65,17 +60,37 @@ namespace Launcher void updateNewProfileOkButton(const QString &text); void updateCloneProfileOkButton(const QString &text); + void addSubdirectories(bool append); + void sortDirectories(); + void removeDirectory(); + void moveArchive(int step); + void moveDirectory(int step); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); void on_deleteProfileAction_triggered(); + void startNavMeshTool(); + void killNavMeshTool(); + void readNavMeshToolStdout(); + void readNavMeshToolStderr(); + void navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus); + public: /// Content List that is always present const static char *mDefaultContentListName; private: + struct NavMeshToolProgress + { + QByteArray mLogData; + QByteArray mMessagesData; + std::map mWorldspaces; + int mCellsCount = 0; + int mExpectedMaxProgress = 0; + }; + MainDialog *mMainDialog; TextInputDialog *mNewProfileDialog; TextInputDialog *mCloneProfileDialog; @@ -87,12 +102,15 @@ namespace Launcher QString mPreviousProfile; QStringList previousSelectedFiles; QString mDataLocal; + QStringList mKnownArchives; + QStringList mNewDataDirs; - void setPluginsCheckstates(Qt::CheckState state); + Process::ProcessInvoker* mNavMeshToolInvoker; + NavMeshToolProgress mNavMeshToolProgress; + void addArchive(const QString& name, Qt::CheckState selected, int row = -1); + void addArchivesFromDir(const QString& dir); void buildView(); - void setupConfig(); - void readConfig(); void setProfile (int index, bool savePrevious); void setProfile (const QString &previous, const QString ¤t, bool savePrevious); void removeProfile (const QString &profile); @@ -102,6 +120,16 @@ namespace Launcher void populateFileViews(const QString& contentModelName); void reloadCells(QStringList selectedFiles); void refreshDataFilesView (); + void updateNavMeshProgress(int minDataSize); + QString selectDirectory(); + + /** + * Returns the file paths of all selected content files + * @return the file paths of all selected content files + */ + QStringList selectedFilePaths() const; + QStringList selectedArchivePaths(bool all=false) const; + QStringList selectedDirectoriesPaths() const; class PathIterator { diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index d7e7eabc50..5cdfb578da 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -1,8 +1,6 @@ #include "graphicspage.hpp" -#include #include -#include #include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED @@ -14,12 +12,14 @@ #include #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 @@ -29,10 +29,8 @@ QString getAspect(int x, int y) return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); } -Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent) +Launcher::GraphicsPage::GraphicsPage(QWidget *parent) : QWidget(parent) - , mCfgMgr(cfg) - , mEngineSettings(engineSettings) { setObjectName ("GraphicsPage"); setupUi(this); @@ -42,12 +40,11 @@ Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings: customWidthSpinBox->setMaximum(res.width()); customHeightSpinBox->setMaximum(res.height()); - connect(fullScreenCheckBox, SIGNAL(stateChanged(int)), this, SLOT(slotFullScreenChanged(int))); + connect(windowModeComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotFullScreenChanged(int))); connect(standardRadioButton, SIGNAL(toggled(bool)), this, SLOT(slotStandardToggled(bool))); connect(screenComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(screenChanged(int))); connect(framerateLimitCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotFramerateLimitToggled(bool))); connect(shadowDistanceCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotShadowDistLimitToggled(bool))); - } bool Launcher::GraphicsPage::setupSDL() @@ -91,26 +88,29 @@ bool Launcher::GraphicsPage::loadSettings() if (!setupSDL()) return false; - if (mEngineSettings.getBool("vsync", "Video")) + // Visuals + if (Settings::Manager::getBool("vsync", "Video")) vSyncCheckBox->setCheckState(Qt::Checked); - if (mEngineSettings.getBool("fullscreen", "Video")) - fullScreenCheckBox->setCheckState(Qt::Checked); + size_t windowMode = static_cast(Settings::Manager::getInt("window mode", "Video")); + if (windowMode > static_cast(Settings::WindowMode::Windowed)) + windowMode = 0; + windowModeComboBox->setCurrentIndex(windowMode); - if (mEngineSettings.getBool("window border", "Video")) + if (Settings::Manager::getBool("window border", "Video")) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) - int aaValue = mEngineSettings.getInt("antialiasing", "Video"); + int aaValue = Settings::Manager::getInt("antialiasing", "Video"); // aaIndex is the index into the allowed values in the pull down. int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); - int width = mEngineSettings.getInt("resolution x", "Video"); - int height = mEngineSettings.getInt("resolution y", "Video"); + 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(mEngineSettings.getInt("screen", "Video")); + screenComboBox->setCurrentIndex(Settings::Manager::getInt("screen", "Video")); int resIndex = resolutionComboBox->findText(resolution, Qt::MatchStartsWith); @@ -123,40 +123,49 @@ bool Launcher::GraphicsPage::loadSettings() customHeightSpinBox->setValue(height); } - float fpsLimit = mEngineSettings.getFloat("framerate limit", "Video"); + float fpsLimit = Settings::Manager::getFloat("framerate limit", "Video"); if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); framerateLimitSpinBox->setValue(fpsLimit); } - if (mEngineSettings.getBool("actor shadows", "Shadows")) + // 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 (mEngineSettings.getBool("player shadows", "Shadows")) + if (Settings::Manager::getBool("player shadows", "Shadows")) playerShadowsCheckBox->setCheckState(Qt::Checked); - if (mEngineSettings.getBool("terrain shadows", "Shadows")) + if (Settings::Manager::getBool("terrain shadows", "Shadows")) terrainShadowsCheckBox->setCheckState(Qt::Checked); - if (mEngineSettings.getBool("object shadows", "Shadows")) + if (Settings::Manager::getBool("object shadows", "Shadows")) objectShadowsCheckBox->setCheckState(Qt::Checked); - if (mEngineSettings.getBool("enable indoor shadows", "Shadows")) + if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) indoorShadowsCheckBox->setCheckState(Qt::Checked); shadowComputeSceneBoundsComboBox->setCurrentIndex( shadowComputeSceneBoundsComboBox->findText( - QString(tr(mEngineSettings.getString("compute scene bounds", "Shadows").c_str())))); + QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str())))); - int shadowDistLimit = mEngineSettings.getInt("maximum shadow map distance", "Shadows"); + int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows"); if (shadowDistLimit > 0) { shadowDistanceCheckBox->setCheckState(Qt::Checked); shadowDistanceSpinBox->setValue(shadowDistLimit); } - float shadowFadeStart = mEngineSettings.getFloat("shadow fade start", "Shadows"); + float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows"); if (shadowFadeStart != 0) fadeStartSpinBox->setValue(shadowFadeStart); - int shadowRes = mEngineSettings.getInt("shadow map resolution", "Shadows"); + int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows"); int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); if (shadowResIndex != -1) shadowResolutionComboBox->setCurrentIndex(shadowResIndex); @@ -166,23 +175,25 @@ bool Launcher::GraphicsPage::loadSettings() 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) bool cVSync = vSyncCheckBox->checkState(); - if (cVSync != mEngineSettings.getBool("vsync", "Video")) - mEngineSettings.setBool("vsync", "Video", cVSync); + if (cVSync != Settings::Manager::getBool("vsync", "Video")) + Settings::Manager::setBool("vsync", "Video", cVSync); - bool cFullScreen = fullScreenCheckBox->checkState(); - if (cFullScreen != mEngineSettings.getBool("fullscreen", "Video")) - mEngineSettings.setBool("fullscreen", "Video", cFullScreen); + 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 != mEngineSettings.getBool("window border", "Video")) - mEngineSettings.setBool("window border", "Video", cWindowBorder); + if (cWindowBorder != Settings::Manager::getBool("window border", "Video")) + Settings::Manager::setBool("window border", "Video", cWindowBorder); int cAAValue = antiAliasingComboBox->currentText().toInt(); - if (cAAValue != mEngineSettings.getInt("antialiasing", "Video")) - mEngineSettings.setInt("antialiasing", "Video", cAAValue); + if (cAAValue != Settings::Manager::getInt("antialiasing", "Video")) + Settings::Manager::setInt("antialiasing", "Video", cAAValue); int cWidth = 0; int cHeight = 0; @@ -197,33 +208,40 @@ void Launcher::GraphicsPage::saveSettings() cHeight = customHeightSpinBox->value(); } - if (cWidth != mEngineSettings.getInt("resolution x", "Video")) - mEngineSettings.setInt("resolution x", "Video", cWidth); + if (cWidth != Settings::Manager::getInt("resolution x", "Video")) + Settings::Manager::setInt("resolution x", "Video", cWidth); - if (cHeight != mEngineSettings.getInt("resolution y", "Video")) - mEngineSettings.setInt("resolution y", "Video", cHeight); + if (cHeight != Settings::Manager::getInt("resolution y", "Video")) + Settings::Manager::setInt("resolution y", "Video", cHeight); int cScreen = screenComboBox->currentIndex(); - if (cScreen != mEngineSettings.getInt("screen", "Video")) - mEngineSettings.setInt("screen", "Video", cScreen); + if (cScreen != Settings::Manager::getInt("screen", "Video")) + Settings::Manager::setInt("screen", "Video", cScreen); - if (framerateLimitCheckBox->checkState()) + if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { float cFpsLimit = framerateLimitSpinBox->value(); - if (cFpsLimit != mEngineSettings.getFloat("framerate limit", "Video")) - mEngineSettings.setFloat("framerate limit", "Video", cFpsLimit); + if (cFpsLimit != Settings::Manager::getFloat("framerate limit", "Video")) + Settings::Manager::setFloat("framerate limit", "Video", cFpsLimit); } - else if (mEngineSettings.getFloat("framerate limit", "Video") != 0) + else if (Settings::Manager::getFloat("framerate limit", "Video") != 0) { - mEngineSettings.setFloat("framerate limit", "Video", 0); + Settings::Manager::setFloat("framerate limit", "Video", 0); } - int cShadowDist = shadowDistanceCheckBox->checkState() ? shadowDistanceSpinBox->value() : 0; - if (mEngineSettings.getInt("maximum shadow map distance", "Shadows") != cShadowDist) - mEngineSettings.setInt("maximum shadow map distance", "Shadows", cShadowDist); + // 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 && mEngineSettings.getFloat("shadow fade start", "Shadows") != cFadeStart) - mEngineSettings.setFloat("shadow fade start", "Shadows", cFadeStart); + 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(); @@ -231,42 +249,42 @@ void Launcher::GraphicsPage::saveSettings() bool cPlayerShadows = playerShadowsCheckBox->checkState(); if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) { - if (!mEngineSettings.getBool("enable shadows", "Shadows")) - mEngineSettings.setBool("enable shadows", "Shadows", true); - if (mEngineSettings.getBool("actor shadows", "Shadows") != cActorShadows) - mEngineSettings.setBool("actor shadows", "Shadows", cActorShadows); - if (mEngineSettings.getBool("player shadows", "Shadows") != cPlayerShadows) - mEngineSettings.setBool("player shadows", "Shadows", cPlayerShadows); - if (mEngineSettings.getBool("object shadows", "Shadows") != cObjectShadows) - mEngineSettings.setBool("object shadows", "Shadows", cObjectShadows); - if (mEngineSettings.getBool("terrain shadows", "Shadows") != cTerrainShadows) - mEngineSettings.setBool("terrain shadows", "Shadows", cTerrainShadows); + 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 (mEngineSettings.getBool("enable shadows", "Shadows")) - mEngineSettings.setBool("enable shadows", "Shadows", false); - if (mEngineSettings.getBool("actor shadows", "Shadows")) - mEngineSettings.setBool("actor shadows", "Shadows", false); - if (mEngineSettings.getBool("player shadows", "Shadows")) - mEngineSettings.setBool("player shadows", "Shadows", false); - if (mEngineSettings.getBool("object shadows", "Shadows")) - mEngineSettings.setBool("object shadows", "Shadows", false); - if (mEngineSettings.getBool("terrain shadows", "Shadows")) - mEngineSettings.setBool("terrain shadows", "Shadows", false); + 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 (mEngineSettings.getBool("enable indoor shadows", "Shadows") != cIndoorShadows) - mEngineSettings.setBool("enable indoor shadows", "Shadows", cIndoorShadows); + if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows) + Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows); int cShadowRes = shadowResolutionComboBox->currentText().toInt(); - if (cShadowRes != mEngineSettings.getInt("shadow map resolution", "Shadows")) - mEngineSettings.setInt("shadow map resolution", "Shadows", cShadowRes); + if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows")) + Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes); auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString(); - if (cComputeSceneBounds != mEngineSettings.getString("compute scene bounds", "Shadows")) - mEngineSettings.setString("compute scene bounds", "Shadows", cComputeSceneBounds); + if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows")) + Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) @@ -299,9 +317,9 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) return result; } - QString aspect = getAspect(mode.w, mode.h); 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 + ")"); @@ -339,9 +357,9 @@ void Launcher::GraphicsPage::screenChanged(int screen) } } -void Launcher::GraphicsPage::slotFullScreenChanged(int state) +void Launcher::GraphicsPage::slotFullScreenChanged(int mode) { - if (state == Qt::Checked) { + if (mode == static_cast(Settings::WindowMode::Fullscreen) || mode == static_cast(Settings::WindowMode::WindowedFullscreen)) { standardRadioButton->toggle(); customRadioButton->setEnabled(false); customWidthSpinBox->setEnabled(false); diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index 3b03a72bdd..a6754ccb04 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -1,8 +1,6 @@ #ifndef GRAPHICSPAGE_H #define GRAPHICSPAGE_H -#include - #include "ui_graphicspage.h" #include @@ -20,7 +18,7 @@ namespace Launcher Q_OBJECT public: - GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = 0); + explicit GraphicsPage(QWidget *parent = nullptr); void saveSettings(); bool loadSettings(); @@ -35,9 +33,6 @@ namespace Launcher void slotShadowDistLimitToggled(bool checked); private: - Files::ConfigurationManager &mCfgMgr; - Settings::Manager &mEngineSettings; - QVector mResolutionsPerScreen; static QStringList getAvailableResolutions(int screen); diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index f15abafce0..486f771366 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,10 +1,10 @@ #include -#include #include -#include #include -#include + +#include +#include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED #undef MAC_OS_X_VERSION_MIN_REQUIRED @@ -14,8 +14,10 @@ #include "maindialog.hpp" -int main(int argc, char *argv[]) +int runLauncher(int argc, char *argv[]) { + Platform::init(); + try { QApplication app(argc, argv); @@ -51,3 +53,8 @@ int main(int argc, char *argv[]) return 0; } } + +int main(int argc, char *argv[]) +{ + return wrapApplication(runLauncher, argc, argv, "Launcher"); +} diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index df2ba68910..69259f8b27 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -3,16 +3,15 @@ #include #include -#include +#include +#include +#include #include -#include -#include -#include -#include #include #include -#include +#include +#include #include "playpage.hpp" #include "graphicspage.hpp" @@ -55,8 +54,8 @@ Launcher::MainDialog::MainDialog(QWidget *parent) iconWidget->setCurrentRow(0); iconWidget->setFlow(QListView::LeftToRight); - QPushButton *helpButton = new QPushButton(tr("Help")); - QPushButton *playButton = new QPushButton(tr("Play")); + auto *helpButton = new QPushButton(tr("Help")); + auto *playButton = new QPushButton(tr("Play")); buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); buttonBox->addButton(helpButton, QDialogButtonBox::HelpRole); buttonBox->addButton(playButton, QDialogButtonBox::AcceptRole); @@ -82,31 +81,31 @@ void Launcher::MainDialog::createIcons() if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); - QListWidgetItem *playButton = new QListWidgetItem(iconWidget); + auto *playButton = new QListWidgetItem(iconWidget); playButton->setIcon(QIcon(":/images/openmw.png")); playButton->setText(tr("Play")); playButton->setTextAlignment(Qt::AlignCenter); playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); + auto *dataFilesButton = new QListWidgetItem(iconWidget); dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); dataFilesButton->setText(tr("Data Files")); dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); + auto *graphicsButton = new QListWidgetItem(iconWidget); graphicsButton->setIcon(QIcon(":/images/preferences-video.png")); graphicsButton->setText(tr("Graphics")); graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); + auto *settingsButton = new QListWidgetItem(iconWidget); settingsButton->setIcon(QIcon(":/images/preferences.png")); settingsButton->setText(tr("Settings")); settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *advancedButton = new QListWidgetItem(iconWidget); + auto *advancedButton = new QListWidgetItem(iconWidget); advancedButton->setIcon(QIcon(":/images/preferences-advanced.png")); advancedButton->setText(tr("Advanced")); advancedButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); @@ -126,9 +125,9 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this); + mGraphicsPage = new GraphicsPage(this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this); + mAdvancedPage = new AdvancedPage(mGameSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -150,7 +149,6 @@ void Launcher::MainDialog::createPages() connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); - } Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() @@ -158,6 +156,20 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() if (!setupLauncherSettings()) return FirstRunDialogResultFailure; + // Dialog wizard and setup will fail if the config directory does not already exist + QDir userConfigDir = QDir(QString::fromStdString(mCfgMgr.getUserConfigPath().string())); + if ( ! userConfigDir.exists() ) { + if ( ! userConfigDir.mkpath(".") ) + { + 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(userConfigDir.canonicalPath()) + ); + return FirstRunDialogResultFailure; + } + } + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) { QMessageBox msgBox; @@ -322,54 +334,46 @@ bool Launcher::MainDialog::setupGameSettings() QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QString globalPath = QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str()); - // Load the user config file first, separately - // So we can write it properly, uncontaminated - QString path = userPath + QLatin1String("openmw.cfg"); - QFile file(path); + QFile file; - qDebug() << "Loading config file:" << path.toUtf8().constData(); - - if (file.exists()) { - 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.
").arg(file.fileName())); - return false; - } - QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); - - mGameSettings.readUserFile(stream); - file.close(); - } - - // Now the rest - priority: user > local > global - QStringList paths; - paths.append(globalPath + QString("openmw.cfg")); - paths.append(localPath + QString("openmw.cfg")); - paths.append(userPath + QString("openmw.cfg")); - - for (const QString &path2 : paths) + auto loadFile = [&] (const QString& path, bool(Config::GameSettings::*reader)(QTextStream&, bool), bool ignoreContent = false) -> std::optional { - qDebug() << "Loading config file:" << path2.toUtf8().constData(); - - file.setFileName(path2); + qDebug() << "Loading config file:" << path.toUtf8().constData(); + file.setFileName(path); if (file.exists()) { 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.
").arg(file.fileName())); - return false; + 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); stream.setCodec(QTextCodec::codecForName("UTF-8")); - mGameSettings.readFile(stream); + (mGameSettings.*reader)(stream, ignoreContent); file.close(); + return true; } + return false; + }; + + // Load the user config file first, separately + // So we can write it properly, uncontaminated + if(!loadFile(userPath + QLatin1String("openmw.cfg"), &Config::GameSettings::readUserFile)) + return false; + + // Now the rest - priority: user > local > global + if(auto result = loadFile(localPath + QString("openmw.cfg"), &Config::GameSettings::readFile, true)) + { + // Load global if local wasn't found + if(!*result && !loadFile(globalPath + QString("openmw.cfg"), &Config::GameSettings::readFile, true)) + return false; } + else + return false; + if(!loadFile(userPath + QString("openmw.cfg"), &Config::GameSettings::readFile)) + return false; return true; } @@ -419,57 +423,23 @@ bool Launcher::MainDialog::setupGameData() bool Launcher::MainDialog::setupGraphicsSettings() { - // This method is almost a copy of OMW::Engine::loadSettings(). They should definitely - // remain consistent, and possibly be merged into a shared component. At the very least - // the filenames should be in the CfgMgr component. - - // Ensure to clear previous settings in case we had already loaded settings. - mEngineSettings.clear(); - - // Create the settings manager and load default settings file - const std::string localDefault = (mCfgMgr.getLocalPath() / "settings-default.cfg").string(); - const std::string globalDefault = (mCfgMgr.getGlobalPath() / "settings-default.cfg").string(); - std::string defaultPath; - - // Prefer the settings-default.cfg in the current directory. - if (boost::filesystem::exists(localDefault)) - defaultPath = localDefault; - else if (boost::filesystem::exists(globalDefault)) - defaultPath = globalDefault; - // Something's very wrong if we can't find the file at all. - else { - cfgError(tr("Error reading OpenMW configuration file"), - tr("
Could not find settings-default.cfg

\ - The problem may be due to an incomplete installation of OpenMW.
\ - Reinstalling OpenMW may resolve the problem.")); - return false; - } - - // Load the default settings, report any parsing errors. - try { - mEngineSettings.loadDefault(defaultPath); - } - catch (std::exception& e) { - std::string msg = std::string("
Error reading settings-default.cfg

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); - return false; - } - - // Load user settings if they exist - const std::string userPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - // User settings are not required to exist, so if they don't we're done. - if (!boost::filesystem::exists(userPath)) return true; - - try { - mEngineSettings.loadUser(userPath); + Settings::Manager::clear(); // Ensure to clear previous settings in case we had already loaded settings. + try + { + boost::program_options::variables_map variables; + boost::program_options::options_description desc; + mCfgMgr.addCommonOptions(desc); + mCfgMgr.readConfiguration(variables, desc, true); + Settings::Manager::load(mCfgMgr); + return true; } - catch (std::exception& e) { - std::string msg = std::string("
Error reading settings.cfg

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); + 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.
") + e.what()); return false; } - - return true; } void Launcher::MainDialog::loadSettings() @@ -543,7 +513,7 @@ bool Launcher::MainDialog::writeSettings() // Graphics settings const std::string settingsPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); try { - mEngineSettings.saveUser(settingsPath); + Settings::Manager::saveUser(settingsPath); } catch (std::exception& e) { std::string msg = "
Error writing settings.cfg

" + @@ -609,7 +579,7 @@ void Launcher::MainDialog::play() msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("
You do not have a game file selected.

\ OpenMW will not start without a game file selected.
")); - msgBox.exec(); + msgBox.exec(); return; } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 214d31dd4a..ca198cef54 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -1,19 +1,15 @@ #ifndef MAINDIALOG_H #define MAINDIALOG_H -#include -#include #ifndef Q_MOC_RUN #include - #include #include #include -#include #endif #include "ui_mainwindow.h" @@ -48,8 +44,8 @@ namespace Launcher Q_OBJECT public: - explicit MainDialog(QWidget *parent = 0); - ~MainDialog(); + explicit MainDialog(QWidget *parent = nullptr); + ~MainDialog() override; FirstRunDialogResult showFirstRunDialog(); @@ -98,7 +94,6 @@ namespace Launcher Files::ConfigurationManager mCfgMgr; Config::GameSettings mGameSettings; - Settings::Manager mEngineSettings; Config::LauncherSettings mLauncherSettings; }; diff --git a/apps/launcher/playpage.hpp b/apps/launcher/playpage.hpp index 1dc5bb0fe0..8f414dc6aa 100644 --- a/apps/launcher/playpage.hpp +++ b/apps/launcher/playpage.hpp @@ -1,8 +1,6 @@ #ifndef PLAYPAGE_H #define PLAYPAGE_H -#include - #include "ui_playpage.h" class QComboBox; @@ -16,7 +14,7 @@ namespace Launcher Q_OBJECT public: - PlayPage(QWidget *parent = 0); + PlayPage(QWidget *parent = nullptr); void setProfilesModel(QAbstractItemModel *model); signals: diff --git a/apps/launcher/sdlinit.cpp b/apps/launcher/sdlinit.cpp index 1fe1fd4c2f..4717fb2818 100644 --- a/apps/launcher/sdlinit.cpp +++ b/apps/launcher/sdlinit.cpp @@ -1,7 +1,6 @@ #include #include -#include bool initSDL() { diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 59d7cfd25b..1739c5cc05 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -5,11 +5,6 @@ #include #include -#include - -#include -#include - #include "utils/textinputdialog.hpp" #include "datafilespage.hpp" diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index ccc2061dd7..fa31eb4033 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -1,9 +1,6 @@ #ifndef SETTINGSPAGE_HPP #define SETTINGSPAGE_HPP -#include -#include - #include #include "ui_settingspage.h" @@ -24,8 +21,8 @@ namespace Launcher public: SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); - ~SettingsPage(); + Config::LauncherSettings &launcherSettings, MainDialog *parent = nullptr); + ~SettingsPage() override; void saveSettings(); bool loadSettings(); diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index 6d1ed2f494..4cb8b545aa 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -1,6 +1,6 @@ #include "cellnameloader.hpp" -#include +#include #include QSet CellNameLoader::getCellNames(QStringList &contentPaths) @@ -10,6 +10,8 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) // Loop through all content files for (auto &contentPath : contentPaths) { + if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) + continue; esmReader.open(contentPath.toStdString()); // Loop through all records @@ -35,7 +37,7 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) bool CellNameLoader::isCellRecord(ESM::NAME &recordName) { - return recordName.intval == ESM::REC_CELL; + return recordName.toInt() == ESM::REC_CELL; } QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) @@ -45,4 +47,5 @@ QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) cell.loadNameAndData(esmReader, isDeleted); return QString::fromStdString(cell.mName); -} \ No newline at end of file +} + diff --git a/apps/launcher/utils/cellnameloader.hpp b/apps/launcher/utils/cellnameloader.hpp index c58d09226a..6143b78bd9 100644 --- a/apps/launcher/utils/cellnameloader.hpp +++ b/apps/launcher/utils/cellnameloader.hpp @@ -1,11 +1,10 @@ #ifndef OPENMW_CELLNAMELOADER_H #define OPENMW_CELLNAMELOADER_H -#include #include #include -#include +#include namespace ESM {class ESMReader; struct Cell;} namespace ContentSelectorView {class ContentSelector;} diff --git a/apps/launcher/utils/lineedit.hpp b/apps/launcher/utils/lineedit.hpp index 50da730459..89de39588a 100644 --- a/apps/launcher/utils/lineedit.hpp +++ b/apps/launcher/utils/lineedit.hpp @@ -11,7 +11,6 @@ #define LINEEDIT_H #include -#include #include #include @@ -24,7 +23,7 @@ class LineEdit : public QLineEdit QString mPlaceholderText; public: - LineEdit(QWidget *parent = 0); + LineEdit(QWidget *parent = nullptr); protected: void resizeEvent(QResizeEvent *) override; diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp new file mode 100644 index 0000000000..469872d158 --- /dev/null +++ b/apps/launcher/utils/openalutil.cpp @@ -0,0 +1,60 @@ +#include +#include + +#include + +#include "openalutil.hpp" + +#ifndef ALC_ALL_DEVICES_SPECIFIER +#define ALC_ALL_DEVICES_SPECIFIER 0x1013 +#endif + +std::vector Launcher::enumerateOpenALDevices() +{ + std::vector devlist; + const ALCchar *devnames; + + if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) + { + devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + } + else + { + devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + } + + while(devnames && *devnames) + { + devlist.emplace_back(devnames); + devnames += strlen(devnames)+1; + } + return devlist; +} + +std::vector Launcher::enumerateOpenALDevicesHrtf() +{ + std::vector ret; + + ALCdevice *device = alcOpenDevice(nullptr); + if(device) + { + if(alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) + { + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; + void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); + memcpy(&alcGetStringiSOFT, &funcPtr, sizeof(funcPtr)); + ALCint num_hrtf; + alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); + ret.reserve(num_hrtf); + for(ALCint i = 0;i < num_hrtf;++i) + { + const ALCchar *entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); + if(strcmp(entry, "") == 0) + break; + ret.emplace_back(entry); + } + } + alcCloseDevice(device); + } + return ret; +} diff --git a/apps/launcher/utils/openalutil.hpp b/apps/launcher/utils/openalutil.hpp new file mode 100644 index 0000000000..b084dce7ce --- /dev/null +++ b/apps/launcher/utils/openalutil.hpp @@ -0,0 +1,8 @@ +#include +#include + +namespace Launcher +{ + std::vector enumerateOpenALDevices(); + std::vector enumerateOpenALDevicesHrtf(); +} diff --git a/apps/launcher/utils/profilescombobox.cpp b/apps/launcher/utils/profilescombobox.cpp index 7df89098e2..dc0a806c9f 100644 --- a/apps/launcher/utils/profilescombobox.cpp +++ b/apps/launcher/utils/profilescombobox.cpp @@ -1,8 +1,5 @@ -#include -#include #include #include -#include #include "profilescombobox.hpp" @@ -30,10 +27,10 @@ void ProfilesComboBox::setEditEnabled(bool editable) setEditable(true); setValidator(mValidator); - ComboBoxLineEdit *edit = new ComboBoxLineEdit(this); + auto *edit = new ComboBoxLineEdit(this); setLineEdit(edit); - setCompleter(0); + setCompleter(nullptr); connect(lineEdit(), SIGNAL(editingFinished()), this, SLOT(slotEditingFinished())); diff --git a/apps/launcher/utils/profilescombobox.hpp b/apps/launcher/utils/profilescombobox.hpp index 7b83c41b2f..f29ac58e12 100644 --- a/apps/launcher/utils/profilescombobox.hpp +++ b/apps/launcher/utils/profilescombobox.hpp @@ -4,7 +4,7 @@ #include "components/contentselector/view/combobox.hpp" #include "lineedit.hpp" -#include +#include class QString; @@ -16,12 +16,12 @@ public: class ComboBoxLineEdit : public LineEdit { public: - explicit ComboBoxLineEdit (QWidget *parent = 0); + explicit ComboBoxLineEdit (QWidget *parent = nullptr); }; public: - explicit ProfilesComboBox(QWidget *parent = 0); + explicit ProfilesComboBox(QWidget *parent = nullptr); void setEditEnabled(bool editable); void setCurrentProfile(int index) { diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 385d086fd0..5bbcf4c198 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -16,16 +16,16 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox->addButton(QDialogButtonBox::Cancel); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - QLabel *label = new QLabel(this); + auto *label = new QLabel(this); label->setText(text); // Line edit QValidator *validator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); - mLineEdit->setCompleter(0); + mLineEdit->setCompleter(nullptr); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); + auto *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); dialogLayout->addWidget(mButtonBox); diff --git a/apps/launcher/utils/textinputdialog.hpp b/apps/launcher/utils/textinputdialog.hpp index a9353956a0..ad64cb3e2a 100644 --- a/apps/launcher/utils/textinputdialog.hpp +++ b/apps/launcher/utils/textinputdialog.hpp @@ -15,8 +15,8 @@ namespace Launcher public: - explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = 0); - ~TextInputDialog (); + explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = nullptr); + ~TextInputDialog () override; inline LineEdit *lineEdit() { return mLineEdit; } void setOkButtonEnabled(bool enabled); diff --git a/apps/mwiniimporter/CMakeLists.txt b/apps/mwiniimporter/CMakeLists.txt index e83656708b..d35f4f4d33 100644 --- a/apps/mwiniimporter/CMakeLists.txt +++ b/apps/mwiniimporter/CMakeLists.txt @@ -33,3 +33,12 @@ if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(openmw-iniimporter gcov) endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-iniimporter PRIVATE + + + + + ) +endif() diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 23aea2deb4..f7523b11cf 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -1,13 +1,12 @@ #include "importer.hpp" #include -#include #include -#include +#include #include #include -#include + namespace bfs = boost::filesystem; @@ -664,49 +663,51 @@ MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::p std::string line; while (std::getline(file, line)) { - line = encoder.getUtf8(line); + std::string_view utf8 = encoder.getUtf8(line); // unify Unix-style and Windows file ending - if (!(line.empty()) && (line[line.length()-1]) == '\r') { - line = line.substr(0, line.length()-1); + if (!(utf8.empty()) && (utf8[utf8.length()-1]) == '\r') { + utf8 = utf8.substr(0, utf8.length()-1); } - if(line.empty()) { + if(utf8.empty()) { continue; } - if(line[0] == '[') { - int pos = line.find(']'); + if(utf8[0] == '[') { + int pos = static_cast(utf8.find(']')); if(pos < 2) { - std::cout << "Warning: ini file wrongly formatted (" << line << "). Line ignored." << std::endl; + std::cout << "Warning: ini file wrongly formatted (" << utf8 << "). Line ignored." << std::endl; continue; } - section = line.substr(1, line.find(']')-1); + section = utf8.substr(1, utf8.find(']')-1); continue; } - int comment_pos = line.find(";"); + int comment_pos = static_cast(utf8.find(';')); if(comment_pos > 0) { - line = line.substr(0,comment_pos); + utf8 = utf8.substr(0,comment_pos); } - int pos = line.find("="); + int pos = static_cast(utf8.find('=')); if(pos < 1) { continue; } - std::string key(section + ":" + line.substr(0,pos)); - std::string value(line.substr(pos+1)); + std::string key(section + ":" + std::string(utf8.substr(0, pos))); + const std::string_view value(utf8.substr(pos+1)); if(value.empty()) { std::cout << "Warning: ignored empty value for key '" << key << "'." << std::endl; continue; } - if(map.find(key) == map.end()) { - map.insert( std::make_pair (key, std::vector() ) ); - } - map[key].push_back(value); + auto it = map.find(key); + + if (it == map.end()) + it = map.emplace_hint(it, std::move(key), std::vector()); + + it->second.push_back(std::string(value)); } return map; @@ -722,7 +723,7 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::p while (std::getline(file, line)) { // we cant say comment by only looking at first char anymore - int comment_pos = line.find("#"); + int comment_pos = static_cast(line.find('#')); if(comment_pos > 0) { line = line.substr(0,comment_pos); } @@ -731,7 +732,7 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::p continue; } - int pos = line.find("="); + int pos = static_cast(line.find('=')); if(pos < 1) { continue; } @@ -778,7 +779,7 @@ void MwIniImporter::mergeFallback(multistrmap &cfg, const multistrmap &ini) cons } void MwIniImporter::insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value) { - const multistrmap::const_iterator it = cfg.find(key); + const auto it = cfg.find(key); if(it == cfg.end()) { cfg.insert(std::make_pair (key, std::vector() )); } @@ -791,7 +792,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con std::string archive; // Search archives listed in ini file - multistrmap::const_iterator it = ini.begin(); + auto it = ini.begin(); for(int i=0; it != ini.end(); i++) { archive = baseArchive; archive.append(std::to_string(i)); @@ -813,7 +814,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con // does not appears in the ini file cfg["fallback-archive"].push_back("Morrowind.bsa"); - for(std::vector::const_iterator iter=archives.begin(); iter!=archives.end(); ++iter) { + for(auto iter=archives.begin(); iter!=archives.end(); ++iter) { cfg["fallback-archive"].push_back(*iter); } } @@ -886,7 +887,7 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); - multistrmap::const_iterator it = ini.begin(); + auto it = ini.begin(); for (int i=0; it != ini.end(); i++) { std::string gameFile = baseGameFile; @@ -969,7 +970,7 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) { for(multistrmap::const_iterator it=cfg.begin(); it != cfg.end(); ++it) { - for(std::vector::const_iterator entry=it->second.begin(); entry != it->second.end(); ++entry) { + for(auto entry=it->second.begin(); entry != it->second.end(); ++entry) { out << (it->first) << "=" << (*entry) << std::endl; } } diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt new file mode 100644 index 0000000000..9f57d52a7e --- /dev/null +++ b/apps/navmeshtool/CMakeLists.txt @@ -0,0 +1,31 @@ +set(NAVMESHTOOL + worldspacedata.cpp + navmesh.cpp + main.cpp +) +source_group(apps\\navmeshtool FILES ${NAVMESHTOOL}) + +openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) + +target_link_libraries(openmw-navmeshtool + ${Boost_PROGRAM_OPTIONS_LIBRARY} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions(--coverage) + target_link_libraries(openmw-navmeshtool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-navmeshtool RUNTIME DESTINATION ".") +endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-navmeshtool PRIVATE + + + + + ) +endif() diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp new file mode 100644 index 0000000000..ea0046b1ae --- /dev/null +++ b/apps/navmeshtool/main.cpp @@ -0,0 +1,234 @@ +#include "worldspacedata.hpp" +#include "navmesh.hpp" + +#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 + +#ifdef WIN32 +#include +#include +#endif + + +namespace NavMeshTool +{ + namespace + { + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + + result.add_options() + ("help", "print help message") + + ("version", "print version information and quit") + + ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), + "set resources directory") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + + ("fallback", bpo::value()->default_value(Fallback::FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + + ("threads", bpo::value()->default_value(std::max(std::thread::hardware_concurrency() - 1, 1)), + "number of threads for parallel processing") + + ("process-interior-cells", bpo::value()->implicit_value(true) + ->default_value(false), "build navmesh for interior cells") + + ("remove-unused-tiles", bpo::value()->implicit_value(true) + ->default_value(false), "remove tiles from cache that will not be used with current content profile") + + ("write-binary-log", bpo::value()->implicit_value(true) + ->default_value(false), "write progress in binary messages to be consumed by the launcher") + ; + Files::ConfigurationManager::addCommonOptions(result); + + return result; + } + + int runNavMeshTool(int argc, char *argv[]) + { + Platform::init(); + + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv) + .options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + + bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); + config.readConfiguration(variables, desc); + Files::mergeComposingVariables(variables, composingVariables, desc); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.filterOutNonExistingPaths(dataDirs); + + const auto fsStrict = variables["fs-strict"].as(); + const auto resDir = variables["resources"].as(); + Version::Version v = Version::getOpenmwVersion(resDir.string()); + Log(Debug::Info) << v.describe(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const auto fileCollections = Files::Collections(dataDirs, !fsStrict); + const auto archives = variables["fallback-archive"].as(); + const auto contentFiles = variables["content"].as(); + const std::size_t threadsNumber = variables["threads"].as(); + + if (threadsNumber < 1) + { + std::cerr << "Invalid threads number: " << threadsNumber << ", expected >= 1"; + return -1; + } + + const bool processInteriorCells = variables["process-interior-cells"].as(); + const bool removeUnusedTiles = variables["remove-unused-tiles"].as(); + const bool writeBinaryLog = variables["write-binary-log"].as(); + +#ifdef WIN32 + if (writeBinaryLog) + _setmode(_fileno(stderr), _O_BINARY); +#endif + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs(fsStrict); + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager settings; + settings.load(config); + + const DetourNavigator::CollisionShapeType agentCollisionShape = DetourNavigator::defaultCollisionShapeType; + const osg::Vec3f agentHalfExtents = Settings::Manager::getVector3("default actor pathfind half extents", "Game"); + const DetourNavigator::AgentBounds agentBounds {agentCollisionShape, agentHalfExtents}; + const std::uint64_t maxDbFileSize = static_cast(Settings::Manager::getInt64("max navmeshdb file size", "Navigator")); + const std::string dbPath = (config.getUserDataPath() / "navmesh.db").string(); + + DetourNavigator::NavMeshDb db(dbPath, maxDbFileSize); + + ESM::ReadersCache readers; + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + 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); + DetourNavigator::RecastGlobalAllocator::init(); + DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + navigatorSettings.mRecast.mSwimHeightScale = EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat(); + + WorldspaceData cellsData = gatherWorldspaceData(navigatorSettings, readers, vfs, bulletShapeManager, + esmData, processInteriorCells, writeBinaryLog); + + const Status status = generateAllNavMeshTiles(agentBounds, navigatorSettings, threadsNumber, + removeUnusedTiles, writeBinaryLog, cellsData, std::move(db)); + + switch (status) + { + case Status::Ok: + Log(Debug::Info) << "Done"; + break; + case Status::Cancelled: + Log(Debug::Warning) << "Cancelled"; + break; + case Status::NotEnoughSpace: + Log(Debug::Warning) << "Navmesh generation is cancelled due to running out of disk space or limits " + << "for navmesh db. Check disk space at the db location \"" << dbPath + << "\". If there is enough space, adjust \"max navmeshdb file size\" setting (see " + << "https://openmw.readthedocs.io/en/latest/reference/modding/settings/navigator.html?highlight=navmesh#max-navmeshdb-file-size)."; + break; + } + + return 0; + } + } +} + +int main(int argc, char *argv[]) +{ + return wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, "NavMeshTool"); +} diff --git a/apps/navmeshtool/navmesh.cpp b/apps/navmeshtool/navmesh.cpp new file mode 100644 index 0000000000..053809eca4 --- /dev/null +++ b/apps/navmeshtool/navmesh.cpp @@ -0,0 +1,322 @@ +#include "navmesh.hpp" + +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::AgentBounds; + using DetourNavigator::GenerateNavMeshTile; + using DetourNavigator::NavMeshDb; + using DetourNavigator::NavMeshTileInfo; + using DetourNavigator::PreparedNavMeshData; + using DetourNavigator::RecastMeshProvider; + using DetourNavigator::MeshSource; + using DetourNavigator::Settings; + using DetourNavigator::ShapeId; + using DetourNavigator::TileId; + using DetourNavigator::TilePosition; + using DetourNavigator::TileVersion; + using DetourNavigator::TilesPositionsRange; + using Sqlite3::Transaction; + + void logGeneratedTiles(std::size_t provided, std::size_t expected) + { + Log(Debug::Info) << provided << "/" << expected << " (" + << (static_cast(provided) / static_cast(expected) * 100) + << "%) navmesh tiles are generated"; + } + + template + void serializeToStderr(const T& value) + { + const std::vector data = serialize(value); + getLockedRawStderr()->write(reinterpret_cast(data.data()), static_cast(data.size())); + } + + void logGeneratedTilesMessage(std::size_t number) + { + serializeToStderr(GeneratedTiles {static_cast(number)}); + } + + struct LogGeneratedTiles + { + void operator()(std::size_t provided, std::size_t expected) const + { + logGeneratedTiles(provided, expected); + } + }; + + class NavMeshTileConsumer final : public DetourNavigator::NavMeshTileConsumer + { + public: + std::atomic_size_t mExpected {0}; + + explicit NavMeshTileConsumer(NavMeshDb&& db, bool removeUnusedTiles, bool writeBinaryLog) + : mDb(std::move(db)) + , mRemoveUnusedTiles(removeUnusedTiles) + , mWriteBinaryLog(writeBinaryLog) + , mTransaction(mDb.startTransaction(Sqlite3::TransactionMode::Immediate)) + , mNextTileId(mDb.getMaxTileId() + 1) + , mNextShapeId(mDb.getMaxShapeId() + 1) + {} + + std::size_t getProvided() const { return mProvided.load(); } + + std::size_t getInserted() const { return mInserted.load(); } + + std::size_t getUpdated() const { return mUpdated.load(); } + + std::size_t getDeleted() const + { + const std::lock_guard lock(mMutex); + return mDeleted; + } + + std::int64_t resolveMeshSource(const MeshSource& source) override + { + const std::lock_guard lock(mMutex); + return DetourNavigator::resolveMeshSource(mDb, source, mNextShapeId); + } + + std::optional find(std::string_view worldspace, const TilePosition &tilePosition, + const std::vector &input) override + { + std::optional result; + std::lock_guard lock(mMutex); + if (const auto tile = mDb.findTile(worldspace, tilePosition, input)) + { + NavMeshTileInfo info; + info.mTileId = tile->mTileId; + info.mVersion = tile->mVersion; + result.emplace(info); + } + return result; + } + + void ignore(std::string_view worldspace, const TilePosition& tilePosition) override + { + if (mRemoveUnusedTiles) + { + std::lock_guard lock(mMutex); + mDeleted += static_cast(mDb.deleteTilesAt(worldspace, tilePosition)); + } + report(); + } + + void identity(std::string_view worldspace, const TilePosition& tilePosition, std::int64_t tileId) override + { + if (mRemoveUnusedTiles) + { + std::lock_guard lock(mMutex); + mDeleted += static_cast(mDb.deleteTilesAtExcept(worldspace, tilePosition, TileId {tileId})); + } + report(); + } + + void insert(std::string_view worldspace, const TilePosition& tilePosition, + std::int64_t version, const std::vector& input, PreparedNavMeshData& data) override + { + { + std::lock_guard lock(mMutex); + if (mRemoveUnusedTiles) + mDeleted += static_cast(mDb.deleteTilesAt(worldspace, tilePosition)); + data.mUserId = static_cast(mNextTileId); + mDb.insertTile(mNextTileId, worldspace, tilePosition, TileVersion {version}, input, serialize(data)); + ++mNextTileId; + } + ++mInserted; + report(); + } + + void update(std::string_view worldspace, const TilePosition& tilePosition, + std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) override + { + data.mUserId = static_cast(tileId); + { + std::lock_guard lock(mMutex); + if (mRemoveUnusedTiles) + mDeleted += static_cast(mDb.deleteTilesAtExcept(worldspace, tilePosition, TileId {tileId})); + mDb.updateTile(TileId {tileId}, TileVersion {version}, serialize(data)); + } + ++mUpdated; + report(); + } + + void cancel(std::string_view reason) override + { + std::unique_lock lock(mMutex); + if (reason.find("database or disk is full") != std::string_view::npos) + mStatus = Status::NotEnoughSpace; + else + mStatus = Status::Cancelled; + mHasTile.notify_one(); + } + + Status wait() + { + constexpr std::chrono::seconds transactionInterval(1); + std::unique_lock lock(mMutex); + auto start = std::chrono::steady_clock::now(); + while (mProvided < mExpected && mStatus == Status::Ok) + { + mHasTile.wait(lock); + const auto now = std::chrono::steady_clock::now(); + if (now - start > transactionInterval) + { + mTransaction.commit(); + mTransaction = mDb.startTransaction(Sqlite3::TransactionMode::Immediate); + start = now; + } + } + logGeneratedTiles(mProvided, mExpected); + if (mWriteBinaryLog) + logGeneratedTilesMessage(mProvided); + return mStatus; + } + + void commit() + { + const std::lock_guard lock(mMutex); + mTransaction.commit(); + } + + void vacuum() + { + const std::lock_guard lock(mMutex); + mDb.vacuum(); + } + + void removeTilesOutsideRange(std::string_view worldspace, const TilesPositionsRange& range) + { + const std::lock_guard lock(mMutex); + mTransaction.commit(); + Log(Debug::Info) << "Removing tiles outside processed range for worldspace \"" << worldspace << "\"..."; + mDeleted += static_cast(mDb.deleteTilesOutsideRange(worldspace, range)); + mTransaction = mDb.startTransaction(Sqlite3::TransactionMode::Immediate); + } + + private: + std::atomic_size_t mProvided {0}; + std::atomic_size_t mInserted {0}; + std::atomic_size_t mUpdated {0}; + std::size_t mDeleted = 0; + Status mStatus = Status::Ok; + mutable std::mutex mMutex; + NavMeshDb mDb; + const bool mRemoveUnusedTiles; + const bool mWriteBinaryLog; + Transaction mTransaction; + TileId mNextTileId; + std::condition_variable mHasTile; + Misc::ProgressReporter mReporter; + ShapeId mNextShapeId; + std::mutex mReportMutex; + + void report() + { + const std::size_t provided = mProvided.fetch_add(1, std::memory_order_relaxed) + 1; + mReporter(provided, mExpected); + mHasTile.notify_one(); + if (mWriteBinaryLog) + logGeneratedTilesMessage(provided); + } + }; + } + + Status generateAllNavMeshTiles(const AgentBounds& agentBounds, const Settings& settings, + std::size_t threadsNumber, bool removeUnusedTiles, bool writeBinaryLog, WorldspaceData& data, + NavMeshDb&& db) + { + Log(Debug::Info) << "Generating navmesh tiles by " << threadsNumber << " parallel workers..."; + + SceneUtil::WorkQueue workQueue(threadsNumber); + auto navMeshTileConsumer = std::make_shared(std::move(db), removeUnusedTiles, writeBinaryLog); + std::size_t tiles = 0; + std::mt19937_64 random; + + for (const std::unique_ptr& input : data.mNavMeshInputs) + { + const auto range = DetourNavigator::makeTilesPositionsRange( + Misc::Convert::toOsgXY(input->mAabb.m_min), + Misc::Convert::toOsgXY(input->mAabb.m_max), + settings.mRecast + ); + + if (removeUnusedTiles) + navMeshTileConsumer->removeTilesOutsideRange(input->mWorldspace, range); + + std::vector worldspaceTiles; + + DetourNavigator::getTilesPositions(range, + [&] (const TilePosition& tilePosition) { worldspaceTiles.push_back(tilePosition); }); + + tiles += worldspaceTiles.size(); + + if (writeBinaryLog) + serializeToStderr(ExpectedTiles {static_cast(tiles)}); + + navMeshTileConsumer->mExpected = tiles; + + std::shuffle(worldspaceTiles.begin(), worldspaceTiles.end(), random); + + for (const TilePosition& tilePosition : worldspaceTiles) + workQueue.addWorkItem(new GenerateNavMeshTile( + input->mWorldspace, + tilePosition, + RecastMeshProvider(input->mTileCachedRecastMeshManager), + agentBounds, + settings, + navMeshTileConsumer + )); + } + + const Status status = navMeshTileConsumer->wait(); + if (status == Status::Ok) + navMeshTileConsumer->commit(); + + const auto inserted = navMeshTileConsumer->getInserted(); + const auto updated = navMeshTileConsumer->getUpdated(); + const auto deleted = navMeshTileConsumer->getDeleted(); + + Log(Debug::Info) << "Generated navmesh for " << navMeshTileConsumer->getProvided() << " tiles, " + << inserted << " are inserted, " + << updated << " updated and " + << deleted << " deleted"; + + if (inserted + updated + deleted > 0) + { + Log(Debug::Info) << "Vacuuming the database..."; + navMeshTileConsumer->vacuum(); + } + + return status; + } +} diff --git a/apps/navmeshtool/navmesh.hpp b/apps/navmeshtool/navmesh.hpp new file mode 100644 index 0000000000..f0199ea1c4 --- /dev/null +++ b/apps/navmeshtool/navmesh.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_NAVMESHTOOL_NAVMESH_H +#define OPENMW_NAVMESHTOOL_NAVMESH_H + +#include + +#include + +namespace DetourNavigator +{ + class NavMeshDb; + struct Settings; + struct AgentBounds; +} + +namespace NavMeshTool +{ + struct WorldspaceData; + + enum class Status + { + Ok, + Cancelled, + NotEnoughSpace, + }; + + Status generateAllNavMeshTiles(const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Settings& settings, + std::size_t threadsNumber, bool removeUnusedTiles, bool writeBinaryLog, WorldspaceData& cellsData, + DetourNavigator::NavMeshDb&& db); +} + +#endif diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp new file mode 100644 index 0000000000..e93b50842c --- /dev/null +++ b/apps/navmeshtool/worldspacedata.cpp @@ -0,0 +1,348 @@ +#include "worldspacedata.hpp" + +#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 + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::CollisionShape; + using DetourNavigator::HeightfieldPlane; + using DetourNavigator::HeightfieldShape; + using DetourNavigator::HeightfieldSurface; + using DetourNavigator::ObjectId; + using DetourNavigator::ObjectTransform; + + struct CellRef + { + ESM::RecNameInts mType; + ESM::RefNum mRefNum; + std::string mRefId; + float mScale; + ESM::Position mPos; + + CellRef(ESM::RecNameInts type, ESM::RefNum refNum, std::string&& refId, float scale, const ESM::Position& pos) + : mType(type), mRefNum(refNum), mRefId(std::move(refId)), mScale(scale), mPos(pos) {} + }; + + ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, std::string_view refId) + { + const auto it = std::lower_bound(esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(), + refId, EsmLoader::LessById {}); + if (it == esmData.mRefIdTypes.end() || it->mId != refId) + return {}; + return it->mType; + } + + std::vector loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, + ESM::ReadersCache& readers) + { + std::vector> cellRefs; + + for (std::size_t i = 0; i < cell.mContextList.size(); i++) + { + ESM::ReadersCache::BusyItem reader = readers.get(static_cast(cell.mContextList[i].index)); + cell.restore(*reader, static_cast(i)); + ESM::CellRef cellRef; + bool deleted = false; + while (ESM::Cell::getNextRef(*reader, cellRef, deleted)) + { + Misc::StringUtils::lowerCaseInPlace(cellRef.mRefID); + const ESM::RecNameInts type = getType(esmData, cellRef.mRefID); + if (type == ESM::RecNameInts {}) + continue; + cellRefs.emplace_back(deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID), + cellRef.mScale, cellRef.mPos); + } + } + + Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; + + const auto getKey = [] (const EsmLoader::Record& v) -> const ESM::RefNum& { return v.mValue.mRefNum; }; + std::vector result = prepareRecords(cellRefs, getKey); + + Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; + + return result; + } + + template + void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, ESM::ReadersCache& readers, + F&& f) + { + std::vector cellRefs = loadCellRefs(cell, esmData, readers); + + Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; + + for (CellRef& cellRef : cellRefs) + { + std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + if (model.empty()) + continue; + + if (cellRef.mType != ESM::REC_STAT) + model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs); + + osg::ref_ptr shape = [&] + { + try + { + return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model, &vfs)); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model << "\": " << e.what(); + return osg::ref_ptr(); + } + } (); + + if (shape == nullptr || shape->mCollisionShape == nullptr) + continue; + + osg::ref_ptr shapeInstance(new Resource::BulletShapeInstance(std::move(shape))); + + switch (cellRef.mType) + { + case ESM::REC_ACTI: + case ESM::REC_CONT: + case ESM::REC_DOOR: + case ESM::REC_STAT: + f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale)); + break; + default: + break; + } + } + } + + struct GetXY + { + osg::Vec2i operator()(const ESM::Land& value) const { return osg::Vec2i(value.mX, value.mY); } + }; + + struct LessByXY + { + bool operator ()(const ESM::Land& lhs, const ESM::Land& rhs) const + { + return GetXY {}(lhs) < GetXY {}(rhs); + } + + bool operator ()(const ESM::Land& lhs, const osg::Vec2i& rhs) const + { + return GetXY {}(lhs) < rhs; + } + + bool operator ()(const osg::Vec2i& lhs, const ESM::Land& rhs) const + { + return lhs < GetXY {}(rhs); + } + }; + + btAABB getAabb(const osg::Vec2i& cellPosition, btScalar minHeight, btScalar maxHeight) + { + btAABB aabb; + aabb.m_min = btVector3( + static_cast(cellPosition.x() * ESM::Land::REAL_SIZE), + static_cast(cellPosition.y() * ESM::Land::REAL_SIZE), + minHeight + ); + aabb.m_max = btVector3( + static_cast((cellPosition.x() + 1) * ESM::Land::REAL_SIZE), + static_cast((cellPosition.y() + 1) * ESM::Land::REAL_SIZE), + maxHeight + ); + return aabb; + } + + void mergeOrAssign(const btAABB& aabb, btAABB& target, bool& initialized) + { + if (initialized) + return target.merge(aabb); + + target.m_min = aabb.m_min; + target.m_max = aabb.m_max; + initialized = true; + } + + std::tuple makeHeightfieldShape(const std::optional& land, + const osg::Vec2i& cellPosition, std::vector>& heightfields, + std::vector>& landDatas) + { + if (!land.has_value() || osg::Vec2i(land->mX, land->mY) != cellPosition + || (land->mDataTypes & ESM::Land::DATA_VHGT) == 0) + return {HeightfieldPlane {ESM::Land::DEFAULT_HEIGHT}, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT}; + + ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique()); + land->loadData(ESM::Land::DATA_VHGT, &landData); + heightfields.push_back(std::vector(std::begin(landData.mHeights), std::end(landData.mHeights))); + HeightfieldSurface surface; + surface.mHeights = heightfields.back().data(); + surface.mMinHeight = landData.mMinHeight; + surface.mMaxHeight = landData.mMaxHeight; + surface.mSize = static_cast(ESM::Land::LAND_SIZE); + return {surface, landData.mMinHeight, landData.mMaxHeight}; + } + + template + void serializeToStderr(const T& value) + { + const std::vector data = serialize(value); + getRawStderr().write(reinterpret_cast(data.data()), static_cast(data.size())); + } + } + + WorldspaceNavMeshInput::WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings) + : mWorldspace(std::move(worldspace)) + , mTileCachedRecastMeshManager(settings) + { + mAabb.m_min = btVector3(0, 0, 0); + mAabb.m_max = btVector3(0, 0, 0); + } + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, ESM::ReadersCache& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells, bool writeBinaryLog) + { + Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; + + std::map> navMeshInputs; + WorldspaceData data; + + std::size_t objectsCounter = 0; + + if (writeBinaryLog) + serializeToStderr(ExpectedCells {static_cast(esmData.mCells.size())}); + + for (std::size_t i = 0; i < esmData.mCells.size(); ++i) + { + const ESM::Cell& cell = esmData.mCells[i]; + const bool exterior = cell.isExterior(); + + if (!exterior && !processInteriorCells) + { + if (writeBinaryLog) + serializeToStderr(ProcessedCells {static_cast(i + 1)}); + Log(Debug::Info) << "Skipped interior" + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + continue; + } + + Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + + const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY); + const std::size_t cellObjectsBegin = data.mObjects.size(); + + WorldspaceNavMeshInput& navMeshInput = [&] () -> WorldspaceNavMeshInput& + { + auto it = navMeshInputs.find(cell.mCellId.mWorldspace); + if (it == navMeshInputs.end()) + { + it = navMeshInputs.emplace(cell.mCellId.mWorldspace, + std::make_unique(cell.mCellId.mWorldspace, settings.mRecast)).first; + it->second->mTileCachedRecastMeshManager.setWorldspace(cell.mCellId.mWorldspace); + } + return *it->second; + } (); + + if (exterior) + { + const auto it = std::lower_bound(esmData.mLands.begin(), esmData.mLands.end(), cellPosition, LessByXY {}); + const auto [heightfieldShape, minHeight, maxHeight] = makeHeightfieldShape( + it == esmData.mLands.end() ? std::optional() : *it, + cellPosition, data.mHeightfields, data.mLandData + ); + + mergeOrAssign(getAabb(cellPosition, minHeight, maxHeight), + navMeshInput.mAabb, navMeshInput.mAabbInitialized); + + navMeshInput.mTileCachedRecastMeshManager.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, heightfieldShape); + + navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, ESM::Land::REAL_SIZE, -1); + } + else + { + if ((cell.mData.mFlags & ESM::Cell::HasWater) != 0) + navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, std::numeric_limits::max(), cell.mWater); + } + + forEachObject(cell, esmData, vfs, bulletShapeManager, readers, + [&] (BulletObject object) + { + const btTransform& transform = object.getCollisionObject().getWorldTransform(); + const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform); + mergeOrAssign(aabb, navMeshInput.mAabb, navMeshInput.mAabbInitialized); + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + navMeshInput.mAabb.merge(BulletHelpers::getAabb(*avoid, transform)); + + const ObjectId objectId(++objectsCounter); + const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(), object.getObjectTransform()); + + navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, shape, transform, + DetourNavigator::AreaType_ground, [] (const auto&) {}); + + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + { + const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform()); + navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, avoidShape, transform, + DetourNavigator::AreaType_null, [] (const auto&) {}); + } + + data.mObjects.emplace_back(std::move(object)); + }); + + const auto cellDescription = cell.getDescription(); + + if (writeBinaryLog) + serializeToStderr(ProcessedCells {static_cast(i + 1)}); + + Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cellDescription + << " with " << (data.mObjects.size() - cellObjectsBegin) << " objects"; + } + + data.mNavMeshInputs.reserve(navMeshInputs.size()); + std::transform(navMeshInputs.begin(), navMeshInputs.end(), std::back_inserter(data.mNavMeshInputs), + [] (auto& v) { return std::move(v.second); }); + + Log(Debug::Info) << "Processed " << esmData.mCells.size() << " cells, added " + << data.mObjects.size() << " objects and " << data.mHeightfields.size() << " height fields"; + + return data; + } +} diff --git a/apps/navmeshtool/worldspacedata.hpp b/apps/navmeshtool/worldspacedata.hpp new file mode 100644 index 0000000000..87febb4b0c --- /dev/null +++ b/apps/navmeshtool/worldspacedata.hpp @@ -0,0 +1,97 @@ +#ifndef OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H +#define OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ESM +{ + class ESMReader; + class ReadersCache; +} + +namespace VFS +{ + class Manager; +} + +namespace Resource +{ + class BulletShapeManager; +} + +namespace EsmLoader +{ + struct EsmData; +} + +namespace DetourNavigator +{ + struct Settings; +} + +namespace NavMeshTool +{ + using DetourNavigator::TileCachedRecastMeshManager; + using DetourNavigator::ObjectTransform; + + struct WorldspaceNavMeshInput + { + std::string mWorldspace; + TileCachedRecastMeshManager mTileCachedRecastMeshManager; + btAABB mAabb; + bool mAabbInitialized = false; + + explicit WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings); + }; + + class BulletObject + { + public: + BulletObject(osg::ref_ptr&& shapeInstance, const ESM::Position& position, + float localScaling) + : mShapeInstance(std::move(shapeInstance)) + , mObjectTransform {position, localScaling} + , mCollisionObject(BulletHelpers::makeCollisionObject( + mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(position.asVec3()), + Misc::Convert::toBullet(Misc::Convert::makeOsgQuat(position)) + )) + { + mShapeInstance->setLocalScaling(btVector3(localScaling, localScaling, localScaling)); + } + + const osg::ref_ptr& getShapeInstance() const noexcept { return mShapeInstance; } + const DetourNavigator::ObjectTransform& getObjectTransform() const noexcept { return mObjectTransform; } + btCollisionObject& getCollisionObject() const noexcept { return *mCollisionObject; } + + private: + osg::ref_ptr mShapeInstance; + DetourNavigator::ObjectTransform mObjectTransform; + std::unique_ptr mCollisionObject; + }; + + struct WorldspaceData + { + std::vector> mNavMeshInputs; + std::vector mObjects; + std::vector> mLandData; + std::vector> mHeightfields; + }; + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, ESM::ReadersCache& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells, bool writeBinaryLog); +} + +#endif diff --git a/apps/niftest/CMakeLists.txt b/apps/niftest/CMakeLists.txt index 3cbee2b7e8..2f0dcb59e3 100644 --- a/apps/niftest/CMakeLists.txt +++ b/apps/niftest/CMakeLists.txt @@ -9,7 +9,6 @@ openmw_add_executable(niftest ) target_link_libraries(niftest - ${Boost_FILESYSTEM_LIBRARY} components ) @@ -17,3 +16,7 @@ if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(niftest gcov) endif() + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(niftest PRIVATE ) +endif() diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index e9484d5f59..219956cd75 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -1,9 +1,9 @@ ///Program to test .nif files both on the FileSystem and in BSA archives. #include -#include -#include +#include +#include #include #include #include @@ -11,25 +11,15 @@ #include #include -#include // Create local aliases for brevity namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; ///See if the file has the named extension -bool hasExtension(std::string filename, std::string extensionToFind) +bool hasExtension(std::string filename, std::string extensionToFind) { - std::string extension = filename.substr(filename.find_last_of(".")+1); - - //Convert strings to lower case for comparison - std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower); - - if(extension == extensionToFind) - return true; - else - return false; + std::string extension = filename.substr(filename.find_last_of('.')+1); + return Misc::StringUtils::ciEqual(extension, extensionToFind); } ///See if the file has the "nif" extension. @@ -44,19 +34,15 @@ bool isBSA(const std::string & filename) } /// Check all the nif files in a given VFS::Archive -/// \note Takes ownership! /// \note Can not read a bsa file inside of a bsa file. -void readVFS(VFS::Archive* anArchive,std::string archivePath = "") +void readVFS(std::unique_ptr&& anArchive, std::string archivePath = "") { VFS::Manager myManager(true); - myManager.addArchive(anArchive); + myManager.addArchive(std::move(anArchive)); myManager.buildIndex(); - std::map files=myManager.getIndex(); - for(std::map::const_iterator it=files.begin(); it!=files.end(); ++it) + for(const auto& name : myManager.getRecursiveDirectoryIterator("")) { - std::string name = it->first; - try{ if(isNIF(name)) { @@ -68,7 +54,7 @@ void readVFS(VFS::Archive* anArchive,std::string archivePath = "") if(!archivePath.empty() && !isBSA(archivePath)) { // std::cout << "Reading BSA File: " << name << std::endl; - readVFS(new VFS::BsaArchive(archivePath+name),archivePath+name+"/"); + readVFS(std::make_unique(archivePath + name), archivePath + name + "/"); // std::cout << "Done with BSA File: " << name << std::endl; } } @@ -134,7 +120,7 @@ int main(int argc, char **argv) Nif::NIFFile::setLoadUnsupportedFiles(true); // std::cout << "Reading Files" << std::endl; - for(std::vector::const_iterator it=files.begin(); it!=files.end(); ++it) + for(auto it=files.begin(); it!=files.end(); ++it) { std::string name = *it; @@ -143,17 +129,17 @@ int main(int argc, char **argv) if(isNIF(name)) { //std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile temp_nif(Files::openConstrainedFileStream(name.c_str()),name); + Nif::NIFFile temp_nif(Files::openConstrainedFileStream(name), name); } else if(isBSA(name)) { // std::cout << "Reading BSA File: " << name << std::endl; - readVFS(new VFS::BsaArchive(name)); + readVFS(std::make_unique(name)); } - else if(bfs::is_directory(bfs::path(name))) + else if(std::filesystem::is_directory(std::filesystem::path(name))) { // std::cout << "Reading All Files in: " << name << std::endl; - readVFS(new VFS::FileSystemArchive(name),name); + readVFS(std::make_unique(name), name); } else { diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index b20920904c..8a3a41c824 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -8,29 +8,29 @@ opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) -opencs_units_noqt (model/doc +opencs_units (model/doc stage savingstate savingstages blacklist messages ) -opencs_hdrs_noqt (model/doc +opencs_hdrs (model/doc state ) opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel - actoradapter + actoradapter idcollection ) -opencs_units_noqt (model/world +opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) -opencs_hdrs_noqt (model/world +opencs_hdrs (model/world columnimp idcollection collection info subcellcollection ) @@ -39,14 +39,14 @@ opencs_units (model/tools tools reportmodel mergeoperation ) -opencs_units_noqt (model/tools +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 ) -opencs_hdrs_noqt (model/tools +opencs_hdrs (model/tools mergestate ) @@ -57,11 +57,11 @@ opencs_units (view/doc ) -opencs_units_noqt (view/doc +opencs_units (view/doc subviewfactory ) -opencs_hdrs_noqt (view/doc +opencs_hdrs (view/doc subviewfactoryimp ) @@ -71,10 +71,10 @@ 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 + bodypartcreator landtexturecreator landcreator tableheadermouseeventhandler ) -opencs_units_noqt (view/world +opencs_units (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils @@ -89,15 +89,15 @@ opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller - cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw + cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) -opencs_units_noqt (view/render +opencs_units (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) -opencs_hdrs_noqt (view/render +opencs_hdrs (view/render mask ) @@ -106,7 +106,7 @@ opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) -opencs_units_noqt (view/tools +opencs_units (view/tools subviews ) @@ -116,14 +116,14 @@ opencs_units (view/prefs opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut - shortcuteventhandler shortcutmanager shortcutsetting modifiersetting + shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting ) -opencs_units_noqt (model/prefs +opencs_units (model/prefs category ) -opencs_units_noqt (model/filter +opencs_units (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) @@ -150,7 +150,6 @@ if(WIN32) endif(WIN32) qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -qt5_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt5_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) # for compiled .ui files @@ -158,7 +157,7 @@ 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}/openmw-cs.cfg") + set (OPENCS_CFG "${OpenMW_BINARY_DIR}/defaults-cs.bin") set (OPENCS_DEFAULT_FILTERS_FILE "${OpenMW_BINARY_DIR}/resources/defaultfilters") set (OPENCS_OPENMW_CFG "${OpenMW_BINARY_DIR}/openmw.cfg") else() @@ -184,8 +183,7 @@ if(APPLE) set(OPENCS_BUNDLE_NAME "OpenMW-CS") set(OPENCS_BUNDLE_RESOURCES_DIR "${OpenMW_BINARY_DIR}/${OPENCS_BUNDLE_NAME}.app/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) @@ -215,17 +213,21 @@ if(APPLE) endif(APPLE) target_link_libraries(openmw-cs - ${OSG_LIBRARIES} - ${OSGTEXT_LIBRARIES} - ${OSGUTIL_LIBRARIES} + # 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`. + # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGVIEWER_LIBRARIES} - ${OSGGA_LIBRARIES} ${OSGFX_LIBRARIES} + ${OSGGA_LIBRARIES} + ${OSGUTIL_LIBRARIES} + ${OSGTEXT_LIBRARIES} + ${OSG_LIBRARIES} ${EXTERN_OSGQT_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} - components + components_qt ) target_link_libraries(openmw-cs Qt5::Widgets Qt5::Core Qt5::Network Qt5::OpenGL) @@ -241,7 +243,7 @@ if (WIN32) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () - INSTALL(FILES "${INSTALL_SOURCE}/openmw-cs.cfg" DESTINATION ".") + INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION ".") endif() if (MSVC) @@ -255,3 +257,22 @@ endif (MSVC) if(APPLE) INSTALL(TARGETS openmw-cs BUNDLE DESTINATION "." COMPONENT Bundle) endif() + +if(USE_QT) + set_property(TARGET openmw-cs PROPERTY AUTOMOC ON) +endif(USE_QT) + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw-cs PRIVATE + + + + + + + + + + + ) +endif() diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 3f53a523f4..b70c9f9ca8 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -5,22 +5,26 @@ #include #include +#include + +#include #include #include #include #include +#include #include "model/doc/document.hpp" #include "model/world/data.hpp" #ifdef _WIN32 -#include +#include #endif using namespace Fallback; CS::Editor::Editor (int argc, char **argv) -: mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), +: mConfigVariables(readConfiguration()), mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), mPid(""), mLock(), mMerge (mDocumentManager), mIpcServerName ("org.openmw.OpenCS"), mServer(nullptr), mClientSocket(nullptr) { @@ -82,55 +86,67 @@ CS::Editor::~Editor () remove(mPid.string().c_str())); // ignore any error } -std::pair > CS::Editor::readConfig(bool quiet) +boost::program_options::variables_map CS::Editor::readConfiguration() { boost::program_options::variables_map variables; boost::program_options::options_description desc("Syntax: openmw-cs \nAllowed options"); desc.add_options() - ("data", boost::program_options::value()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing()) - ("data-local", boost::program_options::value()->default_value(Files::EscapePath(), "")) + ("data", boost::program_options::value()->default_value(Files::MaybeQuotedPathContainer(), "data")->multitoken()->composing()) + ("data-local", boost::program_options::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), "")) ("fs-strict", boost::program_options::value()->implicit_value(true)->default_value(false)) - ("encoding", boost::program_options::value()->default_value("win1252")) - ("resources", boost::program_options::value()->default_value(Files::EscapePath(), "resources")) - ("fallback-archive", boost::program_options::value()-> - default_value(Files::EscapeStringVector(), "fallback-archive")->multitoken()) + ("encoding", boost::program_options::value()->default_value("win1252")) + ("resources", boost::program_options::value()->default_value(Files::MaybeQuotedPath(), "resources")) + ("fallback-archive", boost::program_options::value>()-> + default_value(std::vector(), "fallback-archive")->multitoken()) ("fallback", boost::program_options::value()->default_value(FallbackMap(), "") ->multitoken()->composing(), "fallback values") - ("script-blacklist", boost::program_options::value()->default_value(Files::EscapeStringVector(), "") + ("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)") ("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().string(), "OpenMW-CS"); + + return variables; +} + +std::pair > CS::Editor::readConfig(bool quiet) +{ + boost::program_options::variables_map& variables = mConfigVariables; Fallback::Map::init(variables["fallback"].as().mMap); - mEncodingName = variables["encoding"].as().toStdString(); + mEncodingName = variables["encoding"].as(); mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName)); mFileDialog.setEncoding (QString::fromUtf8(mEncodingName.c_str())); - mDocumentManager.setResourceDir (mResources = variables["resources"].as().mPath); + mDocumentManager.setResourceDir (mResources = variables["resources"].as()); if (variables["script-blacklist-use"].as()) mDocumentManager.setBlacklistedScripts ( - variables["script-blacklist"].as().toStdStringVector()); + variables["script-blacklist"].as>()); mFsStrict = variables["fs-strict"].as(); Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { - dataDirs = Files::PathContainer(Files::EscapePath::toPathContainer(variables["data"].as())); + dataDirs = asPathContainer(variables["data"].as()); } - Files::PathContainer::value_type local(variables["data-local"].as().mPath); + Files::PathContainer::value_type local(variables["data-local"].as()); if (!local.empty()) + { + boost::filesystem::create_directories(local); dataLocal.push_back(local); - - mCfgMgr.processPaths (dataDirs); - mCfgMgr.processPaths (dataLocal, true); + } + mCfgMgr.filterOutNonExistingPaths(dataDirs); + mCfgMgr.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) mLocal = dataLocal[0]; @@ -149,13 +165,9 @@ std::pair > CS::Editor::readConfi dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); //iterate the data directories and add them to the file dialog for loading - for (Files::PathContainer::const_reverse_iterator iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) - { - QString path = QString::fromUtf8 (iter->string().c_str()); - mFileDialog.addFiles(path); - } + mFileDialog.addFiles(dataDirs); - return std::make_pair (dataDirs, variables["fallback-archive"].as().toStdStringVector()); + return std::make_pair (dataDirs, variables["fallback-archive"].as>()); } void CS::Editor::createGame() @@ -364,7 +376,7 @@ int CS::Editor::run() else { ESM::ESMReader fileReader; - ToUTF8::Utf8Encoder encoder = ToUTF8::calculateEncoding(mEncodingName); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncodingName)); fileReader.setEncoder(&encoder); fileReader.open(mFileToLoad.string()); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 1c93427613..b99c2d91e8 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -40,6 +41,7 @@ namespace CS Q_OBJECT Files::ConfigurationManager mCfgMgr; + boost::program_options::variables_map mConfigVariables; CSMPrefs::State mSettingsState; CSMDoc::DocumentManager mDocumentManager; CSVDoc::StartupDialogue mStartup; @@ -58,6 +60,8 @@ namespace CS Files::PathContainer mDataDirs; std::string mEncodingName; + boost::program_options::variables_map readConfiguration(); + ///< Calls mCfgMgr.readConfiguration; should be used before initialization of mSettingsState as it depends on the configuration. std::pair > readConfig(bool quiet=false); ///< \return data paths diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index 5287c8b191..e46a46a34e 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -5,9 +5,9 @@ #include #include -#include #include +#include #include "model/doc/messages.hpp" #include "model/world/universalid.hpp" @@ -43,6 +43,8 @@ class Application : public QApplication int runApplication(int argc, char *argv[]) { + Platform::init(); + #ifdef Q_OS_MAC setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif @@ -63,6 +65,9 @@ int runApplication(int argc, char *argv[]) application.setWindowIcon (QIcon (":./openmw-cs.png")); CS::Editor editor(argc, argv); +#ifdef __linux__ + setlocale(LC_NUMERIC,"C"); +#endif if(!editor.makeIPCServer()) { @@ -76,5 +81,5 @@ int runApplication(int argc, char *argv[]) int main(int argc, char *argv[]) { - return wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); + return wrapApplication(&runApplication, argc, argv, "OpenMW-CS", false); } diff --git a/apps/opencs/model/doc/blacklist.cpp b/apps/opencs/model/doc/blacklist.cpp index b1d402c699..690d79983a 100644 --- a/apps/opencs/model/doc/blacklist.cpp +++ b/apps/opencs/model/doc/blacklist.cpp @@ -21,7 +21,7 @@ void CSMDoc::Blacklist::add (CSMWorld::UniversalId::Type type, { std::vector& list = mIds[type]; - int size = list.size(); + size_t size = list.size(); list.resize (size+ids.size()); diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 3a20555d1e..48a668c8cf 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -1,6 +1,7 @@ #include "document.hpp" #include +#include #include #include @@ -20,6 +21,7 @@ void CSMDoc::Document::addGmsts() ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Floats[i]; gmst.mValue.setType (ESM::VT_Float); + gmst.mRecordFlags = 0; gmst.mValue.setFloat (CSMWorld::DefaultGmsts::FloatsDefaultValues[i]); getData().getGmsts().add (gmst); } @@ -29,6 +31,7 @@ void CSMDoc::Document::addGmsts() ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Ints[i]; gmst.mValue.setType (ESM::VT_Int); + gmst.mRecordFlags = 0; gmst.mValue.setInteger (CSMWorld::DefaultGmsts::IntsDefaultValues[i]); getData().getGmsts().add (gmst); } @@ -38,6 +41,7 @@ void CSMDoc::Document::addGmsts() ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Strings[i]; gmst.mValue.setType (ESM::VT_String); + gmst.mRecordFlags = 0; gmst.mValue.setString (""); getData().getGmsts().add (gmst); } @@ -115,10 +119,10 @@ void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId (gmst.mId)==-1) { - CSMWorld::Record record; - record.mBase = gmst; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGmsts().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = gmst; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGmsts().appendRecord (std::move(record)); } } @@ -126,10 +130,10 @@ void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) { if (getData().getGlobals().searchId (global.mId)==-1) { - CSMWorld::Record record; - record.mBase = global; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGlobals().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = global; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGlobals().appendRecord (std::move(record)); } } @@ -137,10 +141,10 @@ void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffe { if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) { - CSMWorld::Record record; - record.mBase = magicEffect; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getMagicEffects().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = magicEffect; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getMagicEffects().appendRecord (std::move(record)); } } @@ -163,6 +167,7 @@ void CSMDoc::Document::createBase() { ESM::Global record; record.mId = sGlobals[i]; + record.mRecordFlags = 0; record.mValue.setType (i==2 ? ESM::VT_Float : ESM::VT_Long); if (i==0 || i==1) diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index 0332cb43a1..4aae282fb6 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -7,7 +7,6 @@ #include #include -#include #include #include diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 69c78bd5e4..9d2f89b8e9 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -5,7 +5,6 @@ #include "../tools/reportmodel.hpp" #include "document.hpp" -#include "state.hpp" CSMDoc::Loader::Stage::Stage() : mFile (0), mRecordsLoaded (0), mRecordsLeft (false) {} @@ -86,19 +85,19 @@ void CSMDoc::Loader::load() return; } - if (iter->second.mFilesecond.mFilegetContentFiles()[iter->second.mFile]; - int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, false); + int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, /*project*/false); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage (document, path.filename().string(), steps); } - else if (iter->second.mFile==size) + else if (iter->second.mFile==size) // start loading the last (project) file { - int steps = document->getData().startLoading (document->getProjectPath(), false, true); + int steps = document->getData().startLoading (document->getProjectPath(), /*base*/false, true); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; diff --git a/apps/opencs/model/doc/messages.hpp b/apps/opencs/model/doc/messages.hpp index 671ded82a0..355493b79b 100644 --- a/apps/opencs/model/doc/messages.hpp +++ b/apps/opencs/model/doc/messages.hpp @@ -4,8 +4,6 @@ #include #include -#include - #include "../world/universalid.hpp" namespace CSMDoc diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index 218e13e38e..369c6bb105 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -7,7 +7,6 @@ #include "../world/universalid.hpp" -#include "state.hpp" #include "stage.hpp" void CSMDoc::Operation::prepareStages() diff --git a/apps/opencs/model/doc/operation.hpp b/apps/opencs/model/doc/operation.hpp index ff396fa3ce..b094c08b4c 100644 --- a/apps/opencs/model/doc/operation.hpp +++ b/apps/opencs/model/doc/operation.hpp @@ -6,7 +6,6 @@ #include #include -#include #include "messages.hpp" diff --git a/apps/opencs/model/doc/operationholder.hpp b/apps/opencs/model/doc/operationholder.hpp index b73d61dab1..69af6ed66c 100644 --- a/apps/opencs/model/doc/operationholder.hpp +++ b/apps/opencs/model/doc/operationholder.hpp @@ -25,7 +25,7 @@ namespace CSMDoc public: - OperationHolder (Operation *operation = 0); + OperationHolder (Operation *operation = nullptr); void setOperation (Operation *operation); diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp index 84bc61a9a0..3b2178ce7c 100644 --- a/apps/opencs/model/doc/runner.cpp +++ b/apps/opencs/model/doc/runner.cpp @@ -1,14 +1,14 @@ #include "runner.hpp" -#include #include #include #include +#include #include "operationholder.hpp" CSMDoc::Runner::Runner (const boost::filesystem::path& projectPath) -: mRunning (false), mStartup (0), mProjectPath (projectPath) +: mRunning (false), mStartup (nullptr), mProjectPath (projectPath) { connect (&mProcess, SIGNAL (finished (int, QProcess::ExitStatus)), this, SLOT (finished (int, QProcess::ExitStatus))); @@ -25,7 +25,7 @@ CSMDoc::Runner::~Runner() { if (mRunning) { - disconnect (&mProcess, 0, this, 0); + disconnect (&mProcess, nullptr, this, nullptr); mProcess.kill(); mProcess.waitForFinished(); } @@ -36,7 +36,7 @@ void CSMDoc::Runner::start (bool delayed) if (mStartup) { delete mStartup; - mStartup = 0; + mStartup = nullptr; } if (!delayed) @@ -78,11 +78,13 @@ void CSMDoc::Runner::start (bool delayed) else arguments << "--new-game=1"; - arguments << ("--script-run="+mStartup->fileName());; + arguments << ("--script-run="+mStartup->fileName()); arguments << QString::fromUtf8 (("--data=\""+mProjectPath.parent_path().string()+"\"").c_str()); + arguments << "--replace=content"; + for (std::vector::const_iterator iter (mContentFiles.begin()); iter!=mContentFiles.end(); ++iter) { @@ -102,7 +104,7 @@ void CSMDoc::Runner::start (bool delayed) void CSMDoc::Runner::stop() { delete mStartup; - mStartup = 0; + mStartup = nullptr; if (mProcess.state()==QProcess::NotRunning) { diff --git a/apps/opencs/model/doc/runner.hpp b/apps/opencs/model/doc/runner.hpp index 517122492a..0cfbaab3af 100644 --- a/apps/opencs/model/doc/runner.hpp +++ b/apps/opencs/model/doc/runner.hpp @@ -10,7 +10,7 @@ #include #include -#include +#include class QTemporaryFile; diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 44698cd2e3..6fdf75eb84 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -1,10 +1,10 @@ #include "savingstages.hpp" -#include +#include -#include +#include -#include +#include #include "../world/infocollection.hpp" #include "../world/cellcoordinates.hpp" @@ -53,7 +53,10 @@ void CSMDoc::WriteHeaderStage::perform (int stage, Messages& messages) mState.getWriter().setAuthor (""); mState.getWriter().setDescription (""); mState.getWriter().setRecordCount (0); - mState.getWriter().setFormat (ESM::Header::CurrentFormat); + + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mState.getWriter().setFormat(0); } else { @@ -114,7 +117,7 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) { infoModified = true; break; @@ -140,31 +143,31 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message // write modified selected info records for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) { - ESM::DialInfo info = iter->get(); + ESM::DialInfo info = (*iter)->get(); info.mId = info.mId.substr (info.mId.find_last_of ('#')+1); - info.mPrev = ""; + info.mPrev.clear(); if (iter!=range.first) { CSMWorld::InfoCollection::RecordConstIterator prev = iter; --prev; - info.mPrev = prev->get().mId.substr (prev->get().mId.find_last_of ('#')+1); + info.mPrev = (*prev)->get().mId.substr ((*prev)->get().mId.find_last_of ('#')+1); } CSMWorld::InfoCollection::RecordConstIterator next = iter; ++next; - info.mNext = ""; + info.mNext.clear(); if (next!=range.second) { - info.mNext = next->get().mId.substr (next->get().mId.find_last_of ('#')+1); + info.mNext = (*next)->get().mId.substr ((*next)->get().mId.find_last_of ('#')+1); } writer.startRecord (info.sRecordId); - info.save (writer, iter->mState == CSMWorld::RecordBase::State_Deleted); + info.save (writer, (*iter)->mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (info.sRecordId); } } @@ -253,10 +256,71 @@ int CSMDoc::WriteCellCollectionStage::setup() return mDocument.getData().getCells().getSize(); } +void CSMDoc::WriteCellCollectionStage::writeReferences (const std::deque& references, bool interior, unsigned int& newRefNum) +{ + ESM::ESMWriter& writer = mState.getWriter(); + + for (std::deque::const_iterator iter (references.begin()); + iter!=references.end(); ++iter) + { + const CSMWorld::Record& ref = + mDocument.getData().getReferences().getRecord (*iter); + + if (ref.isModified() || ref.mState == CSMWorld::RecordBase::State_Deleted) + { + CSMWorld::CellRef refRecord = ref.get(); + + // Check for uninitialized content file + if (!refRecord.mRefNum.hasContentFile()) + refRecord.mRefNum.mContentFile = 0; + + // recalculate the ref's cell location + std::ostringstream stream; + if (!interior) + { + std::pair index = refRecord.getCellIndex(); + stream << "#" << index.first << " " << index.second; + } + + if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 || + (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && + refRecord.mCell!=stream.str())) + { + refRecord.mRefNum.mIndex = newRefNum++; + } + else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) + != stream.str() && !interior) + { + // An empty mOriginalCell is meant to indicate that it is the same as + // the current cell. It is possible that a moved ref is moved again. + + ESM::MovedCellRef moved; + moved.mRefNum = refRecord.mRefNum; + + // Need to fill mTarget with the ref's new position. + std::istringstream istream (stream.str().c_str()); + + char ignore; + istream >> ignore >> moved.mTarget[0] >> moved.mTarget[1]; + + refRecord.mRefNum.save (writer, false, "MVRF"); + writer.writeHNT ("CNDT", moved.mTarget); + } + + refRecord.save (writer, false, false, ref.mState == CSMWorld::RecordBase::State_Deleted); + } + } +} + void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& cell = mDocument.getData().getCells().getRecord (stage); + const CSMWorld::RefIdCollection& referenceables = mDocument.getData().getReferenceables(); + const CSMWorld::RefIdData& refIdData = referenceables.getDataSet(); + + std::deque tempRefs; + std::deque persistentRefs; std::map >::const_iterator references = mState.getSubRecords().find (Misc::StringUtils::lowerCase (cell.get().mId)); @@ -281,6 +345,18 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) CSMWorld::CellRef refRecord = ref.get(); + CSMWorld::RefIdData::LocalIndex localIndex = refIdData.searchId(refRecord.mRefID); + unsigned int recordFlags = refIdData.getRecordFlags(refRecord.mRefID); + bool isPersistent = ((recordFlags & ESM::FLAG_Persistent) != 0) + || refRecord.mTeleport + || localIndex.second == CSMWorld::UniversalId::Type_Creature + || localIndex.second == CSMWorld::UniversalId::Type_Npc; + + if (isPersistent) + persistentRefs.push_back(*iter); + else + tempRefs.push_back(*iter); + if (refRecord.mNew || (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && /// \todo consider worldspace @@ -289,7 +365,6 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) if (refRecord.mRefNum.mIndex >= newRefNum) newRefNum = refRecord.mRefNum.mIndex + 1; - } } @@ -312,56 +387,9 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) // write references if (references!=mState.getSubRecords().end()) { - for (std::deque::const_iterator iter (references->second.begin()); - iter!=references->second.end(); ++iter) - { - const CSMWorld::Record& ref = - mDocument.getData().getReferences().getRecord (*iter); - - if (ref.isModified() || ref.mState == CSMWorld::RecordBase::State_Deleted) - { - CSMWorld::CellRef refRecord = ref.get(); - - // Check for uninitialized content file - if (!refRecord.mRefNum.hasContentFile()) - refRecord.mRefNum.mContentFile = 0; - - // recalculate the ref's cell location - std::ostringstream stream; - if (!interior) - { - std::pair index = refRecord.getCellIndex(); - stream << "#" << index.first << " " << index.second; - } - - if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 || - (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && - refRecord.mCell!=stream.str())) - { - refRecord.mRefNum.mIndex = newRefNum++; - } - else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) - != stream.str() && !interior) - { - // An empty mOriginalCell is meant to indicate that it is the same as - // the current cell. It is possible that a moved ref is moved again. - - ESM::MovedCellRef moved; - moved.mRefNum = refRecord.mRefNum; - - // Need to fill mTarget with the ref's new position. - std::istringstream istream (stream.str().c_str()); - - char ignore; - istream >> ignore >> moved.mTarget[0] >> moved.mTarget[1]; - - refRecord.mRefNum.save (writer, false, "MVRF"); - writer.writeHNT ("CNDT", moved.mTarget); - } - - refRecord.save (writer, false, false, ref.mState == CSMWorld::RecordBase::State_Deleted); - } - } + writeReferences(persistentRefs, interior, newRefNum); + cellRecord.saveTempMarker(writer, int(references->second.size()) - persistentRefs.size()); + writeReferences(tempRefs, interior, newRefNum); } writer.endRecord (cellRecord.sRecordId); diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index d3a0cc9b31..9ccd1772e1 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -108,7 +108,7 @@ namespace CSMDoc state == CSMWorld::RecordBase::State_ModifiedOnly || state == CSMWorld::RecordBase::State_Deleted) { - writer.startRecord (record.sRecordId); + writer.startRecord (record.sRecordId, record.mRecordFlags); record.save (writer, state == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } @@ -171,6 +171,8 @@ namespace CSMDoc Document& mDocument; SavingState& mState; + void writeReferences (const std::deque& references, bool interior, unsigned int& newRefNum); + public: WriteCellCollectionStage (Document& document, SavingState& state); diff --git a/apps/opencs/model/doc/savingstate.hpp b/apps/opencs/model/doc/savingstate.hpp index e6c8c545a7..727352a872 100644 --- a/apps/opencs/model/doc/savingstate.hpp +++ b/apps/opencs/model/doc/savingstate.hpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include diff --git a/apps/opencs/model/filter/andnode.cpp b/apps/opencs/model/filter/andnode.cpp index 7578657178..508202edfc 100644 --- a/apps/opencs/model/filter/andnode.cpp +++ b/apps/opencs/model/filter/andnode.cpp @@ -1,7 +1,5 @@ #include "andnode.hpp" -#include - CSMFilter::AndNode::AndNode (const std::vector >& nodes) : NAryNode (nodes, "and") {} diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp index 2fa9ac6ccb..9415f1daff 100644 --- a/apps/opencs/model/filter/narynode.cpp +++ b/apps/opencs/model/filter/narynode.cpp @@ -9,7 +9,7 @@ CSMFilter::NAryNode::NAryNode (const std::vector >& nodes, int CSMFilter::NAryNode::getSize() const { - return mNodes.size(); + return static_cast(mNodes.size()); } const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const diff --git a/apps/opencs/model/filter/ornode.cpp b/apps/opencs/model/filter/ornode.cpp index 9e6d8b2c46..d6abaca40b 100644 --- a/apps/opencs/model/filter/ornode.cpp +++ b/apps/opencs/model/filter/ornode.cpp @@ -1,7 +1,5 @@ #include "ornode.hpp" -#include - CSMFilter::OrNode::OrNode (const std::vector >& nodes) : NAryNode (nodes, "or") {} diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index d2a4f2a356..d02205d596 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -17,6 +17,19 @@ #include "textnode.hpp" #include "valuenode.hpp" +namespace +{ + bool isAlpha(char c) + { + return std::isalpha(static_cast(c)); + } + + bool isDigit(char c) + { + return std::isdigit(static_cast(c)); + } +} + namespace CSMFilter { struct Token @@ -103,7 +116,7 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() { char c = mInput[mIndex]; - if (std::isalpha (c) || c==':' || c=='_' || (!string.empty() && std::isdigit (c)) || c=='"' || + if (isAlpha(c) || c==':' || c=='_' || (!string.empty() && isDigit(c)) || c=='"' || (!string.empty() && string[0]=='"')) string += c; else @@ -150,7 +163,7 @@ CSMFilter::Token CSMFilter::Parser::getNumberToken() { char c = mInput[mIndex]; - if (std::isdigit (c)) + if (isDigit(c)) { string += c; hasDigit = true; @@ -225,10 +238,10 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() case '!': ++mIndex; return Token (Token::Type_OneShot); } - if (c=='"' || c=='_' || std::isalpha (c) || c==':') + if (c=='"' || c=='_' || isAlpha(c) || c==':') return getStringToken(); - if (c=='-' || c=='.' || std::isdigit (c)) + if (c=='-' || c=='.' || isDigit(c)) return getNumberToken(); error(); @@ -247,11 +260,11 @@ std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, b { case Token::Type_Keyword_True: - return std::shared_ptr (new BooleanNode (true)); + return std::make_shared(true); case Token::Type_Keyword_False: - return std::shared_ptr (new BooleanNode (false)); + return std::make_shared(false); case Token::Type_Keyword_And: case Token::Type_Keyword_Or: @@ -265,7 +278,7 @@ std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, b if (mError) return std::shared_ptr(); - return std::shared_ptr (new NotNode (node)); + return std::make_shared(node); } case Token::Type_Keyword_Text: @@ -325,16 +338,10 @@ std::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyw break; } - if (nodes.empty()) - { - error(); - return std::shared_ptr(); - } - switch (keyword.mType) { - case Token::Type_Keyword_And: return std::shared_ptr (new AndNode (nodes)); - case Token::Type_Keyword_Or: return std::shared_ptr (new OrNode (nodes)); + case Token::Type_Keyword_And: return std::make_shared(nodes); + case Token::Type_Keyword_Or: return std::make_shared(nodes); default: error(); return std::shared_ptr(); } } @@ -400,7 +407,7 @@ std::shared_ptr CSMFilter::Parser::parseText() return std::shared_ptr(); } - return std::shared_ptr (new TextNode (columnId, text)); + return std::make_shared(columnId, text); } std::shared_ptr CSMFilter::Parser::parseValue() @@ -525,7 +532,7 @@ std::shared_ptr CSMFilter::Parser::parseValue() return std::shared_ptr(); } - return std::shared_ptr (new ValueNode (columnId, lowerType, upperType, lower, upper)); + return std::make_shared(columnId, lowerType, upperType, lower, upper); } void CSMFilter::Parser::error() @@ -572,7 +579,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) else { // Empty filter string equals to filter "true". - mFilter.reset (new BooleanNode (true)); + mFilter = std::make_shared(true); } return true; diff --git a/apps/opencs/model/prefs/boolsetting.cpp b/apps/opencs/model/prefs/boolsetting.cpp index 6431dc6af3..2ef141b0d3 100644 --- a/apps/opencs/model/prefs/boolsetting.cpp +++ b/apps/opencs/model/prefs/boolsetting.cpp @@ -1,4 +1,3 @@ - #include "boolsetting.hpp" #include @@ -9,9 +8,9 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::BoolSetting::BoolSetting (Category *parent, Settings::Manager *values, +CSMPrefs::BoolSetting::BoolSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, bool default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(0) +: Setting (parent, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip (const std::string& tooltip) @@ -33,14 +32,14 @@ std::pair CSMPrefs::BoolSetting::makeWidgets (QWidget *par connect (mWidget, SIGNAL (stateChanged (int)), this, SLOT (valueChanged (int))); - return std::make_pair (static_cast (0), mWidget); + return std::make_pair (static_cast (nullptr), mWidget); } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { - mWidget->setCheckState(getValues().getBool(getKey(), getParent()->getKey()) + mWidget->setCheckState(Settings::Manager::getBool(getKey(), getParent()->getKey()) ? Qt::Checked : Qt::Unchecked); } @@ -50,7 +49,7 @@ void CSMPrefs::BoolSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); - getValues().setBool (getKey(), getParent()->getKey(), value); + Settings::Manager::setBool (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); diff --git a/apps/opencs/model/prefs/boolsetting.hpp b/apps/opencs/model/prefs/boolsetting.hpp index 941cb50372..4bfd7df47a 100644 --- a/apps/opencs/model/prefs/boolsetting.hpp +++ b/apps/opencs/model/prefs/boolsetting.hpp @@ -17,7 +17,7 @@ namespace CSMPrefs public: - BoolSetting (Category *parent, Settings::Manager *values, + BoolSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, bool default_); BoolSetting& setTooltip (const std::string& tooltip); diff --git a/apps/opencs/model/prefs/coloursetting.cpp b/apps/opencs/model/prefs/coloursetting.cpp index 1a41621da2..569a759632 100644 --- a/apps/opencs/model/prefs/coloursetting.cpp +++ b/apps/opencs/model/prefs/coloursetting.cpp @@ -1,4 +1,3 @@ - #include "coloursetting.hpp" #include @@ -12,9 +11,9 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::ColourSetting::ColourSetting (Category *parent, Settings::Manager *values, +CSMPrefs::ColourSetting::ColourSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, QColor default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(0) +: Setting (parent, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip (const std::string& tooltip) @@ -46,7 +45,7 @@ void CSMPrefs::ColourSetting::updateWidget() if (mWidget) { mWidget->setColor(QString::fromStdString - (getValues().getString(getKey(), getParent()->getKey()))); + (Settings::Manager::getString(getKey(), getParent()->getKey()))); } } @@ -55,7 +54,7 @@ void CSMPrefs::ColourSetting::valueChanged() CSVWidget::ColorEditor& widget = dynamic_cast (*sender()); { QMutexLocker lock (getMutex()); - getValues().setString (getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); + Settings::Manager::setString (getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); } getParent()->getState()->update (*this); diff --git a/apps/opencs/model/prefs/coloursetting.hpp b/apps/opencs/model/prefs/coloursetting.hpp index 4a814c0e2e..097eec4313 100644 --- a/apps/opencs/model/prefs/coloursetting.hpp +++ b/apps/opencs/model/prefs/coloursetting.hpp @@ -22,7 +22,7 @@ namespace CSMPrefs public: - ColourSetting (Category *parent, Settings::Manager *values, + ColourSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, QColor default_); diff --git a/apps/opencs/model/prefs/doublesetting.cpp b/apps/opencs/model/prefs/doublesetting.cpp index 8ae6f4818c..d8253353b5 100644 --- a/apps/opencs/model/prefs/doublesetting.cpp +++ b/apps/opencs/model/prefs/doublesetting.cpp @@ -12,11 +12,11 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::DoubleSetting::DoubleSetting (Category *parent, Settings::Manager *values, +CSMPrefs::DoubleSetting::DoubleSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, double default_) -: Setting (parent, values, mutex, key, label), +: Setting (parent, mutex, key, label), mPrecision(2), mMin (0), mMax (std::numeric_limits::max()), - mDefault (default_), mWidget(0) + mDefault (default_), mWidget(nullptr) {} CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setPrecision(int precision) @@ -75,7 +75,7 @@ void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) { - mWidget->setValue(getValues().getFloat(getKey(), getParent()->getKey())); + mWidget->setValue(Settings::Manager::getFloat(getKey(), getParent()->getKey())); } } @@ -83,7 +83,7 @@ void CSMPrefs::DoubleSetting::valueChanged (double value) { { QMutexLocker lock (getMutex()); - getValues().setFloat (getKey(), getParent()->getKey(), value); + Settings::Manager::setFloat (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); diff --git a/apps/opencs/model/prefs/doublesetting.hpp b/apps/opencs/model/prefs/doublesetting.hpp index 47886e446f..f65a776351 100644 --- a/apps/opencs/model/prefs/doublesetting.hpp +++ b/apps/opencs/model/prefs/doublesetting.hpp @@ -20,7 +20,7 @@ namespace CSMPrefs public: - DoubleSetting (Category *parent, Settings::Manager *values, + DoubleSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, double default_); diff --git a/apps/opencs/model/prefs/enumsetting.cpp b/apps/opencs/model/prefs/enumsetting.cpp index 62cac062a7..096c9aab38 100644 --- a/apps/opencs/model/prefs/enumsetting.cpp +++ b/apps/opencs/model/prefs/enumsetting.cpp @@ -40,9 +40,9 @@ CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const std::string& value, const } -CSMPrefs::EnumSetting::EnumSetting (Category *parent, Settings::Manager *values, +CSMPrefs::EnumSetting::EnumSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, const EnumValue& default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(0) +: Setting (parent, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip (const std::string& tooltip) @@ -107,7 +107,7 @@ void CSMPrefs::EnumSetting::updateWidget() if (mWidget) { int index = mWidget->findText(QString::fromStdString - (getValues().getString(getKey(), getParent()->getKey()))); + (Settings::Manager::getString(getKey(), getParent()->getKey()))); mWidget->setCurrentIndex(index); } @@ -117,7 +117,7 @@ void CSMPrefs::EnumSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); - getValues().setString (getKey(), getParent()->getKey(), mValues.mValues.at (value).mValue); + Settings::Manager::setString (getKey(), getParent()->getKey(), mValues.mValues.at (value).mValue); } getParent()->getState()->update (*this); diff --git a/apps/opencs/model/prefs/enumsetting.hpp b/apps/opencs/model/prefs/enumsetting.hpp index 235f6adc3a..d0467b0639 100644 --- a/apps/opencs/model/prefs/enumsetting.hpp +++ b/apps/opencs/model/prefs/enumsetting.hpp @@ -41,7 +41,7 @@ namespace CSMPrefs public: - EnumSetting (Category *parent, Settings::Manager *values, + EnumSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, const EnumValue& default_); diff --git a/apps/opencs/model/prefs/intsetting.cpp b/apps/opencs/model/prefs/intsetting.cpp index 25dbf78c29..b3d4a66334 100644 --- a/apps/opencs/model/prefs/intsetting.cpp +++ b/apps/opencs/model/prefs/intsetting.cpp @@ -12,10 +12,10 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::IntSetting::IntSetting (Category *parent, Settings::Manager *values, +CSMPrefs::IntSetting::IntSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, int default_) -: Setting (parent, values, mutex, key, label), mMin (0), mMax (std::numeric_limits::max()), - mDefault (default_), mWidget(0) +: Setting (parent, mutex, key, label), mMin (0), mMax (std::numeric_limits::max()), + mDefault (default_), mWidget(nullptr) {} CSMPrefs::IntSetting& CSMPrefs::IntSetting::setRange (int min, int max) @@ -67,7 +67,7 @@ void CSMPrefs::IntSetting::updateWidget() { if (mWidget) { - mWidget->setValue(getValues().getInt(getKey(), getParent()->getKey())); + mWidget->setValue(Settings::Manager::getInt(getKey(), getParent()->getKey())); } } @@ -75,7 +75,7 @@ void CSMPrefs::IntSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); - getValues().setInt (getKey(), getParent()->getKey(), value); + Settings::Manager::setInt (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); diff --git a/apps/opencs/model/prefs/intsetting.hpp b/apps/opencs/model/prefs/intsetting.hpp index f18213b77b..c03e010ad7 100644 --- a/apps/opencs/model/prefs/intsetting.hpp +++ b/apps/opencs/model/prefs/intsetting.hpp @@ -19,7 +19,7 @@ namespace CSMPrefs public: - IntSetting (Category *parent, Settings::Manager *values, + IntSetting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label, int default_); // defaults to [0, std::numeric_limits::max()] diff --git a/apps/opencs/model/prefs/modifiersetting.cpp b/apps/opencs/model/prefs/modifiersetting.cpp index da6b2ccddf..606cece2bc 100644 --- a/apps/opencs/model/prefs/modifiersetting.cpp +++ b/apps/opencs/model/prefs/modifiersetting.cpp @@ -7,15 +7,17 @@ #include #include +#include + #include "state.hpp" #include "shortcutmanager.hpp" namespace CSMPrefs { - ModifierSetting::ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, + ModifierSetting::ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, values, mutex, key, label) - , mButton(0) + : Setting(parent, mutex, key, label) + , mButton(nullptr) , mEditorActive(false) { } @@ -47,7 +49,7 @@ namespace CSMPrefs { if (mButton) { - std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); + std::string shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); @@ -135,7 +137,7 @@ namespace CSMPrefs { QMutexLocker lock(getMutex()); - getValues().setString(getKey(), getParent()->getKey(), value); + Settings::Manager::setString(getKey(), getParent()->getKey(), value); } getParent()->getState()->update(*this); diff --git a/apps/opencs/model/prefs/modifiersetting.hpp b/apps/opencs/model/prefs/modifiersetting.hpp index 977badb8df..d1af9d25c6 100644 --- a/apps/opencs/model/prefs/modifiersetting.hpp +++ b/apps/opencs/model/prefs/modifiersetting.hpp @@ -16,8 +16,7 @@ namespace CSMPrefs public: - ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, - const std::string& label); + ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); std::pair makeWidgets(QWidget* parent) override; diff --git a/apps/opencs/model/prefs/setting.cpp b/apps/opencs/model/prefs/setting.cpp index 165062232a..524938bcc7 100644 --- a/apps/opencs/model/prefs/setting.cpp +++ b/apps/opencs/model/prefs/setting.cpp @@ -4,23 +4,19 @@ #include #include +#include + #include "category.hpp" #include "state.hpp" -Settings::Manager& CSMPrefs::Setting::getValues() -{ - return *mValues; -} - QMutex *CSMPrefs::Setting::getMutex() { return mMutex; } -CSMPrefs::Setting::Setting (Category *parent, Settings::Manager *values, QMutex *mutex, +CSMPrefs::Setting::Setting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label) -: QObject (parent->getState()), mParent (parent), mValues (values), mMutex (mutex), mKey (key), - mLabel (label) +: QObject (parent->getState()), mParent (parent), mMutex (mutex), mKey (key), mLabel (label) {} CSMPrefs::Setting:: ~Setting() {} @@ -52,25 +48,25 @@ const std::string& CSMPrefs::Setting::getLabel() const int CSMPrefs::Setting::toInt() const { QMutexLocker lock (mMutex); - return mValues->getInt (mKey, mParent->getKey()); + return Settings::Manager::getInt (mKey, mParent->getKey()); } double CSMPrefs::Setting::toDouble() const { QMutexLocker lock (mMutex); - return mValues->getFloat (mKey, mParent->getKey()); + return Settings::Manager::getFloat (mKey, mParent->getKey()); } std::string CSMPrefs::Setting::toString() const { QMutexLocker lock (mMutex); - return mValues->getString (mKey, mParent->getKey()); + return Settings::Manager::getString (mKey, mParent->getKey()); } bool CSMPrefs::Setting::isTrue() const { QMutexLocker lock (mMutex); - return mValues->getBool (mKey, mParent->getKey()); + return Settings::Manager::getBool (mKey, mParent->getKey()); } QColor CSMPrefs::Setting::toColor() const diff --git a/apps/opencs/model/prefs/setting.hpp b/apps/opencs/model/prefs/setting.hpp index 7cb2d7acf6..fcf70c26cd 100644 --- a/apps/opencs/model/prefs/setting.hpp +++ b/apps/opencs/model/prefs/setting.hpp @@ -10,11 +10,6 @@ class QWidget; class QColor; class QMutex; -namespace Settings -{ - class Manager; -} - namespace CSMPrefs { class Category; @@ -24,20 +19,17 @@ namespace CSMPrefs Q_OBJECT Category *mParent; - Settings::Manager *mValues; QMutex *mMutex; std::string mKey; std::string mLabel; protected: - Settings::Manager& getValues(); - QMutex *getMutex(); public: - Setting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label); + Setting (Category *parent, QMutex *mutex, const std::string& key, const std::string& label); virtual ~Setting(); diff --git a/apps/opencs/model/prefs/shortcut.cpp b/apps/opencs/model/prefs/shortcut.cpp index 924b9535e2..ff7b949a4a 100644 --- a/apps/opencs/model/prefs/shortcut.cpp +++ b/apps/opencs/model/prefs/shortcut.cpp @@ -23,7 +23,7 @@ namespace CSMPrefs , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) - , mAction(0) + , mAction(nullptr) { assert (parent); @@ -42,7 +42,7 @@ namespace CSMPrefs , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) - , mAction(0) + , mAction(nullptr) { assert (parent); @@ -62,7 +62,7 @@ namespace CSMPrefs , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) - , mAction(0) + , mAction(nullptr) { assert (parent); @@ -218,6 +218,6 @@ namespace CSMPrefs void Shortcut::actionDeleted() { - mAction = 0; + mAction = nullptr; } } diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index f39492c6c7..4b3f81d0ef 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -2,7 +2,6 @@ #include -#include #include #include "shortcut.hpp" @@ -178,7 +177,7 @@ namespace CSMPrefs { const int MaxKeys = 4; // A limitation of QKeySequence - size_t end = data.find(";"); + size_t end = data.find(';'); size_t size = std::min(end, data.size()); std::string value = data.substr(0, size); @@ -191,7 +190,7 @@ namespace CSMPrefs while (start < value.size()) { - end = data.find("+", start); + end = data.find('+', start); end = std::min(end, value.size()); std::string name = value.substr(start, end - start); @@ -243,7 +242,7 @@ namespace CSMPrefs void ShortcutManager::convertFromString(const std::string& data, int& modifier) const { - size_t start = data.find(";") + 1; + size_t start = data.find(';') + 1; start = std::min(start, data.size()); std::string name = data.substr(start); @@ -781,7 +780,7 @@ namespace CSMPrefs std::make_pair((int)Qt::Key_LastNumberRedial , "LastNumberRedial"), std::make_pair((int)Qt::Key_Camera , "Camera"), std::make_pair((int)Qt::Key_CameraFocus , "CameraFocus"), - std::make_pair(0 , (const char*) 0) + std::make_pair(0 , (const char*) nullptr) }; } diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index de495b9fc9..622c182dce 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -8,17 +8,17 @@ #include #include +#include + #include "state.hpp" #include "shortcutmanager.hpp" namespace CSMPrefs { - const int ShortcutSetting::MaxKeys; - - ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, + ShortcutSetting::ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, values, mutex, key, label) - , mButton(0) + : Setting(parent, mutex, key, label) + , mButton(nullptr) , mEditorActive(false) , mEditorPos(0) { @@ -55,7 +55,7 @@ namespace CSMPrefs { if (mButton) { - std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); + std::string shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); @@ -181,7 +181,7 @@ namespace CSMPrefs { QMutexLocker lock(getMutex()); - getValues().setString(getKey(), getParent()->getKey(), value); + Settings::Manager::setString(getKey(), getParent()->getKey(), value); } getParent()->getState()->update(*this); diff --git a/apps/opencs/model/prefs/shortcutsetting.hpp b/apps/opencs/model/prefs/shortcutsetting.hpp index a0c588b42e..cc1394b47e 100644 --- a/apps/opencs/model/prefs/shortcutsetting.hpp +++ b/apps/opencs/model/prefs/shortcutsetting.hpp @@ -16,7 +16,7 @@ namespace CSMPrefs public: - ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, + ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); std::pair makeWidgets(QWidget* parent) override; @@ -34,7 +34,7 @@ namespace CSMPrefs void storeValue(const QKeySequence& sequence); void resetState(); - static const int MaxKeys = 4; + static constexpr int MaxKeys = 4; QPushButton* mButton; diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index abd1ddfc8d..a93684a9f2 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "intsetting.hpp" #include "doublesetting.hpp" #include "boolsetting.hpp" @@ -12,27 +14,7 @@ #include "shortcutsetting.hpp" #include "modifiersetting.hpp" -CSMPrefs::State *CSMPrefs::State::sThis = 0; - -void CSMPrefs::State::load() -{ - // default settings file - boost::filesystem::path local = mConfigurationManager.getLocalPath() / mConfigFile; - boost::filesystem::path global = mConfigurationManager.getGlobalPath() / mConfigFile; - - if (boost::filesystem::exists (local)) - mSettings.loadDefault (local.string()); - else if (boost::filesystem::exists (global)) - mSettings.loadDefault (global.string()); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"openmw-cs.cfg\" was properly installed."); - - // user settings file - boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; - - if (boost::filesystem::exists (user)) - mSettings.loadUser (user.string()); -} +CSMPrefs::State *CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::declare() { @@ -108,6 +90,9 @@ void CSMPrefs::State::declare() "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) + .setTooltip("When editing a record, open the view in a new window," + " rather than docked in the main view."); declareCategory ("ID Dialogues"); declareBool ("toolbar", "Show toolbar", true); @@ -210,6 +195,20 @@ void CSMPrefs::State::declare() 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)). + 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)). + 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)). + 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); declareCategory ("Tooltips"); declareBool ("scene", "Show Tooltips in 3D scenes", true); @@ -234,7 +233,19 @@ void CSMPrefs::State::declare() 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); + 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). 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"); @@ -263,6 +274,12 @@ void CSMPrefs::State::declare() declareBool ("open-list-view", "Open displays list view", false). 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). + 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). + setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection."). + addValues (secondarySelectAction); declareCategory ("Key Bindings"); @@ -393,6 +410,16 @@ void CSMPrefs::State::declare() declareSubcategory ("Script Editor"); declareShortcut ("script-editor-comment", "Comment Selection", QKeySequence()); declareShortcut ("script-editor-uncomment", "Uncomment Selection", QKeySequence()); + + 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"); } void CSMPrefs::State::declareCategory (const std::string& key) @@ -418,10 +445,10 @@ CSMPrefs::IntSetting& CSMPrefs::State::declareInt (const std::string& key, setDefault(key, std::to_string(default_)); - default_ = mSettings.getInt (key, mCurrentCategory->second.getKey()); + default_ = Settings::Manager::getInt (key, mCurrentCategory->second.getKey()); CSMPrefs::IntSetting *setting = - new CSMPrefs::IntSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, + new CSMPrefs::IntSetting (&mCurrentCategory->second, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); @@ -439,10 +466,10 @@ CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble (const std::string& key, stream << default_; setDefault(key, stream.str()); - default_ = mSettings.getFloat (key, mCurrentCategory->second.getKey()); + default_ = Settings::Manager::getFloat (key, mCurrentCategory->second.getKey()); CSMPrefs::DoubleSetting *setting = - new CSMPrefs::DoubleSetting (&mCurrentCategory->second, &mSettings, &mMutex, + new CSMPrefs::DoubleSetting (&mCurrentCategory->second, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); @@ -458,10 +485,10 @@ CSMPrefs::BoolSetting& CSMPrefs::State::declareBool (const std::string& key, setDefault (key, default_ ? "true" : "false"); - default_ = mSettings.getBool (key, mCurrentCategory->second.getKey()); + default_ = Settings::Manager::getBool (key, mCurrentCategory->second.getKey()); CSMPrefs::BoolSetting *setting = - new CSMPrefs::BoolSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, + new CSMPrefs::BoolSetting (&mCurrentCategory->second, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); @@ -477,10 +504,10 @@ CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum (const std::string& key, setDefault (key, default_.mValue); - default_.mValue = mSettings.getString (key, mCurrentCategory->second.getKey()); + default_.mValue = Settings::Manager::getString (key, mCurrentCategory->second.getKey()); CSMPrefs::EnumSetting *setting = - new CSMPrefs::EnumSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, + new CSMPrefs::EnumSetting (&mCurrentCategory->second, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); @@ -496,10 +523,10 @@ CSMPrefs::ColourSetting& CSMPrefs::State::declareColour (const std::string& key, setDefault (key, default_.name().toUtf8().data()); - default_.setNamedColor (QString::fromUtf8 (mSettings.getString (key, mCurrentCategory->second.getKey()).c_str())); + default_.setNamedColor (QString::fromUtf8 (Settings::Manager::getString (key, mCurrentCategory->second.getKey()).c_str())); CSMPrefs::ColourSetting *setting = - new CSMPrefs::ColourSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, + new CSMPrefs::ColourSetting (&mCurrentCategory->second, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); @@ -519,16 +546,34 @@ CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut (const std::string& // Setup with actual data QKeySequence sequence; - getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), sequence); + getShortcutManager().convertFromString(Settings::Manager::getString(key, mCurrentCategory->second.getKey()), sequence); getShortcutManager().setSequence(key, sequence); - CSMPrefs::ShortcutSetting *setting = new CSMPrefs::ShortcutSetting (&mCurrentCategory->second, &mSettings, &mMutex, + CSMPrefs::ShortcutSetting *setting = new CSMPrefs::ShortcutSetting (&mCurrentCategory->second, &mMutex, key, label); mCurrentCategory->second.addSetting (setting); return *setting; } +CSMPrefs::StringSetting& CSMPrefs::State::declareString (const std::string& key, const std::string& label, std::string default_) +{ + 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_); + + mCurrentCategory->second.addSetting (setting); + + return *setting; +} + CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier(const std::string& key, const std::string& label, int default_) { @@ -541,10 +586,10 @@ CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier(const std::string& k // Setup with actual data int modifier; - getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), modifier); + getShortcutManager().convertFromString(Settings::Manager::getString(key, mCurrentCategory->second.getKey()), modifier); getShortcutManager().setModifier(key, modifier); - CSMPrefs::ModifierSetting *setting = new CSMPrefs::ModifierSetting (&mCurrentCategory->second, &mSettings, &mMutex, + CSMPrefs::ModifierSetting *setting = new CSMPrefs::ModifierSetting (&mCurrentCategory->second, &mMutex, key, label); mCurrentCategory->second.addSetting (setting); @@ -557,7 +602,7 @@ void CSMPrefs::State::declareSeparator() throw std::logic_error ("no category for setting"); CSMPrefs::Setting *setting = - new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", ""); + new CSMPrefs::Setting (&mCurrentCategory->second, &mMutex, "", ""); mCurrentCategory->second.addSetting (setting); } @@ -568,7 +613,7 @@ void CSMPrefs::State::declareSubcategory(const std::string& label) throw std::logic_error ("no category for setting"); CSMPrefs::Setting *setting = - new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", label); + new CSMPrefs::Setting (&mCurrentCategory->second, &mMutex, "", label); mCurrentCategory->second.addSetting (setting); } @@ -578,14 +623,14 @@ void CSMPrefs::State::setDefault (const std::string& key, const std::string& def Settings::CategorySetting fullKey (mCurrentCategory->second.getKey(), key); Settings::CategorySettingValueMap::iterator iter = - mSettings.mDefaultSettings.find (fullKey); + Settings::Manager::mDefaultSettings.find (fullKey); - if (iter==mSettings.mDefaultSettings.end()) - mSettings.mDefaultSettings.insert (std::make_pair (fullKey, default_)); + if (iter==Settings::Manager::mDefaultSettings.end()) + Settings::Manager::mDefaultSettings.insert (std::make_pair (fullKey, default_)); } CSMPrefs::State::State (const Files::ConfigurationManager& configurationManager) -: mConfigFile ("openmw-cs.cfg"), mConfigurationManager (configurationManager), +: mConfigFile ("openmw-cs.cfg"), mDefaultConfigFile("defaults-cs.bin"), mConfigurationManager (configurationManager), mCurrentCategory (mCategories.end()) { if (sThis) @@ -593,19 +638,18 @@ CSMPrefs::State::State (const Files::ConfigurationManager& configurationManager) sThis = this; - load(); declare(); } CSMPrefs::State::~State() { - sThis = 0; + sThis = nullptr; } void CSMPrefs::State::save() { boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; - mSettings.saveUser (user.string()); + Settings::Manager::saveUser (user.string()); } CSMPrefs::State::Iterator CSMPrefs::State::begin() @@ -648,16 +692,16 @@ CSMPrefs::State& CSMPrefs::State::get() void CSMPrefs::State::resetCategory(const std::string& category) { - for (Settings::CategorySettingValueMap::iterator i = mSettings.mUserSettings.begin(); - i != mSettings.mUserSettings.end(); ++i) + 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 - mSettings.mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); + Settings::Manager::mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); // reset the value to the default - i->second = mSettings.mDefaultSettings[i->first]; + i->second = Settings::Manager::mDefaultSettings[i->first]; } } diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index a32583dc51..f464013644 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -11,11 +11,10 @@ #include #endif -#include - #include "category.hpp" #include "setting.hpp" #include "enumsetting.hpp" +#include "stringsetting.hpp" #include "shortcutmanager.hpp" class QColor; @@ -47,9 +46,9 @@ namespace CSMPrefs private: const std::string mConfigFile; + const std::string mDefaultConfigFile; const Files::ConfigurationManager& mConfigurationManager; ShortcutManager mShortcutManager; - Settings::Manager mSettings; Collection mCategories; Iterator mCurrentCategory; QMutex mMutex; @@ -60,8 +59,6 @@ namespace CSMPrefs private: - void load(); - void declare(); void declareCategory (const std::string& key); @@ -78,6 +75,8 @@ namespace CSMPrefs ShortcutSetting& declareShortcut (const std::string& key, const std::string& label, const QKeySequence& default_); + StringSetting& declareString (const std::string& key, const std::string& label, std::string default_); + ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); void declareSeparator(); diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp new file mode 100644 index 0000000000..c062e23465 --- /dev/null +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -0,0 +1,54 @@ + +#include "stringsetting.hpp" + +#include +#include + +#include + +#include "category.hpp" +#include "state.hpp" + +CSMPrefs::StringSetting::StringSetting (Category *parent, + QMutex *mutex, const std::string& key, const std::string& label, std::string default_) +: Setting (parent, mutex, key, label), mDefault (default_), mWidget(nullptr) +{} + +CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip (const std::string& tooltip) +{ + mTooltip = tooltip; + return *this; +} + +std::pair CSMPrefs::StringSetting::makeWidgets (QWidget *parent) +{ + mWidget = new QLineEdit (QString::fromUtf8 (mDefault.c_str()), parent); + + if (!mTooltip.empty()) + { + QString tooltip = QString::fromUtf8 (mTooltip.c_str()); + mWidget->setToolTip (tooltip); + } + + connect (mWidget, SIGNAL (textChanged (QString)), this, SLOT (textChanged (QString))); + + return std::make_pair (static_cast (nullptr), mWidget); +} + +void CSMPrefs::StringSetting::updateWidget() +{ + if (mWidget) + { + mWidget->setText(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); + } +} + +void CSMPrefs::StringSetting::textChanged (const QString& text) +{ + { + QMutexLocker lock (getMutex()); + Settings::Manager::setString (getKey(), getParent()->getKey(), text.toStdString()); + } + + getParent()->getState()->update (*this); +} diff --git a/apps/opencs/model/prefs/stringsetting.hpp b/apps/opencs/model/prefs/stringsetting.hpp new file mode 100644 index 0000000000..d4203bce8f --- /dev/null +++ b/apps/opencs/model/prefs/stringsetting.hpp @@ -0,0 +1,36 @@ +#ifndef CSM_PREFS_StringSetting_H +#define CSM_PREFS_StringSetting_H + +#include "setting.hpp" + +class QLineEdit; + +namespace CSMPrefs +{ + class StringSetting : public Setting + { + 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 default_); + + StringSetting& setTooltip (const std::string& tooltip); + + /// Return label, input widget. + std::pair makeWidgets (QWidget *parent) override; + + void updateWidget() override; + + private slots: + + void textChanged (const QString& text); + }; +} + +#endif diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 498894f882..1d88673adc 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_BIRTHSIGNCHECK_H #define CSM_TOOLS_BIRTHSIGNCHECK_H -#include +#include #include "../world/idcollection.hpp" #include "../world/resources.hpp" diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp index 2c379bd078..2eba75c495 100644 --- a/apps/opencs/model/tools/bodypartcheck.hpp +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -1,8 +1,8 @@ #ifndef CSM_TOOLS_BODYPARTCHECK_H #define CSM_TOOLS_BODYPARTCHECK_H -#include -#include +#include +#include #include "../world/resources.hpp" #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index a82121597c..aa64805e5d 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -2,12 +2,11 @@ #include -#include -#include +#include +#include #include "../prefs/state.hpp" -#include "../world/universalid.hpp" CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) : mClasses (classes) diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index a78c2eb975..9d66336d43 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_CLASSCHECK_H #define CSM_TOOLS_CLASSCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/enchantmentcheck.hpp b/apps/opencs/model/tools/enchantmentcheck.hpp index e9c8b9eece..8ee71ad7cb 100644 --- a/apps/opencs/model/tools/enchantmentcheck.hpp +++ b/apps/opencs/model/tools/enchantmentcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_ENCHANTMENTCHECK_H #define CSM_TOOLS_ENCHANTMENTCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 8a198e9535..2be59c2f9d 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -2,11 +2,10 @@ #include -#include +#include #include "../prefs/state.hpp" -#include "../world/universalid.hpp" CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) : mFactions (factions) diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index d281c1b416..a6a6815976 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_FACTIONCHECK_H #define CSM_TOOLS_FACTIONCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/gmstcheck.hpp b/apps/opencs/model/tools/gmstcheck.hpp index 2c12a8607a..c57f6a088b 100644 --- a/apps/opencs/model/tools/gmstcheck.hpp +++ b/apps/opencs/model/tools/gmstcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_GMSTCHECK_H #define CSM_TOOLS_GMSTCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index ae83abfa01..5959cdf54b 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -35,7 +35,7 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it) { - const CSMWorld::Record infoRecord = (*it); + const CSMWorld::Record infoRecord = (*it->get()); if (infoRecord.isDeleted()) continue; diff --git a/apps/opencs/model/tools/journalcheck.hpp b/apps/opencs/model/tools/journalcheck.hpp index b63127b522..65ce8b85df 100644 --- a/apps/opencs/model/tools/journalcheck.hpp +++ b/apps/opencs/model/tools/journalcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_JOURNALCHECK_H #define CSM_TOOLS_JOURNALCHECK_H -#include +#include #include "../world/idcollection.hpp" #include "../world/infocollection.hpp" diff --git a/apps/opencs/model/tools/magiceffectcheck.hpp b/apps/opencs/model/tools/magiceffectcheck.hpp index 4b2c24cc7c..e264683d04 100644 --- a/apps/opencs/model/tools/magiceffectcheck.hpp +++ b/apps/opencs/model/tools/magiceffectcheck.hpp @@ -1,8 +1,8 @@ #ifndef CSM_TOOLS_MAGICEFFECTCHECK_HPP #define CSM_TOOLS_MAGICEFFECTCHECK_HPP -#include -#include +#include +#include #include "../world/idcollection.hpp" #include "../world/refidcollection.hpp" diff --git a/apps/opencs/model/tools/mandatoryid.cpp b/apps/opencs/model/tools/mandatoryid.cpp index 23adb9d376..d0d9cc0b9d 100644 --- a/apps/opencs/model/tools/mandatoryid.cpp +++ b/apps/opencs/model/tools/mandatoryid.cpp @@ -11,7 +11,7 @@ CSMTools::MandatoryIdStage::MandatoryIdStage (const CSMWorld::CollectionBase& id int CSMTools::MandatoryIdStage::setup() { - return mIds.size(); + return static_cast(mIds.size()); } void CSMTools::MandatoryIdStage::perform (int stage, CSMDoc::Messages& messages) diff --git a/apps/opencs/model/tools/mergestages.cpp b/apps/opencs/model/tools/mergestages.cpp index 897c3329c5..9b24f6ccf9 100644 --- a/apps/opencs/model/tools/mergestages.cpp +++ b/apps/opencs/model/tools/mergestages.cpp @@ -1,8 +1,5 @@ - #include "mergestages.hpp" -#include - #include #include "mergestate.hpp" @@ -103,10 +100,9 @@ void CSMTools::MergeReferencesStage::perform (int stage, CSMDoc::Messages& messa ref.mRefNum.mContentFile = 0; ref.mNew = false; - CSMWorld::Record newRecord ( - CSMWorld::RecordBase::State_ModifiedOnly, 0, &ref); - - mState.mTarget->getData().getReferences().appendRecord (newRecord); + mState.mTarget->getData().getReferences().appendRecord ( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref))); } } @@ -128,7 +124,9 @@ void CSMTools::PopulateLandTexturesMergeStage::perform (int stage, CSMDoc::Messa if (!record.isDeleted()) { - mState.mTarget->getData().getLandTextures().appendRecord(record); + mState.mTarget->getData().getLandTextures().appendRecord( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } @@ -150,7 +148,9 @@ void CSMTools::MergeLandStage::perform (int stage, CSMDoc::Messages& messages) if (!record.isDeleted()) { - mState.mTarget->getData().getLand().appendRecord (record); + mState.mTarget->getData().getLand().appendRecord ( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } @@ -185,10 +185,9 @@ void CSMTools::FixLandsAndLandTexturesMergeStage::perform (int stage, CSMDoc::Me const CSMWorld::Record& oldRecord = mState.mTarget->getData().getLand().getRecord (stage); - CSMWorld::Record newRecord(CSMWorld::RecordBase::State_ModifiedOnly, - nullptr, &oldRecord.get()); - - mState.mTarget->getData().getLand().setRecord(stage, newRecord); + mState.mTarget->getData().getLand().setRecord (stage, + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &oldRecord.get()))); } } diff --git a/apps/opencs/model/tools/mergestages.hpp b/apps/opencs/model/tools/mergestages.hpp index bcb3fe2ad2..778bea7c68 100644 --- a/apps/opencs/model/tools/mergestages.hpp +++ b/apps/opencs/model/tools/mergestages.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -82,7 +83,8 @@ namespace CSMTools const CSMWorld::Record& record = source.getRecord (stage); if (!record.isDeleted()) - target.appendRecord (CSMWorld::Record (CSMWorld::RecordBase::State_ModifiedOnly, 0, &record.get())); + target.appendRecord (std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } class MergeRefIdsStage : public CSMDoc::Stage diff --git a/apps/opencs/model/tools/mergestate.hpp b/apps/opencs/model/tools/mergestate.hpp index 96e6752e20..a100d7a2e0 100644 --- a/apps/opencs/model/tools/mergestate.hpp +++ b/apps/opencs/model/tools/mergestate.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_MERGESTATE_H #define CSM_TOOLS_MERGESTATE_H -#include +#include #include #include diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index 7c70f13b00..fe08f4bb67 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_RACECHECK_H #define CSM_TOOLS_RACECHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 27a73be93d..127281f5d9 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -38,7 +38,7 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) // 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.mA + region.mData.mB; + region.mData.mSnow + region.mData.mBlizzard; 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/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index e7ddb0bcab..71893c6c5d 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_REGIONCHECK_H #define CSM_TOOLS_REGIONCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index f9a1fdb0c7..a2901a6630 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -24,7 +24,7 @@ int CSMTools::ReportModel::rowCount (const QModelIndex & parent) const if (parent.isValid()) return 0; - return mRows.size(); + return static_cast(mRows.size()); } int CSMTools::ReportModel::columnCount (const QModelIndex & parent) const @@ -140,7 +140,7 @@ bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& p void CSMTools::ReportModel::add (const CSMDoc::Message& message) { - beginInsertRows (QModelIndex(), mRows.size(), mRows.size()); + beginInsertRows (QModelIndex(), static_cast(mRows.size()), static_cast(mRows.size())); mRows.push_back (message); @@ -176,7 +176,7 @@ void CSMTools::ReportModel::clear() { if (!mRows.empty()) { - beginRemoveRows (QModelIndex(), 0, mRows.size()-1); + beginRemoveRows (QModelIndex(), 0, static_cast(mRows.size())-1); mRows.clear(); endRemoveRows(); } diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index 952127edf2..6fa2dfcfc9 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -1,5 +1,7 @@ #include "scriptcheck.hpp" +#include + #include #include #include @@ -50,7 +52,7 @@ void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) } CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) -: mDocument (document), mContext (document.getData()), mMessages (0), mWarningMode (Mode_Ignore) +: mDocument (document), mContext (document.getData()), mMessages (nullptr), mWarningMode (Mode_Ignore) { /// \todo add an option to configure warning mode setWarningsMode (0); @@ -73,7 +75,7 @@ int CSMTools::ScriptCheckStage::setup() mWarningMode = Mode_Strict; mContext.clear(); - mMessages = 0; + mMessages = nullptr; mId.clear(); Compiler::ErrorHandler::reset(); @@ -130,5 +132,5 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) messages.add (id, stream.str(), "", CSMDoc::Message::Severity_SeriousError); } - mMessages = 0; + mMessages = nullptr; } diff --git a/apps/opencs/model/tools/searchstage.cpp b/apps/opencs/model/tools/searchstage.cpp index 3db10b0c37..7cd3f49243 100644 --- a/apps/opencs/model/tools/searchstage.cpp +++ b/apps/opencs/model/tools/searchstage.cpp @@ -5,7 +5,7 @@ #include "searchoperation.hpp" CSMTools::SearchStage::SearchStage (const CSMWorld::IdTableBase *model) -: mModel (model), mOperation (0) +: mModel (model), mOperation (nullptr) {} int CSMTools::SearchStage::setup() diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index b1af887f6c..f13a5b7a29 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_SKILLCHECK_H #define CSM_TOOLS_SKILLCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index 80eb9e7f29..fc3255c538 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_SOUNDCHECK_H #define CSM_TOOLS_SOUNDCHECK_H -#include +#include #include "../world/resources.hpp" #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index dc9ce65c0a..eb2302aa70 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -1,13 +1,9 @@ #include "spellcheck.hpp" -#include -#include - -#include +#include #include "../prefs/state.hpp" -#include "../world/universalid.hpp" CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) : mSpells (spells) diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index bfc9628107..1a8d3d0237 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -1,7 +1,7 @@ #ifndef CSM_TOOLS_SPELLCHECK_H #define CSM_TOOLS_SPELLCHECK_H -#include +#include #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/tools/startscriptcheck.hpp b/apps/opencs/model/tools/startscriptcheck.hpp index a45d3c9437..4113090793 100644 --- a/apps/opencs/model/tools/startscriptcheck.hpp +++ b/apps/opencs/model/tools/startscriptcheck.hpp @@ -1,8 +1,8 @@ #ifndef CSM_TOOLS_STARTSCRIPTCHECK_H #define CSM_TOOLS_STARTSCRIPTCHECK_H -#include -#include +#include +#include #include "../doc/stage.hpp" diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index 07a721e8e2..07e0cc038e 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -1,7 +1,5 @@ #include "tools.hpp" -#include - #include "../doc/state.hpp" #include "../doc/operation.hpp" #include "../doc/document.hpp" @@ -43,7 +41,7 @@ CSMDoc::OperationHolder *CSMTools::Tools::get (int type) case CSMDoc::State_Merging: return &mMerge; } - return 0; + return nullptr; } const CSMDoc::OperationHolder *CSMTools::Tools::get (int type) const @@ -138,8 +136,8 @@ CSMDoc::OperationHolder *CSMTools::Tools::getVerifier() } CSMTools::Tools::Tools (CSMDoc::Document& document, ToUTF8::FromType encoding) -: mDocument (document), mData (document.getData()), mVerifierOperation (0), - mSearchOperation (0), mMergeOperation (0), mNextReportNumber (0), mEncoding (encoding) +: mDocument (document), mData (document.getData()), mVerifierOperation (nullptr), + mSearchOperation (nullptr), mMergeOperation (nullptr), mNextReportNumber (0), mEncoding (encoding) { // index 0: load error log mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel)); diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index fe9bc991d4..74643a46ee 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -395,12 +395,12 @@ bool CSMTools::TopicInfoCheckStage::verifyId(const std::string& name, const CSMW if (index == -1) { - messages.add(id, T::getRecordType() + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, std::string(T::getRecordType()) + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (collection.getRecord(index).isDeleted()) { - messages.add(id, "Deleted " + T::getRecordType() + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Deleted " + std::string(T::getRecordType()) + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp index b9dbdc1536..de3fa82ae3 100644 --- a/apps/opencs/model/tools/topicinfocheck.hpp +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -3,13 +3,13 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include "../world/cell.hpp" #include "../world/idcollection.hpp" diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 5ed80a1e4e..e6a193b3e0 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -1,14 +1,17 @@ #include "actoradapter.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include "data.hpp" +#include +#include + namespace CSMWorld { const std::string& ActorAdapter::RaceData::getId() const @@ -121,7 +124,7 @@ namespace CSMWorld return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf); } - const std::string ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const + std::string_view ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const { auto it = mParts.find(index); if (it == mParts.end()) @@ -131,7 +134,7 @@ namespace CSMWorld if (mFemale) { // Note: we should use male parts for females as fallback - const std::string femalePart = mRaceData->getFemalePart(index); + const std::string& femalePart = mRaceData->getFemalePart(index); if (!femalePart.empty()) return femalePart; } @@ -139,11 +142,10 @@ namespace CSMWorld return mRaceData->getMalePart(index); } - return ""; + return {}; } - const std::string& partName = it->second.first; - return partName; + return it->second.first; } bool ActorAdapter::ActorData::hasDependency(const std::string& id) const @@ -226,7 +228,7 @@ namespace CSMWorld } // Create the actor data - data.reset(new ActorData()); + data = std::make_shared(); setupActor(id, data); mCachedActors.insert(id, data); return data; @@ -429,7 +431,7 @@ namespace CSMWorld if (data) return data; // Create the race data - data.reset(new RaceData()); + data = std::make_shared(); setupRace(id, data); mCachedRaces.insert(id, data); return data; @@ -593,56 +595,33 @@ namespace CSMWorld } else if (type == UniversalId::Type_Clothing) { - int priority = 0; - // TODO: reserve bodyparts for robes and skirts auto& clothing = dynamic_cast&>(record).get(); + std::vector parts; if (clothing.mData.mType == ESM::Clothing::Robe) { - auto reservedList = std::vector(); - - ESM::PartReference pr; - pr.mMale = ""; - pr.mFemale = ""; - - ESM::PartReferenceType parts[] = { + parts = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, - ESM::PRT_RForearm, ESM::PRT_LForearm + ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass }; - size_t parts_size = sizeof(parts)/sizeof(parts[0]); - for(size_t p = 0;p < parts_size;++p) - { - pr.mPart = parts[p]; - reservedList.push_back(pr); - } - - priority = parts_size; - addParts(reservedList, priority); } else if (clothing.mData.mType == ESM::Clothing::Skirt) { - auto reservedList = std::vector(); + parts = {ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg}; + } + std::vector reservedList; + for (const auto& p : parts) + { ESM::PartReference pr; - pr.mMale = ""; - pr.mFemale = ""; - - ESM::PartReferenceType parts[] = { - ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg - }; - size_t parts_size = sizeof(parts)/sizeof(parts[0]); - for(size_t p = 0;p < parts_size;++p) - { - pr.mPart = parts[p]; - reservedList.push_back(pr); - } - - priority = parts_size; - addParts(reservedList, priority); + pr.mPart = p; + reservedList.emplace_back(pr); } + int priority = parts.size(); addParts(clothing.mParts.mParts, priority); + addParts(reservedList, priority); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); diff --git a/apps/opencs/model/world/actoradapter.hpp b/apps/opencs/model/world/actoradapter.hpp index 912a6bcb38..2d8375bb2f 100644 --- a/apps/opencs/model/world/actoradapter.hpp +++ b/apps/opencs/model/world/actoradapter.hpp @@ -4,12 +4,14 @@ #include #include #include +#include +#include #include #include -#include -#include +#include +#include #include #include "refidcollection.hpp" @@ -93,7 +95,7 @@ namespace CSMWorld /// Returns the skeleton the actor should use for attaching parts to std::string getSkeleton() const; /// Retrieves the associated actor part - const std::string getPart(ESM::PartReferenceType index) const; + std::string_view getPart(ESM::PartReferenceType index) const; /// Checks if the actor has a data dependency bool hasDependency(const std::string& id) const; diff --git a/apps/opencs/model/world/cell.hpp b/apps/opencs/model/world/cell.hpp index 160610874c..256a07d301 100644 --- a/apps/opencs/model/world/cell.hpp +++ b/apps/opencs/model/world/cell.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/cellcoordinates.cpp b/apps/opencs/model/world/cellcoordinates.cpp index af8c26d70a..9fde26a962 100644 --- a/apps/opencs/model/world/cellcoordinates.cpp +++ b/apps/opencs/model/world/cellcoordinates.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include CSMWorld::CellCoordinates::CellCoordinates() : mX (0), mY (0) {} diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 451ef9d0e1..859866058c 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include @@ -84,7 +86,7 @@ namespace CSMWorld private: - std::vector > mRecords; + std::vector > > mRecords; std::map mIndex; std::vector *> mColumns; @@ -94,9 +96,7 @@ namespace CSMWorld protected: - const std::map& getIdMap() const; - - const std::vector >& getRecords() const; + const std::vector > >& getRecords() const; bool reorderRowsImp (int baseIndex, const std::vector& newOrder); ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices @@ -154,16 +154,16 @@ namespace CSMWorld ///< Change the state of a record from base to modified, if it is not already. /// \return True if the record was changed. - int searchId (const std::string& id) const override; + int searchId(std::string_view id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - void replace (int index, const RecordBase& record) override; + void replace (int index, std::unique_ptr record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. - void appendRecord (const RecordBase& record, + void appendRecord (std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) override; ///< If the record type does not match, an exception is thrown. ///< \param type Will be ignored, unless the collection supports multiple record types @@ -181,7 +181,7 @@ namespace CSMWorld /// /// \param listDeleted include deleted record in the list - virtual void insertRecord (const RecordBase& record, int index, + virtual void insertRecord (std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None); ///< Insert record before index. /// @@ -198,20 +198,14 @@ namespace CSMWorld void addColumn (Column *column); - void setRecord (int index, const Record& record); + void setRecord (int index, std::unique_ptr > record); ///< \attention This function must not change the ID. NestableColumn *getNestableColumn (int column) const; }; template - const std::map& Collection::getIdMap() const - { - return mIndex; - } - - template - const std::vector >& Collection::getRecords() const + const std::vector > >& Collection::getRecords() const { return mRecords; } @@ -231,15 +225,16 @@ namespace CSMWorld return false; // reorder records - std::vector > buffer (size); + std::vector > > buffer (size); for (int i=0; isetModified (buffer[newOrder[i]]->get()); } - std::copy (buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); + std::move (buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); // adjust index for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); @@ -255,18 +250,19 @@ namespace CSMWorld int Collection::cloneRecordImp(const std::string& origin, const std::string& destination, UniversalId::Type type) { - Record copy; - copy.mModified = getRecord(origin).get(); - copy.mState = RecordBase::State_ModifiedOnly; - IdAccessorT().setId(copy.get(), destination); + auto copy = std::make_unique>(); + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + IdAccessorT().setId(copy->get(), destination); - if (type == UniversalId::Type_Reference) { - CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©.mModified; + if (type == UniversalId::Type_Reference) + { + CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©->mModified; ptr->mRefNum.mIndex = 0; } int index = getAppendIndex(destination, type); - insertRecord(copy, getAppendIndex(destination, type)); + insertRecord(std::move(copy), getAppendIndex(destination, type)); return index; } @@ -275,7 +271,7 @@ namespace CSMWorld int Collection::touchRecordImp(const std::string& id) { int index = getIndex(id); - Record& record = mRecords.at(index); + Record& record = *mRecords.at(index); if (record.isDeleted()) { throw std::runtime_error("attempt to touch deleted record"); @@ -302,7 +298,7 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type) { int index = cloneRecordImp(origin, destination, type); - mRecords.at(index).get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); } template @@ -317,7 +313,7 @@ namespace CSMWorld int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index).get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); return true; } @@ -344,15 +340,15 @@ namespace CSMWorld if (iter==mIndex.end()) { - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = record; - insertRecord (record2, getAppendIndex (id)); + insertRecord (std::move(record2), getAppendIndex (id)); } else { - mRecords[iter->second].setModified (record); + mRecords[iter->second]->setModified (record); } } @@ -365,7 +361,7 @@ namespace CSMWorld template std::string Collection::getId (int index) const { - return IdAccessorT().getId (mRecords.at (index).get()); + return IdAccessorT().getId (mRecords.at (index)->get()); } template @@ -388,13 +384,13 @@ namespace CSMWorld template QVariant Collection::getData (int index, int column) const { - return mColumns.at (column)->get (mRecords.at (index)); + return mColumns.at (column)->get (*mRecords.at (index)); } template void Collection::setData (int index, int column, const QVariant& data) { - return mColumns.at (column)->set (mRecords.at (index), data); + return mColumns.at (column)->set (*mRecords.at (index), data); } template @@ -421,8 +417,8 @@ namespace CSMWorld template void Collection::merge() { - for (typename std::vector >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) - iter->merge(); + for (typename std::vector > >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) + (*iter)->merge(); purge(); } @@ -434,7 +430,7 @@ namespace CSMWorld while (i (mRecords.size())) { - if (mRecords[i].isErased()) + if (mRecords[i]->isErased()) removeRows (i, 1); else ++i; @@ -475,15 +471,15 @@ namespace CSMWorld IdAccessorT().setId(record, id); record.blank(); - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = record; - insertRecord (record2, getAppendIndex (id, type), type); + insertRecord (std::move(record2), getAppendIndex (id, type), type); } template - int Collection::searchId (const std::string& id) const + int Collection::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase(id); @@ -496,18 +492,19 @@ namespace CSMWorld } template - void Collection::replace (int index, const RecordBase& record) + void Collection::replace (int index, std::unique_ptr record) { - mRecords.at (index) = dynamic_cast&> (record); + std::unique_ptr > tmp(static_cast*>(record.release())); + mRecords.at (index) = std::move(tmp); } template - void Collection::appendRecord (const RecordBase& record, + void Collection::appendRecord (std::unique_ptr record, UniversalId::Type type) { - insertRecord (record, - getAppendIndex (IdAccessorT().getId ( - dynamic_cast&> (record).get()), type), type); + int index = + getAppendIndex(IdAccessorT().getId(static_cast*>(record.get())->get()), type); + insertRecord (std::move(record), index, type); } template @@ -525,8 +522,8 @@ namespace CSMWorld for (typename std::map::const_iterator iter = mIndex.begin(); iter!=mIndex.end(); ++iter) { - if (listDeleted || !mRecords[iter->second].isDeleted()) - ids.push_back (IdAccessorT().getId (mRecords[iter->second].get())); + if (listDeleted || !mRecords[iter->second]->isDeleted()) + ids.push_back (IdAccessorT().getId (mRecords[iter->second]->get())); } return ids; @@ -536,46 +533,52 @@ namespace CSMWorld const Record& Collection::getRecord (const std::string& id) const { int index = getIndex (id); - return mRecords.at (index); + return *mRecords.at (index); } template const Record& Collection::getRecord (int index) const { - return mRecords.at (index); + return *mRecords.at (index); } template - void Collection::insertRecord (const RecordBase& record, int index, + void Collection::insertRecord (std::unique_ptr record, int index, UniversalId::Type type) { - if (index<0 || index>static_cast (mRecords.size())) + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) throw std::runtime_error ("index out of range"); - const Record& record2 = dynamic_cast&> (record); + std::unique_ptr > record2(static_cast*>(record.release())); + std::string lowerId = Misc::StringUtils::lowerCase(IdAccessorT().getId(record2->get())); - mRecords.insert (mRecords.begin()+index, record2); + if (index == size) + mRecords.push_back (std::move(record2)); + else + mRecords.insert (mRecords.begin()+index, std::move(record2)); - if (index (mRecords.size())-1) + if (index < size-1) { - for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); - ++iter) - if (iter->second>=index) - ++(iter->second); + for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); ++iter) + { + if (iter->second >= index) + ++(iter->second); + } } - mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (IdAccessorT().getId ( - record2.get())), index)); + mIndex.insert (std::make_pair (lowerId, index)); } template - void Collection::setRecord (int index, const Record& record) + void Collection::setRecord (int index, + std::unique_ptr > record) { - if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index).get()))!= - Misc::StringUtils::lowerCase (IdAccessorT().getId (record.get()))) + if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index)->get())) != + Misc::StringUtils::lowerCase (IdAccessorT().getId (record->get()))) throw std::runtime_error ("attempt to change the ID of a record"); - mRecords.at (index) = record; + mRecords.at (index) = std::move(record); } template diff --git a/apps/opencs/model/world/collectionbase.cpp b/apps/opencs/model/world/collectionbase.cpp index 6134dc1727..f20fc643e2 100644 --- a/apps/opencs/model/world/collectionbase.cpp +++ b/apps/opencs/model/world/collectionbase.cpp @@ -8,6 +8,11 @@ CSMWorld::CollectionBase::CollectionBase() {} CSMWorld::CollectionBase::~CollectionBase() {} +int CSMWorld::CollectionBase::getInsertIndex (const std::string& id, UniversalId::Type type, RecordBase *record) const +{ + return getAppendIndex(id, type); +} + int CSMWorld::CollectionBase::searchColumnIndex (Columns::ColumnId id) const { int columns = getColumns(); diff --git a/apps/opencs/model/world/collectionbase.hpp b/apps/opencs/model/world/collectionbase.hpp index bac790c5d0..be6131ee52 100644 --- a/apps/opencs/model/world/collectionbase.hpp +++ b/apps/opencs/model/world/collectionbase.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "universalid.hpp" #include "columns.hpp" @@ -60,17 +62,17 @@ namespace CSMWorld UniversalId::Type type = UniversalId::Type_None) = 0; ///< \param type Will be ignored, unless the collection supports multiple record types - virtual int searchId (const std::string& id) const = 0; + virtual int searchId(std::string_view id) const = 0; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - virtual void replace (int index, const RecordBase& record) = 0; + virtual void replace (int index, std::unique_ptr record) = 0; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. ///< \param type Will be ignored, unless the collection supports multiple record types - virtual void appendRecord (const RecordBase& record, + virtual void appendRecord (std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) = 0; ///< If the record type does not match, an exception is thrown. @@ -99,6 +101,12 @@ namespace CSMWorld /// /// \return Success? + virtual int getInsertIndex (const std::string& id, + UniversalId::Type type = UniversalId::Type_None, + RecordBase *record = nullptr) const; + ///< Works like getAppendIndex unless an overloaded method uses the record pointer + /// to get additional info about the record that results in an alternative index. + int searchColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, -1 is returned. diff --git a/apps/opencs/model/world/columnbase.cpp b/apps/opencs/model/world/columnbase.cpp index cf333c1b1a..6f7bb12b3c 100644 --- a/apps/opencs/model/world/columnbase.cpp +++ b/apps/opencs/model/world/columnbase.cpp @@ -104,7 +104,8 @@ bool CSMWorld::ColumnBase::isId (Display display) bool CSMWorld::ColumnBase::isText (Display display) { return display==Display_String || display==Display_LongString || - display==Display_String32 || display==Display_LongString256; + display==Display_String32 || display==Display_String64 || + display==Display_LongString256; } bool CSMWorld::ColumnBase::isScript (Display display) diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index 6dc58bd634..688cdffb74 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include "record.hpp" @@ -135,6 +134,7 @@ namespace CSMWorld Display_InfoCondVar, Display_InfoCondComp, Display_String32, + Display_String64, Display_LongString256, Display_BookType, Display_BloodType, diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index bec5008d35..78c7bc02d5 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -1,7 +1,6 @@ #include "columnimp.hpp" #include -#include namespace CSMWorld { @@ -52,7 +51,7 @@ namespace CSMWorld QVariant LandPluginIndexColumn::get(const Record& record) const { - return record.get().mPlugin; + return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index c69ca1d842..2d69c664d4 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -6,12 +6,11 @@ #include #include -#include #include -#include -#include -#include +#include +#include +#include #include "columnbase.hpp" #include "columns.hpp" @@ -334,7 +333,8 @@ namespace CSMWorld template struct NameColumn : public Column { - NameColumn() : Column (Columns::ColumnId_Name, ColumnBase::Display_String) {} + NameColumn(ColumnBase::Display display = ColumnBase::Display_String) + : Column (Columns::ColumnId_Name, display) {} QVariant get (const Record& record) const override { @@ -2254,7 +2254,7 @@ namespace CSMWorld QVariant get (const Record& record) const override { - const std::string *string = 0; + const std::string *string = nullptr; switch (this->mColumnId) { @@ -2272,7 +2272,7 @@ namespace CSMWorld void set (Record& record, const QVariant& data) override { - std::string *string = 0; + std::string *string = nullptr; ESXRecordT record2 = record.get(); @@ -2312,7 +2312,7 @@ namespace CSMWorld QVariant get (const Record& record) const override { - const std::string *string = 0; + const std::string *string = nullptr; switch (this->mColumnId) { @@ -2330,7 +2330,7 @@ namespace CSMWorld void set (Record& record, const QVariant& data) override { - std::string *string = 0; + std::string *string = nullptr; ESXRecordT record2 = record.get(); diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index eaea66c2f9..deaf5823c3 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -255,7 +255,7 @@ namespace CSMWorld { ColumnId_AiWanderDist, "Wander Dist" }, { ColumnId_AiDuration, "Ai Duration" }, { ColumnId_AiWanderToD, "Wander ToD" }, - { ColumnId_AiWanderRepeat, "Wander Repeat" }, + { ColumnId_AiWanderRepeat, "Ai Repeat" }, { ColumnId_AiActivateName, "Activate" }, { ColumnId_AiTargetId, "Target ID" }, { ColumnId_AiTargetCell, "Target Cell" }, @@ -269,7 +269,7 @@ namespace CSMWorld { ColumnId_LevelledItemId,"Levelled Item" }, { ColumnId_LevelledItemLevel,"Item Level" }, { ColumnId_LevelledItemType, "Calculate all levels <= player" }, - { ColumnId_LevelledItemTypeEach, "Select a new item each instance" }, + { ColumnId_LevelledItemTypeEach, "Select a new item for each instance" }, { ColumnId_LevelledItemChanceNone, "Chance None" }, { ColumnId_PowerList, "Powers" }, @@ -294,7 +294,6 @@ namespace CSMWorld { ColumnId_NpcReputation, "Reputation" }, { ColumnId_NpcRank, "NPC Rank" }, { ColumnId_Gold, "Gold" }, - { ColumnId_NpcPersistence, "Persistent" }, { ColumnId_RaceAttributes, "Race Attributes" }, { ColumnId_Male, "Male" }, @@ -371,6 +370,9 @@ namespace CSMWorld { ColumnId_Skill6, "Skill 6" }, { ColumnId_Skill7, "Skill 7" }, + { ColumnId_Persistent, "Persistent" }, + { ColumnId_Blocked, "Blocked" }, + { -1, 0 } // end marker }; } @@ -390,7 +392,7 @@ int CSMWorld::Columns::getId (const std::string& name) std::string name2 = Misc::StringUtils::lowerCase (name); for (int i=0; sNames[i].mName; ++i) - if (Misc::StringUtils::ciEqual(sNames[i].mName, name2)) + if (Misc::StringUtils::ciEqual(std::string_view(sNames[i].mName), name2)) return sNames[i].mId; return -1; @@ -403,7 +405,7 @@ namespace "Combat", "Magic", "Stealth", 0 }; - // see ESM::Attribute::AttributeID in + // see ESM::Attribute::AttributeID in static const char *sAttributes[] = { "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", @@ -496,7 +498,7 @@ namespace "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 }; - // impact from magic effects, see ESM::Skill::SkillEnum in + // impact from magic effects, see ESM::Skill::SkillEnum in static const char *sSkills[] = { "Block", "Armorer", "MediumArmor", "HeavyArmor", "BluntWeapon", @@ -507,13 +509,13 @@ namespace "Speechcraft", "HandToHand", 0 }; - // range of magic effects, see ESM::RangeType in + // range of magic effects, see ESM::RangeType in static const char *sEffectRange[] = { "Self", "Touch", "Target", 0 }; - // magic effect names, see ESM::MagicEffect::Effects in + // magic effect names, see ESM::MagicEffect::Effects in static const char *sEffectId[] = { "WaterBreathing", "SwiftSwim", "WaterWalking", "Shield", "FireShield", @@ -547,7 +549,7 @@ namespace "SummonBonewolf", "SummonCreature04", "SummonCreature05", 0 }; - // see ESM::PartReferenceType in + // see ESM::PartReferenceType in static const char *sPartRefType[] = { "Head", "Hair", "Neck", "Cuirass", "Groin", @@ -558,7 +560,7 @@ namespace "Weapon", "Tail", 0 }; - // see the enums in + // see the enums in static const char *sAiPackageType[] = { "AI Wander", "AI Travel", "AI Follow", "AI Escort", "AI Activate", 0 diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index c85eaac5f1..8cf02b46ac 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -280,7 +280,7 @@ namespace CSMWorld ColumnId_NpcReputation = 258, ColumnId_NpcRank = 259, ColumnId_Gold = 260, - ColumnId_NpcPersistence = 261, + // unused ColumnId_RaceAttributes = 262, ColumnId_Male = 263, @@ -343,6 +343,9 @@ namespace CSMWorld ColumnId_FactionAttrib1 = 311, ColumnId_FactionAttrib2 = 312, + ColumnId_Persistent = 313, + ColumnId_Blocked = 314, + // 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/commanddispatcher.cpp b/apps/opencs/model/world/commanddispatcher.cpp index 36b3ba2e00..dc815f43f3 100644 --- a/apps/opencs/model/world/commanddispatcher.cpp +++ b/apps/opencs/model/world/commanddispatcher.cpp @@ -141,7 +141,6 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons std::unique_ptr modifyCell; - std::unique_ptr modifyDataRefNum; int columnId = model->data (index, ColumnBase::Role_ColumnId).toInt(); @@ -170,29 +169,20 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons if (cellId.find ('#')!=std::string::npos) { - // Need to recalculate the cell and (if necessary) clear the instance's refNum - modifyCell.reset (new UpdateCellCommand (model2, row)); - - // Not sure which model this should be applied to - int refNumColumn = model2.searchColumnIndex (Columns::ColumnId_RefNum); - - if (refNumColumn!=-1) - modifyDataRefNum.reset (new ModifyCommand(*model, model->index(row, refNumColumn), 0)); + // Need to recalculate the cell + modifyCell = std::make_unique(model2, row); } } } } - std::unique_ptr modifyData ( - new CSMWorld::ModifyCommand (*model, index, new_)); + auto modifyData = std::make_unique(*model, index, new_); if (modifyCell.get()) { CommandMacro macro (mDocument.getUndoStack()); macro.push (modifyData.release()); macro.push (modifyCell.release()); - if (modifyDataRefNum.get()) - macro.push (modifyDataRefNum.release()); } else mDocument.getUndoStack().push (modifyData.release()); diff --git a/apps/opencs/model/world/commanddispatcher.hpp b/apps/opencs/model/world/commanddispatcher.hpp index 1d29e48c19..538fd7f188 100644 --- a/apps/opencs/model/world/commanddispatcher.hpp +++ b/apps/opencs/model/world/commanddispatcher.hpp @@ -34,7 +34,7 @@ namespace CSMWorld public: CommandDispatcher (CSMDoc::Document& document, const CSMWorld::UniversalId& id, - QObject *parent = 0); + QObject *parent = nullptr); ///< \param id ID of the table the commands should operate on primarily. void setEditLock (bool locked); diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index e9682d7c9a..a264ebef7f 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -24,11 +24,11 @@ CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUnd , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mTable.getRecord(mId).clone()); } void CSMWorld::TouchCommand::redo() { + mOld.reset(mTable.getRecord(mId).clone().get()); mChanged = mTable.touchRecord(mId); } @@ -36,7 +36,7 @@ void CSMWorld::TouchCommand::undo() { if (mChanged) { - mTable.setRecord(mId, *mOld); + mTable.setRecord(mId, std::move(mOld)); mChanged = false; } } @@ -76,6 +76,7 @@ void CSMWorld::ImportLandTexturesCommand::redo() } std::vector oldTextures; + oldTextures.reserve(texIndices.size()); for (int index : texIndices) { oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); @@ -158,7 +159,6 @@ CSMWorld::TouchLandCommand::TouchLandCommand(IdTable& landTable, IdTable& ltexTa , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mLands.getRecord(mId).clone()); } const std::string& CSMWorld::TouchLandCommand::getOriginId() const @@ -174,13 +174,14 @@ const std::string& CSMWorld::TouchLandCommand::getDestinationId() const void CSMWorld::TouchLandCommand::onRedo() { mChanged = mLands.touchRecord(mId); + if (mChanged) mOld.reset(mLands.getRecord(mId).clone().get()); } void CSMWorld::TouchLandCommand::onUndo() { if (mChanged) { - mLands.setRecord(mId, *mOld); + mLands.setRecord(mId, std::move(mOld)); mChanged = false; } } @@ -189,13 +190,16 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (&model), mIndex (index), mNew (new_), mHasRecordState(false), mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) { - if (QAbstractProxyModel *proxy = dynamic_cast (&model)) + if (QAbstractProxyModel *proxy = dynamic_cast (mModel)) { // Replace proxy with actual model - mIndex = proxy->mapToSource (index); + mIndex = proxy->mapToSource (mIndex); mModel = proxy->sourceModel(); } +} +void CSMWorld::ModifyCommand::redo() +{ if (mIndex.parent().isValid()) { CSMWorld::IdTree* tree = &dynamic_cast(*mModel); @@ -222,10 +226,7 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI mRecordStateIndex = table->index(rowIndex, stateColumnIndex); mOldRecordState = static_cast(table->data(mRecordStateIndex).toInt()); } -} -void CSMWorld::ModifyCommand::redo() -{ mOld = mModel->data (mIndex, Qt::EditRole); mModel->setData (mIndex, mNew); } @@ -290,20 +291,19 @@ void CSMWorld::CreateCommand::undo() } CSMWorld::RevertCommand::RevertCommand (IdTable& model, const std::string& id, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (0) +: QUndoCommand (parent), mModel (model), mId (id), mOld(nullptr) { setText (("Revert record " + id).c_str()); - - mOld = model.getRecord (id).clone(); } CSMWorld::RevertCommand::~RevertCommand() { - delete mOld; } void CSMWorld::RevertCommand::redo() { + mOld = mModel.getRecord (mId).clone(); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); @@ -321,25 +321,24 @@ void CSMWorld::RevertCommand::redo() void CSMWorld::RevertCommand::undo() { - mModel.setRecord (mId, *mOld); + mModel.setRecord (mId, std::move(mOld)); } CSMWorld::DeleteCommand::DeleteCommand (IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (0), mType(type) +: QUndoCommand (parent), mModel (model), mId (id), mOld(nullptr), mType(type) { setText (("Delete record " + id).c_str()); - - mOld = model.getRecord (id).clone(); } CSMWorld::DeleteCommand::~DeleteCommand() { - delete mOld; } void CSMWorld::DeleteCommand::redo() { + mOld = mModel.getRecord (mId).clone(); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); @@ -357,7 +356,7 @@ void CSMWorld::DeleteCommand::redo() void CSMWorld::DeleteCommand::undo() { - mModel.setRecord (mId, *mOld, mType); + mModel.setRecord (mId, std::move(mOld), mType); } @@ -397,6 +396,10 @@ void CSMWorld::CloneCommand::redo() { mModel.cloneRecord (mIdOrigin, mId, mType); applyModifications(); + for (auto& value : mOverrideValues) + { + mModel.setData(mModel.getModelIndex (mId, value.first), value.second); + } } void CSMWorld::CloneCommand::undo() @@ -404,6 +407,11 @@ void CSMWorld::CloneCommand::undo() mModel.removeRow (mModel.getModelIndex (mId, 0).row()); } +void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value) +{ + mOverrideValues.emplace_back(std::make_pair(column, value)); +} + CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent) : CreateCommand(model, id, parent) { @@ -414,18 +422,19 @@ void CSMWorld::CreatePathgridCommand::redo() { CreateCommand::redo(); - Record record = static_cast& >(mModel.getRecord(mId)); - record.get().blank(); - record.get().mCell = mId; + std::unique_ptr > record + = std::make_unique >(static_cast& >(mModel.getRecord(mId))); + record->get().blank(); + record->get().mCell = mId; std::pair coords = CellCoordinates::fromId(mId); if (coords.second) { - record.get().mData.mX = coords.first.getX(); - record.get().mData.mY = coords.first.getY(); + record->get().mData.mX = coords.first.getX(); + record->get().mData.mY = coords.first.getY(); } - mModel.setRecord(mId, record, mType); + mModel.setRecord(mId, std::move(record), mType); } CSMWorld::UpdateCellCommand::UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent) @@ -489,8 +498,8 @@ CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTree& model, void CSMWorld::DeleteNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.removeRows (mNestedRow, 1, parentIndex); mModifyParentCommand->redo(); + mModel.removeRows (mNestedRow, 1, parentIndex); } @@ -520,8 +529,8 @@ CSMWorld::AddNestedCommand::AddNestedCommand(IdTree& model, const std::string& i void CSMWorld::AddNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.addNestedRow (parentIndex, mNewRow); mModifyParentCommand->redo(); + mModel.addNestedRow (parentIndex, mNewRow); } void CSMWorld::AddNestedCommand::undo() diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index 88af32636b..4b619d11ed 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -16,7 +16,6 @@ #include "universalid.hpp" #include "nestedtablewrapper.hpp" -class QModelIndex; class QAbstractItemModel; namespace CSMWorld @@ -140,7 +139,7 @@ namespace CSMWorld public: ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, - QUndoCommand *parent = 0); + QUndoCommand *parent = nullptr); void redo() override; @@ -167,7 +166,7 @@ namespace CSMWorld public: - CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); + CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); void setType (UniversalId::Type type); @@ -183,24 +182,27 @@ namespace CSMWorld class CloneCommand : public CreateCommand { std::string mIdOrigin; + std::vector> mOverrideValues; public: CloneCommand (IdTable& model, const std::string& idOrigin, const std::string& IdDestination, const UniversalId::Type type, - QUndoCommand* parent = 0); + QUndoCommand* parent = nullptr); void redo() override; void undo() override; + + void setOverrideValue(int column, QVariant value); }; class RevertCommand : public QUndoCommand { IdTable& mModel; std::string mId; - RecordBase *mOld; + std::unique_ptr mOld; // not implemented RevertCommand (const RevertCommand&); @@ -208,7 +210,7 @@ namespace CSMWorld public: - RevertCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); + RevertCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); virtual ~RevertCommand(); @@ -221,7 +223,7 @@ namespace CSMWorld { IdTable& mModel; std::string mId; - RecordBase *mOld; + std::unique_ptr mOld; UniversalId::Type mType; // not implemented @@ -231,7 +233,7 @@ namespace CSMWorld public: DeleteCommand (IdTable& model, const std::string& id, - UniversalId::Type type = UniversalId::Type_None, QUndoCommand *parent = 0); + UniversalId::Type type = UniversalId::Type_None, QUndoCommand *parent = nullptr); virtual ~DeleteCommand(); @@ -259,7 +261,7 @@ namespace CSMWorld { public: - CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent = 0); + CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); void redo() override; }; @@ -279,7 +281,7 @@ namespace CSMWorld public: - UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent = 0); + UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent = nullptr); void redo() override; @@ -316,7 +318,7 @@ namespace CSMWorld public: - DeleteNestedCommand (IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = 0); + DeleteNestedCommand (IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); void redo() override; @@ -338,7 +340,7 @@ namespace CSMWorld public: - AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = 0); + AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); void redo() override; diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 23720a99a6..247900f407 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -5,10 +5,9 @@ #include -#include +#include #include -#include -#include +#include #include #include @@ -68,14 +67,14 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives, const boost::filesystem::path& resDir) : mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), - mReader (0), mDialogue (0), mReaderIndex(1), + mReader (nullptr), mDialogue (nullptr), mReaderIndex(1), mFsStrict(fsStrict), mDataPaths(dataPaths), mArchives(archives) { - mVFS.reset(new VFS::Manager(mFsStrict)); + mVFS = std::make_unique(mFsStrict); VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths, !mFsStrict), mArchives, true); mResourcesManager.setVFS(mVFS.get()); - mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); + mResourceSystem = std::make_unique(mVFS.get()); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); Shader::ShaderManager::DefineMap shadowDefines = SceneUtil::ShadowManager::getShadowsDisabledDefines(); @@ -83,6 +82,9 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat defines["clamp"] = "1"; // Clamp lighting defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; + defines["lightingModel"] = "0"; + defines["reverseZ"] = "0"; + defines["refraction_enabled"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); @@ -129,7 +131,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat mFactions.addColumn (new StringIdColumn); mFactions.addColumn (new RecordStateColumn); mFactions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Faction)); - mFactions.addColumn (new NameColumn); + mFactions.addColumn (new NameColumn(ColumnBase::Display_String32)); mFactions.addColumn (new AttributesColumn (0)); mFactions.addColumn (new AttributesColumn (1)); mFactions.addColumn (new HiddenColumn); @@ -338,7 +340,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat mCells.addColumn (new StringIdColumn); mCells.addColumn (new RecordStateColumn); mCells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Cell)); - mCells.addColumn (new NameColumn); + mCells.addColumn (new NameColumn(ColumnBase::Display_String64)); mCells.addColumn (new FlagColumn (Columns::ColumnId_SleepForbidden, ESM::Cell::NoSleep)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); @@ -600,7 +602,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat UniversalId::Type_Video); addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData); - mActorAdapter.reset(new ActorAdapter(*this)); + mActorAdapter = std::make_unique(*this); mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files } @@ -916,8 +918,8 @@ const CSMWorld::MetaData& CSMWorld::Data::getMetaData() const void CSMWorld::Data::setMetaData (const MetaData& metaData) { - Record record (RecordBase::State_ModifiedOnly, 0, &metaData); - mMetaData.setRecord (0, record); + mMetaData.setRecord (0, std::make_unique >( + Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) @@ -932,7 +934,7 @@ QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& // construction of the ESX data where no update signals are available. if (id.getType()==UniversalId::Type_RegionMap) { - RegionMap *table = 0; + RegionMap *table = nullptr; addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, false); return table; } @@ -957,14 +959,33 @@ void CSMWorld::Data::merge() mGlobals.merge(); } +int CSMWorld::Data::getTotalRecords (const std::vector& files) +{ + int records = 0; + + std::unique_ptr reader = std::make_unique(); + + for (unsigned int i = 0; i < files.size(); ++i) + { + if (!boost::filesystem::exists(files[i])) + continue; + + reader->open(files[i].string()); + records += reader->getRecordCount(); + reader->close(); + } + + return records; +} + int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) { // 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 = 0; + mReader = nullptr; - mDialogue = 0; + mDialogue = nullptr; mReader = new ESM::ESMReader; mReader->setEncoder (&mEncoder); @@ -982,21 +1003,8 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base metaData.mId = "sys::meta"; metaData.load (*mReader); - mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, 0, &metaData)); - } - - // Fix uninitialized master data index - for (std::vector::const_iterator masterData = mReader->getGameFiles().begin(); - masterData != mReader->getGameFiles().end(); ++masterData) - { - std::map::iterator nameResult = mContentFileNames.find(masterData->name); - if (nameResult != mContentFileNames.end()) - { - ESM::Header::MasterData& hackedMasterData = const_cast(*masterData); - - - hackedMasterData.index = nameResult->second; - } + mMetaData.setRecord (0, std::make_unique >( + Record (RecordBase::State_ModifiedOnly, nullptr, &metaData))); } return mReader->getRecordCount(); @@ -1024,10 +1032,11 @@ void CSMWorld::Data::loadFallbackEntries() ESM::Static newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Static); + newMarker.mRecordFlags = 0; + auto record = std::make_unique>(); + record->mBase = newMarker; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord (std::move(record), CSMWorld::UniversalId::Type_Static); } } @@ -1038,10 +1047,11 @@ void CSMWorld::Data::loadFallbackEntries() ESM::Door newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Door); + newMarker.mRecordFlags = 0; + auto record = std::make_unique>(); + record->mBase = newMarker; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord (std::move(record), CSMWorld::UniversalId::Type_Door); } } } @@ -1064,9 +1074,9 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) else delete mReader; - mReader = 0; + mReader = nullptr; - mDialogue = 0; + mDialogue = nullptr; loadFallbackEntries(); @@ -1078,7 +1088,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) bool unhandledRecord = false; - switch (n.intval) + switch (n.toInt()) { case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; case ESM::REC_GMST: mGmsts.load (*mReader, mBase); break; @@ -1151,7 +1161,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) if (isDeleted) { // record vector can be shuffled around which would make pointer to record invalid - mDialogue = 0; + mDialogue = nullptr; if (mJournals.tryDelete (record.mId)) { diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 51f9211625..a337b74879 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -9,25 +9,25 @@ #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 +#include +#include +#include +#include +#include #include @@ -98,13 +98,13 @@ namespace CSMWorld NestedIdCollection mEnchantments; IdCollection mBodyParts; IdCollection mMagicEffects; - SubCellCollection mPathgrids; IdCollection mDebugProfiles; IdCollection mSoundGens; IdCollection mStartScripts; NestedInfoCollection mTopicInfos; InfoCollection mJournalInfos; NestedIdCollection mCells; + SubCellCollection mPathgrids; IdCollection mLandTextures; IdCollection mLand; RefIdCollection mReferenceables; @@ -118,7 +118,7 @@ namespace CSMWorld const ESM::Dialogue *mDialogue; // last loaded dialogue bool mBase; bool mProject; - std::map > mRefLoadCache; + std::map > mRefLoadCache; int mReaderIndex; bool mFsStrict; @@ -152,7 +152,7 @@ namespace CSMWorld Data (ToUTF8::FromType encoding, bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives, const boost::filesystem::path& resDir); - virtual ~Data(); + ~Data() override; const VFS::Manager* getVFS() const; @@ -292,6 +292,8 @@ namespace CSMWorld void merge(); ///< Merge modified into base. + int getTotalRecords (const std::vector& files); // for better loading bar + int startLoading (const boost::filesystem::path& path, bool base, bool project); ///< Begin merging content of a file into base or modified. /// diff --git a/apps/opencs/model/world/idcollection.cpp b/apps/opencs/model/world/idcollection.cpp new file mode 100644 index 0000000000..802ffe2487 --- /dev/null +++ b/apps/opencs/model/world/idcollection.cpp @@ -0,0 +1,43 @@ +#include "idcollection.hpp" + +namespace CSMWorld +{ + template<> + int IdCollection >::load (ESM::ESMReader& reader, bool base) + { + Pathgrid record; + bool isDeleted = false; + + loadRecord (record, reader, isDeleted); + + std::string id = IdAccessor().getId (record); + int index = this->searchId (id); + + if (record.mPoints.empty() || record.mEdges.empty()) + isDeleted = true; + + if (isDeleted) + { + if (index==-1) + { + // deleting a record that does not exist + // ignore it for now + /// \todo report the problem to the user + return -1; + } + + if (base) + { + this->removeRows (index, 1); + return -1; + } + + auto baseRecord = std::make_unique>(this->getRecord(index)); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); + return index; + } + + return load (record, base, index); + } +} diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 7849aab926..d246dc6852 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -1,10 +1,11 @@ #ifndef CSM_WOLRD_IDCOLLECTION_H #define CSM_WOLRD_IDCOLLECTION_H -#include +#include #include "collection.hpp" #include "land.hpp" +#include "pathgrid.hpp" namespace CSMWorld { @@ -83,9 +84,9 @@ namespace CSMWorld return -1; } - Record baseRecord = this->getRecord (index); - baseRecord.mState = RecordBase::State_Deleted; - this->setRecord (index, baseRecord); + auto baseRecord = std::make_unique>(this->getRecord(index)); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); return index; } @@ -96,30 +97,30 @@ namespace CSMWorld int IdCollection::load (const ESXRecordT& record, bool base, int index) { - if (index==-2) + if (index==-2) // index unknown index = this->searchId (IdAccessorT().getId (record)); if (index==-1) { // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; + auto record2 = std::make_unique>(); + record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record2->mBase : record2->mModified) = record; index = this->getSize(); - this->appendRecord (record2); + this->appendRecord(std::move(record2)); } else { // old record - Record record2 = Collection::getRecord (index); + auto record2 = std::make_unique>(Collection::getRecord(index)); if (base) - record2.mBase = record; + record2->mBase = record; else - record2.setModified (record); + record2->setModified(record); - this->setRecord (index, record2); + this->setRecord(index, std::move(record2)); } return index; @@ -133,7 +134,7 @@ namespace CSMWorld if (index==-1) return false; - Record record = Collection::getRecord (index); + const Record& record = Collection::getRecord (index); if (record.isDeleted()) return false; @@ -144,12 +145,16 @@ namespace CSMWorld } else { - record.mState = RecordBase::State_Deleted; - this->setRecord (index, record); + auto record2 = std::make_unique>(Collection::getRecord(index)); + record2->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(record2)); } return true; } + + template<> + int IdCollection >::load(ESM::ESMReader& reader, bool base); } #endif diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 27d60ae98d..75213aab84 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -1,13 +1,13 @@ #include "idtable.hpp" #include -#include #include #include #include #include -#include +#include +#include #include "collectionbase.hpp" #include "columnbase.hpp" @@ -123,6 +123,14 @@ Qt::ItemFlags CSMWorld::IdTable::flags (const QModelIndex & index) const if (mIdCollection->getColumn (index.column()).isUserEditable()) flags |= Qt::ItemIsEditable; + int blockedColumn = searchColumnIndex(Columns::ColumnId_Blocked); + if (blockedColumn != -1 && blockedColumn != index.column()) + { + bool isBlocked = mIdCollection->getData(index.row(), blockedColumn).toInt(); + if (isBlocked) + flags = Qt::ItemIsSelectable; // not enabled (to grey out) + } + return flags; } @@ -191,7 +199,7 @@ void CSMWorld::IdTable::cloneRecord(const std::string& origin, const std::string& destination, CSMWorld::UniversalId::Type type) { - int index = mIdCollection->getAppendIndex (destination); + int index = mIdCollection->getAppendIndex (destination, type); beginInsertRows (QModelIndex(), index, index); mIdCollection->cloneRecord(origin, destination, type); @@ -228,23 +236,30 @@ QModelIndex CSMWorld::IdTable::getModelIndex (const std::string& id, int column) return QModelIndex(); } -void CSMWorld::IdTable::setRecord (const std::string& id, const RecordBase& record, CSMWorld::UniversalId::Type type) +void CSMWorld::IdTable::setRecord (const std::string& id, + std::unique_ptr record, CSMWorld::UniversalId::Type type) { int index = mIdCollection->searchId (id); if (index==-1) { - index = mIdCollection->getAppendIndex (id, type); + // For info records, appendRecord may use a different index than the one returned by + // getAppendIndex (because of prev/next links). This can result in the display not + // updating correctly after an undo + // + // Use an alternative method to get the correct index. For non-Info records the + // record pointer is ignored and internally calls getAppendIndex. + int index2 = mIdCollection->getInsertIndex (id, type, record.get()); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows (QModelIndex(), index2, index2); - mIdCollection->appendRecord (record, type); + mIdCollection->appendRecord (std::move(record), type); endInsertRows(); } else { - mIdCollection->replace (index, record); + mIdCollection->replace (index, std::move(record)); emit dataChanged (CSMWorld::IdTable::index (index, 0), CSMWorld::IdTable::index (index, mIdCollection->getColumns()-1)); } @@ -270,7 +285,7 @@ void CSMWorld::IdTable::reorderRows (int baseIndex, const std::vector& newO if (!newOrder.empty()) if (mIdCollection->reorderRows (baseIndex, newOrder)) emit dataChanged (index (baseIndex, 0), - index (baseIndex+newOrder.size()-1, mIdCollection->getColumns()-1)); + index (baseIndex+static_cast(newOrder.size())-1, mIdCollection->getColumns()-1)); } std::pair CSMWorld::IdTable::view (int row) const @@ -339,8 +354,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import for (int i = 0; i < idCollection()->getSize(); ++i) { auto& record = static_cast&>(idCollection()->getRecord(i)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture); if (record.isModified()) reverseLookupMap.emplace(texture, idCollection()->getId(i)); } @@ -361,8 +375,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import // Look for a pre-existing record auto& record = static_cast&>(idCollection()->getRecord(oldRow)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture); auto searchIt = reverseLookupMap.find(texture); if (searchIt != reverseLookupMap.end()) { diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 6b7b8d3182..c38e1b5f9c 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -2,6 +2,7 @@ #define CSM_WOLRD_IDTABLE_H #include +#include #include "idtablebase.hpp" #include "universalid.hpp" @@ -67,7 +68,7 @@ namespace CSMWorld QModelIndex getModelIndex (const std::string& id, int column) const override; - void setRecord (const std::string& id, const RecordBase& record, + void setRecord (const std::string& id, std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None); ///< Add record or overwrite existing record. diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index 3e24f8d12e..9d9dd7db3d 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -121,8 +121,11 @@ QString CSMWorld::IdTableProxyModel::getRecordId(int sourceRow) const void CSMWorld::IdTableProxyModel::refreshFilter() { - updateColumnMap(); - invalidateFilter(); + if (mFilter) + { + updateColumnMap(); + invalidateFilter(); + } } void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 14d7057924..7e0563834a 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -35,7 +35,7 @@ namespace CSMWorld public: - IdTableProxyModel (QObject *parent = 0); + IdTableProxyModel (QObject *parent = nullptr); virtual QModelIndex getModelIndex (const std::string& id, int column) const; diff --git a/apps/opencs/model/world/idtree.cpp b/apps/opencs/model/world/idtree.cpp index a8dfacb01d..1e3398bbb2 100644 --- a/apps/opencs/model/world/idtree.cpp +++ b/apps/opencs/model/world/idtree.cpp @@ -201,7 +201,7 @@ QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const const std::pair& address(unfoldIndexAddress(id)); if (address.first >= this->rowCount() || address.second >= this->columnCount()) - throw "Parent index is not present in the model"; + throw std::logic_error("Parent index is not present in the model"); return createIndex(address.first, address.second); } @@ -216,7 +216,7 @@ unsigned int CSMWorld::IdTree::foldIndexAddress (const QModelIndex& index) const std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) const { if (id == 0) - throw "Attempt to unfold index id of the top level data cell"; + throw std::runtime_error("Attempt to unfold index id of the top level data cell"); --id; int row = id / this->columnCount(); diff --git a/apps/opencs/model/world/idtree.hpp b/apps/opencs/model/world/idtree.hpp index c525a60b88..294645a151 100644 --- a/apps/opencs/model/world/idtree.hpp +++ b/apps/opencs/model/world/idtree.hpp @@ -42,7 +42,7 @@ namespace CSMWorld IdTree (NestedCollection *nestedCollection, CollectionBase *idCollection, unsigned int features = 0); ///< The ownerships of \a nestedCollecton and \a idCollection are not transferred. - virtual ~IdTree(); + ~IdTree() override; int rowCount (const QModelIndex & parent = QModelIndex()) const override; diff --git a/apps/opencs/model/world/info.hpp b/apps/opencs/model/world/info.hpp index 1bcb2dc2d0..9405c002c7 100644 --- a/apps/opencs/model/world/info.hpp +++ b/apps/opencs/model/world/info.hpp @@ -1,7 +1,7 @@ #ifndef CSM_WOLRD_INFO_H #define CSM_WOLRD_INFO_H -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/infocollection.cpp b/apps/opencs/model/world/infocollection.cpp index be73aece6f..8ff11009b1 100644 --- a/apps/opencs/model/world/infocollection.cpp +++ b/apps/opencs/model/world/infocollection.cpp @@ -2,87 +2,172 @@ #include #include +#include -#include -#include +#include +#include #include -void CSMWorld::InfoCollection::load (const Info& record, bool base) +namespace CSMWorld { - int index = searchId (record.mId); + template<> + void Collection >::removeRows (int index, int count) + { + mRecords.erase(mRecords.begin()+index, mRecords.begin()+index+count); - if (index==-1) + // index map is updated in InfoCollection::removeRows() + } + + template<> + void Collection >::insertRecord (std::unique_ptr record, + int index, UniversalId::Type type) { - // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); - std::string topic = Misc::StringUtils::lowerCase (record2.get().mTopicId); + std::unique_ptr > record2(static_cast*>(record.release())); - if (!record2.get().mPrev.empty()) - { - index = getInfoIndex (record2.get().mPrev, topic); + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin()+index, std::move(record2)); - if (index!=-1) - ++index; - } + // index map is updated in InfoCollection::insertRecord() + } - if (index==-1 && !record2.get().mNext.empty()) + template<> + bool Collection >::reorderRowsImp (int baseIndex, + const std::vector& newOrder) + { + if (!newOrder.empty()) { - index = getInfoIndex (record2.get().mNext, topic); - } + int size = static_cast(newOrder.size()); - if (index==-1) - { - Range range = getTopicRange (topic); + // check that all indices are present + std::vector test(newOrder); + std::sort(test.begin(), test.end()); + if (*test.begin() != 0 || *--test.end() != size-1) + return false; + + // reorder records + std::vector > > buffer(size); - index = std::distance (getRecords().begin(), range.second); + // FIXME: BUG: undo does not remove modified flag + for (int i = 0; i < size; ++i) + { + buffer[newOrder[i]] = std::move(mRecords[baseIndex+i]); + if (buffer[newOrder[i]]) + buffer[newOrder[i]]->setModified(buffer[newOrder[i]]->get()); + } + + std::move(buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); + + // index map is updated in InfoCollection::reorderRows() } - insertRecord (record2, index); + return true; + } +} + +void CSMWorld::InfoCollection::load (const Info& record, bool base) +{ + int index = searchId (record.mId); + + if (index==-1) + { + // new record + auto record2 = std::make_unique>(); + record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record2->mBase : record2->mModified) = record; + + appendRecord(std::move(record2)); } else { // old record - Record record2 = getRecord (index); + auto record2 = std::make_unique>(getRecord(index)); if (base) - record2.mBase = record; + record2->mBase = record; else - record2.setModified (record); + record2->setModified (record); - setRecord (index, record2); + setRecord (index, std::move(record2)); } } -int CSMWorld::InfoCollection::getInfoIndex (const std::string& id, const std::string& topic) const +int CSMWorld::InfoCollection::getInfoIndex(std::string_view id, std::string_view topic) const { - std::string fullId = Misc::StringUtils::lowerCase (topic) + "#" + id; + // find the topic first + std::unordered_map > >::const_iterator iter + = mInfoIndex.find(Misc::StringUtils::lowerCase(topic)); - std::pair range = getTopicRange (topic); + if (iter == mInfoIndex.end()) + return -1; - for (; range.first!=range.second; ++range.first) - if (Misc::StringUtils::ciEqual(range.first->get().mId, fullId)) - return std::distance (getRecords().begin(), range.first); + // brute force loop + for (std::vector >::const_iterator it = iter->second.begin(); + it != iter->second.end(); ++it) + { + if (Misc::StringUtils::ciEqual(it->first, id)) + return it->second; + } return -1; } -int CSMWorld::InfoCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const +// Calling insertRecord() using index from getInsertIndex() needs to take into account of +// prev/next records; an example is deleting a record then undo +int CSMWorld::InfoCollection::getInsertIndex (const std::string& id, + UniversalId::Type type, RecordBase *record) const { - std::string::size_type separator = id.find_last_of ('#'); + if (record == nullptr) + { + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + id); - if (separator==std::string::npos) - throw std::runtime_error ("invalid info ID: " + id); + std::pair range = getTopicRange(id.substr(0, separator)); - std::pair range = getTopicRange (id.substr (0, separator)); + if (range.first == range.second) + return Collection >::getAppendIndex(id, type); - if (range.first==range.second) - return Collection >::getAppendIndex (id, type); + return std::distance(getRecords().begin(), range.second); + } + + int index = -1; + + const Info& info = static_cast*>(record)->get(); + std::string topic = info.mTopicId; + + // if the record has a prev, find its index value + if (!info.mPrev.empty()) + { + index = getInfoIndex(info.mPrev, topic); + + if (index != -1) + ++index; // if prev exists, set current index to one above prev + } - return std::distance (getRecords().begin(), range.second); + // if prev doesn't exist or not found and the record has a next, find its index value + if (index == -1 && !info.mNext.empty()) + { + // if next exists, use its index as the current index + index = getInfoIndex(info.mNext, topic); + } + + // if next doesn't exist or not found (i.e. neither exist yet) then start a new one + if (index == -1) + { + Range range = getTopicRange(topic); // getTopicRange converts topic to lower case first + + index = std::distance(getRecords().begin(), range.second); + } + + return index; } bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector& newOrder) @@ -99,7 +184,17 @@ bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector >::reorderRowsImp(baseIndex, newOrder)) + return false; + + // adjust index + int size = static_cast(newOrder.size()); + for (auto& [hash, infos] : mInfoIndex) + for (auto& [a, b] : infos) + if (b >= baseIndex && b < baseIndex + size) + b = newOrder.at(b - baseIndex) + baseIndex; + + return true; } void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue) @@ -126,9 +221,9 @@ void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ES } else { - Record record = getRecord (index); - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + auto record = std::make_unique>(getRecord(index)); + record->mState = RecordBase::State_Deleted; + setRecord (index, std::move(record)); } } else @@ -142,73 +237,54 @@ void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ES CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange (const std::string& topic) const { - std::string topic2 = Misc::StringUtils::lowerCase (topic); + std::string lowerTopic = Misc::StringUtils::lowerCase (topic); - std::map::const_iterator iter = getIdMap().lower_bound (topic2); + // find the topic + std::unordered_map > >::const_iterator iter + = mInfoIndex.find(lowerTopic); - // Skip invalid records: The beginning of a topic string could be identical to another topic - // string. - for (; iter!=getIdMap().end(); ++iter) - { - std::string testTopicId = - Misc::StringUtils::lowerCase (getRecord (iter->second).get().mTopicId); - - if (testTopicId==topic2) - break; - - std::size_t size = topic2.size(); - - if (testTopicId.size()second; - - while (begin != getRecords().begin()) + // topic found, find the starting index + int low = INT_MAX; + for (std::vector >::const_iterator it = iter->second.begin(); + it != iter->second.end(); ++it) { - if (!Misc::StringUtils::ciEqual(begin->get().mTopicId, topic2)) - { - // we've gone one too far, go back - ++begin; - break; - } - --begin; + low = std::min(low, it->second); } - // Find end - RecordConstIterator end = begin; + RecordConstIterator begin = getRecords().begin() + low; - for (; end!=getRecords().end(); ++end) - if (!Misc::StringUtils::ciEqual(end->get().mTopicId, topic2)) - break; + // Find end (one past the range) + RecordConstIterator end = begin + iter->second.size(); + + assert(static_cast(std::distance(begin, end)) == iter->second.size()); return Range (begin, end); } void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId) { - std::string id = Misc::StringUtils::lowerCase(dialogueId); std::vector erasedRecords; - std::map::const_iterator current = getIdMap().lower_bound(id); - std::map::const_iterator end = getIdMap().end(); - for (; current != end; ++current) + Range range = getTopicRange(dialogueId); // getTopicRange converts dialogueId to lower case first + + for (; range.first != range.second; ++range.first) { - Record record = getRecord(current->second); + const Record& record = **range.first; if (Misc::StringUtils::ciEqual(dialogueId, record.get().mTopicId)) { if (record.mState == RecordBase::State_ModifiedOnly) { - erasedRecords.push_back(current->second); + erasedRecords.push_back(range.first - getRecords().begin()); } else { - record.mState = RecordBase::State_Deleted; - setRecord(current->second, record); + auto record2 = std::make_unique>(record); + record2->mState = RecordBase::State_Deleted; + setRecord(range.first - getRecords().begin(), std::move(record2)); } } else @@ -223,3 +299,105 @@ void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId erasedRecords.pop_back(); } } + +// FIXME: removing a record should adjust prev/next and mark those records as modified +// accordingly (also consider undo) +void CSMWorld::InfoCollection::removeRows (int index, int count) +{ + Collection >::removeRows(index, count); // erase records only + + for (std::unordered_map > >::iterator iter + = mInfoIndex.begin(); iter != mInfoIndex.end();) + { + for (std::vector >::iterator it = iter->second.begin(); + it != iter->second.end();) + { + if (it->second >= index) + { + if (it->second >= index+count) + { + it->second -= count; + ++it; + } + else + it = iter->second.erase(it); + } + else + ++it; + } + + // check for an empty vector + if (iter->second.empty()) + mInfoIndex.erase(iter++); + else + ++iter; + } +} + +void CSMWorld::InfoCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) +{ + auto record2 = std::make_unique>(); + + record2->mState = Record::State_ModifiedOnly; + record2->mModified.blank(); + + record2->get().mId = id; + + insertRecord(std::move(record2), getInsertIndex(id, type, nullptr), type); // call InfoCollection::insertRecord() +} + +int CSMWorld::InfoCollection::searchId(std::string_view id) const +{ + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + std::string(id)); + + return getInfoIndex(id.substr(separator+1), id.substr(0, separator)); +} + +void CSMWorld::InfoCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) +{ + int index = getInsertIndex(static_cast*>(record.get())->get().mId, type, record.get()); + + insertRecord(std::move(record), index, type); +} + +void CSMWorld::InfoCollection::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) +{ + int size = static_cast(getRecords().size()); + + std::string id = static_cast*>(record.get())->get().mId; + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + id); + + Collection >::insertRecord(std::move(record), index, type); // add records only + + // adjust index + if (index < size-1) + { + for (std::unordered_map > >::iterator iter + = mInfoIndex.begin(); iter != mInfoIndex.end(); ++iter) + { + for (std::vector >::iterator it = iter->second.begin(); + it != iter->second.end(); ++it) + { + if (it->second >= index) + ++(it->second); + } + } + } + + // get iterator for existing topic or a new topic + std::string lowerId = Misc::StringUtils::lowerCase(id); + std::pair > >::iterator, bool> res + = mInfoIndex.insert( + std::make_pair(lowerId.substr(0, separator), + std::vector >())); // empty vector + + // insert info and index + res.first->second.push_back(std::make_pair(lowerId.substr(separator+1), index)); +} diff --git a/apps/opencs/model/world/infocollection.hpp b/apps/opencs/model/world/infocollection.hpp index 8f5aea6012..96061fb03c 100644 --- a/apps/opencs/model/world/infocollection.hpp +++ b/apps/opencs/model/world/infocollection.hpp @@ -1,6 +1,9 @@ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H +#include +#include + #include "collection.hpp" #include "info.hpp" @@ -11,27 +14,52 @@ namespace ESM namespace CSMWorld { + template<> + void Collection >::removeRows (int index, int count); + + template<> + void Collection >::insertRecord (std::unique_ptr record, + int index, UniversalId::Type type); + + template<> + bool Collection >::reorderRowsImp (int baseIndex, + const std::vector& newOrder); + class InfoCollection : public Collection > { public: - typedef std::vector >::const_iterator RecordConstIterator; + typedef std::vector > >::const_iterator RecordConstIterator; typedef std::pair Range; private: + // The general strategy is to keep the records in Collection kept in order (within + // a topic group) while the index lookup maps are not ordered. It is assumed that + // each topic has a small number of infos, which allows the use of vectors for + // iterating through them without too much penalty. + // + // NOTE: topic string as well as id string are stored in lower case. + std::unordered_map > > mInfoIndex; + void load (const Info& record, bool base); - int getInfoIndex (const std::string& id, const std::string& topic) const; + int getInfoIndex(std::string_view id, std::string_view topic) const; ///< Return index for record \a id or -1 (if not present; deleted records are considered) /// /// \param id info ID without topic prefix + // + /// \attention id and topic are assumed to be in lower case public: - int getAppendIndex (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) const override; + int getInsertIndex (const std::string& id, + UniversalId::Type type = UniversalId::Type_None, + RecordBase *record = nullptr) const override; ///< \param type Will be ignored, unless the collection supports multiple record types + /// + /// Works like getAppendIndex unless an overloaded method uses the record pointer + /// to get additional info about the record that results in an alternative index. bool reorderRows (int baseIndex, const std::vector& newOrder) override; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices @@ -46,6 +74,20 @@ namespace CSMWorld /// the given topic. void removeDialogueInfos(const std::string& dialogueId); + + void removeRows (int index, int count) override; + + void appendBlankRecord (const std::string& id, + UniversalId::Type type = UniversalId::Type_None) override; + + int searchId(std::string_view id) const override; + + void appendRecord (std::unique_ptr record, + UniversalId::Type type = UniversalId::Type_None) override; + + void insertRecord (std::unique_ptr record, + int index, + UniversalId::Type type = UniversalId::Type_None) override; }; } diff --git a/apps/opencs/model/world/infoselectwrapper.hpp b/apps/opencs/model/world/infoselectwrapper.hpp index ce26a46dc7..7c7a839fa6 100644 --- a/apps/opencs/model/world/infoselectwrapper.hpp +++ b/apps/opencs/model/world/infoselectwrapper.hpp @@ -1,7 +1,7 @@ #ifndef CSM_WORLD_INFOSELECTWRAPPER_H #define CSM_WORLD_INFOSELECTWRAPPER_H -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/infotableproxymodel.hpp b/apps/opencs/model/world/infotableproxymodel.hpp index 6a8e66b4f8..92afdabdc5 100644 --- a/apps/opencs/model/world/infotableproxymodel.hpp +++ b/apps/opencs/model/world/infotableproxymodel.hpp @@ -28,7 +28,7 @@ namespace CSMWorld ///< \a currentRow is a row of the source model. public: - InfoTableProxyModel(UniversalId::Type type, QObject *parent = 0); + InfoTableProxyModel(UniversalId::Type type, QObject *parent = nullptr); void setSourceModel(QAbstractItemModel *sourceModel) override; diff --git a/apps/opencs/model/world/land.hpp b/apps/opencs/model/world/land.hpp index e604f13119..99da5cfac0 100644 --- a/apps/opencs/model/world/land.hpp +++ b/apps/opencs/model/world/land.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp index 43deb64a47..c8ac8369ed 100644 --- a/apps/opencs/model/world/landtexture.cpp +++ b/apps/opencs/model/world/landtexture.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp index a7376438c1..601d4b79c9 100644 --- a/apps/opencs/model/world/landtexture.hpp +++ b/apps/opencs/model/world/landtexture.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace CSMWorld { diff --git a/apps/opencs/model/world/metadata.cpp b/apps/opencs/model/world/metadata.cpp index b2fa3487cd..e7d6c8900c 100644 --- a/apps/opencs/model/world/metadata.cpp +++ b/apps/opencs/model/world/metadata.cpp @@ -1,12 +1,14 @@ #include "metadata.hpp" -#include -#include -#include +#include +#include +#include void CSMWorld::MetaData::blank() { - mFormat = ESM::Header::CurrentFormat; + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mFormat = 0; mAuthor.clear(); mDescription.clear(); } diff --git a/apps/opencs/model/world/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index e8b4102d7c..131c71d5a0 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -1,7 +1,7 @@ #include "nestedcoladapterimp.hpp" -#include -#include +#include +#include #include "idcollection.hpp" #include "pathgrid.hpp" @@ -1059,8 +1059,8 @@ namespace CSMWorld case 5: return region.mData.mThunder; case 6: return region.mData.mAsh; case 7: return region.mData.mBlight; - case 8: return region.mData.mA; // Snow - case 9: return region.mData.mB; // Blizzard + case 8: return region.mData.mSnow; + case 9: return region.mData.mBlizzard; default: break; } } @@ -1086,8 +1086,8 @@ namespace CSMWorld 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.mA = chance; break; - case 9: region.mData.mB = 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"); } diff --git a/apps/opencs/model/world/nestedcoladapterimp.hpp b/apps/opencs/model/world/nestedcoladapterimp.hpp index 54780d290e..b1785cc919 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.hpp +++ b/apps/opencs/model/world/nestedcoladapterimp.hpp @@ -3,12 +3,12 @@ #include -#include -#include -#include // for converting magic effect id to string & back -#include // for converting skill names +#include +#include +#include // for converting magic effect id to string & back +#include // for converting skill names #include // for converting attributes -#include +#include #include "nestedcolumnadapter.hpp" #include "nestedtablewrapper.hpp" @@ -163,7 +163,7 @@ namespace CSMWorld std::vector& spells = raceOrBthSgn.mPowers.mList; // blank row - std::string spell = ""; + std::string spell; spells.insert(spells.begin()+position, spell); diff --git a/apps/opencs/model/world/nestedidcollection.hpp b/apps/opencs/model/world/nestedidcollection.hpp index a699d4bd66..1928786c78 100644 --- a/apps/opencs/model/world/nestedidcollection.hpp +++ b/apps/opencs/model/world/nestedidcollection.hpp @@ -30,7 +30,7 @@ namespace CSMWorld public: NestedIdCollection (); - ~NestedIdCollection(); + ~NestedIdCollection() override; void addNestedRow(int row, int column, int position) override; @@ -90,23 +90,23 @@ namespace CSMWorld template void NestedIdCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).addRow(record, position); + getAdapter(Collection::getColumn(column)).addRow(*record, position); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template void NestedIdCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template @@ -121,13 +121,13 @@ namespace CSMWorld void NestedIdCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setData( - record, data, subRow, subColumn); + *record, data, subRow, subColumn); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template @@ -142,13 +142,13 @@ namespace CSMWorld void NestedIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setTable( - record, nestedTable); + *record, nestedTable); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template diff --git a/apps/opencs/model/world/nestedinfocollection.cpp b/apps/opencs/model/world/nestedinfocollection.cpp index 4abaaf9c02..e2e90c49d1 100644 --- a/apps/opencs/model/world/nestedinfocollection.cpp +++ b/apps/opencs/model/world/nestedinfocollection.cpp @@ -35,22 +35,22 @@ namespace CSMWorld void NestedInfoCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection >::getRecord(row)); - getAdapter(Collection >::getColumn(column)).addRow(record, position); + getAdapter(Collection >::getColumn(column)).addRow(*record, position); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } void NestedInfoCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection >::getRecord(row)); - getAdapter(Collection >::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection >::getColumn(column)).removeRow(*record, subRow); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } QVariant NestedInfoCollection::getNestedData (int row, @@ -63,13 +63,13 @@ namespace CSMWorld void NestedInfoCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setData( - record, data, subRow, subColumn); + *record, data, subRow, subColumn); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, @@ -82,13 +82,13 @@ namespace CSMWorld void NestedInfoCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setTable( - record, nestedTable); + *record, nestedTable); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } int NestedInfoCollection::getNestedRowsCount(int row, int column) const diff --git a/apps/opencs/model/world/nestedinfocollection.hpp b/apps/opencs/model/world/nestedinfocollection.hpp index fe2cd43faa..bb747efadf 100644 --- a/apps/opencs/model/world/nestedinfocollection.hpp +++ b/apps/opencs/model/world/nestedinfocollection.hpp @@ -22,7 +22,7 @@ namespace CSMWorld public: NestedInfoCollection (); - ~NestedInfoCollection(); + ~NestedInfoCollection() override; void addNestedRow(int row, int column, int position) override; diff --git a/apps/opencs/model/world/nestedtablewrapper.hpp b/apps/opencs/model/world/nestedtablewrapper.hpp index 7d46dff8bf..1b652683a4 100644 --- a/apps/opencs/model/world/nestedtablewrapper.hpp +++ b/apps/opencs/model/world/nestedtablewrapper.hpp @@ -20,7 +20,7 @@ namespace CSMWorld NestedTableWrapper(const NestedTable& nestedTable) : mNestedTable(nestedTable) {} - virtual ~NestedTableWrapper() {} + ~NestedTableWrapper() override {} int size() const override { diff --git a/apps/opencs/model/world/pathgrid.hpp b/apps/opencs/model/world/pathgrid.hpp index 22d01b0710..712b3969d6 100644 --- a/apps/opencs/model/world/pathgrid.hpp +++ b/apps/opencs/model/world/pathgrid.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include namespace CSMWorld { @@ -20,7 +20,7 @@ namespace CSMWorld { std::string mId; - void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells); + void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection >& cells); void load (ESM::ESMReader &esm, bool &isDeleted); }; } diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 82f2abe770..bb43612e5e 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -1,6 +1,7 @@ #ifndef CSM_WOLRD_RECORD_H #define CSM_WOLRD_RECORD_H +#include #include namespace CSMWorld @@ -20,9 +21,9 @@ namespace CSMWorld virtual ~RecordBase(); - virtual RecordBase *clone() const = 0; + virtual std::unique_ptr clone() const = 0; - virtual RecordBase *modifiedCopy() const = 0; + virtual std::unique_ptr modifiedCopy() const = 0; virtual void assign (const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. @@ -45,9 +46,9 @@ namespace CSMWorld Record(State state, const ESXRecordT *base = 0, const ESXRecordT *modified = 0); - RecordBase *clone() const override; + std::unique_ptr clone() const override; - RecordBase *modifiedCopy() const override; + std::unique_ptr modifiedCopy() const override; void assign (const RecordBase& record) override; @@ -85,15 +86,16 @@ namespace CSMWorld } template - RecordBase *Record::modifiedCopy() const + std::unique_ptr Record::modifiedCopy() const { - return new Record (State_ModifiedOnly, 0, &(this->get())); + return std::make_unique >( + Record(State_ModifiedOnly, nullptr, &(this->get()))); } template - RecordBase *Record::clone() const + std::unique_ptr Record::clone() const { - return new Record (*this); + return std::make_unique >(Record(*this)); } template diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index b336235909..0b07b484ca 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -2,7 +2,7 @@ #include "cellcoordinates.hpp" -CSMWorld::CellRef::CellRef() : mNew (true) +CSMWorld::CellRef::CellRef() : mNew (true), mIdNum(0) { mRefNum.mIndex = 0; mRefNum.mContentFile = 0; diff --git a/apps/opencs/model/world/ref.hpp b/apps/opencs/model/world/ref.hpp index 5d10a3a1b3..1eefe79f67 100644 --- a/apps/opencs/model/world/ref.hpp +++ b/apps/opencs/model/world/ref.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace CSMWorld { @@ -14,6 +14,7 @@ namespace CSMWorld std::string mCell; std::string mOriginalCell; bool mNew; // new reference, not counted yet, ref num not assigned yet + unsigned int mIdNum; CellRef(); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index d8f6b391b7..804a13cf7b 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -1,15 +1,45 @@ #include "refcollection.hpp" -#include -#include +#include #include "ref.hpp" #include "cell.hpp" #include "universalid.hpp" #include "record.hpp" +#include + +namespace CSMWorld +{ + template<> + void Collection >::removeRows (int index, int count) + { + mRecords.erase(mRecords.begin()+index, mRecords.begin()+index+count); + + // index map is updated in RefCollection::removeRows() + } + + template<> + void Collection >::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) + { + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); + + std::unique_ptr > record2(static_cast*>(record.release())); + + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin()+index, std::move(record2)); + + // index map is updated in RefCollection::insertRecord() + } +} + 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); @@ -20,8 +50,9 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool ESM::MovedCellRef mref; mref.mRefNum.mIndex = 0; bool isDeleted = false; + bool isMoved = false; - while (ESM::Cell::getNextRef(reader, ref, isDeleted, true, &mref)) + while (ESM::Cell::getNextRef(reader, ref, isDeleted, mref, isMoved)) { // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). @@ -35,7 +66,7 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool ref.mCell = "#" + std::to_string(index.first) + " " + std::to_string(index.second); // Handle non-base moved references - if (!base && mref.mRefNum.mIndex != 0) + if (!base && isMoved) { // Moved references must have a link back to their original cell // See discussion: https://forum.openmw.org/viewtopic.php?f=6&t=577&start=30 @@ -60,15 +91,47 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool else ref.mCell = cell2.mId; - mref.mRefNum.mIndex = 0; + 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; - // ignore content file number - std::map::iterator iter = cache.begin(); - ref.mRefNum.mIndex = ref.mRefNum.mIndex & 0x00ffffff; - for (; iter != cache.end(); ++iter) + std::map::iterator iter = cache.find(refNum); + + if (isMoved) { - if (ref.mRefNum.mIndex == iter->first.mIndex) - break; + if (iter == cache.end()) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, + mCells.getId(cellIndex)); + + messages.add(id, "Attempt to move a non-existent reference - RefNum index " + + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/"", + CSMDoc::Message::Severity_Warning); + continue; + } + + int index = getIntIndex(iter->second); + + // ensure we have the same record id for setRecord() + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId); + + auto record = std::make_unique>(); + // TODO: check whether a base record be moved + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); + + // overwrite original record + setRecord(index, std::move(record)); + + continue; // NOTE: assumed moved references are not deleted at the same time } if (isDeleted) @@ -78,13 +141,15 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); - messages.add (id, "Attempt to delete a non-existent reference"); + messages.add (id, "Attempt to delete a non-existent reference - RefNum index " + + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/"", + CSMDoc::Message::Severity_Warning); continue; } - int index = getIndex (iter->second); - - Record record = getRecord (index); + int index = getIntIndex (iter->second); if (base) { @@ -93,8 +158,9 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool } else { - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + auto record = std::make_unique>(getRecord(index)); + record->mState = RecordBase::State_Deleted; + setRecord(index, std::move(record)); } continue; @@ -103,28 +169,47 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool if (iter==cache.end()) { // new reference + ref.mIdNum = mNextId; // FIXME: fragile ref.mId = getNewId(); - Record record; - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record.mBase : record.mModified) = ref; + cache.emplace(refNum, ref.mIdNum); - appendRecord (record); + auto record = std::make_unique>(); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); - cache.insert (std::make_pair (ref.mRefNum, ref.mId)); + appendRecord(std::move(record)); } else { // old reference -> merge - ref.mId = iter->second; + int index = getIntIndex(iter->second); +#if 0 + // ref.mRefNum.mIndex : the key + // iter->second : previously cached idNum for the key + // index : position of the record for that idNum + // getRecord(index).get() : record in the index position + assert(iter->second != getRecord(index).get().mIdNum); // sanity check - int index = getIndex (ref.mId); + // check if the plugin used the same RefNum index for a different record + if (ref.mRefID != getRecord(index).get().mRefID) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); + messages.add(id, + "RefNum renamed from RefID \"" + getRecord(index).get().mRefID + "\" to \"" + + ref.mRefID + "\" (RefNum index " + std::to_string(ref.mRefNum.mIndex) + ")", + /*hint*/"", + CSMDoc::Message::Severity_Info); + } +#endif + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId); - Record record = getRecord (index); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; - (base ? record.mBase : record.mModified) = ref; + auto record = std::make_unique>(getRecord(index)); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; + (base ? record->mBase : record->mModified) = std::move(ref); - setRecord (index, record); + setRecord(index, std::move(record)); } } } @@ -133,3 +218,117 @@ std::string CSMWorld::RefCollection::getNewId() { return "ref#" + std::to_string(mNextId++); } + +unsigned int CSMWorld::RefCollection::extractIdNum(std::string_view id) const +{ + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid ref ID: " + std::string(id)); + + return static_cast(std::stoi(std::string(id.substr(separator+1)))); +} + +int CSMWorld::RefCollection::getIntIndex (unsigned int id) const +{ + int index = searchId(id); + + if (index == -1) + throw std::runtime_error("invalid RefNum: " + std::to_string(id)); + + return index; +} + +int CSMWorld::RefCollection::searchId (unsigned int id) const +{ + std::map::const_iterator iter = mRefIndex.find(id); + + if (iter == mRefIndex.end()) + return -1; + + return iter->second; +} + +void CSMWorld::RefCollection::removeRows (int index, int count) +{ + Collection >::removeRows(index, count); // erase records only + + std::map::iterator iter = mRefIndex.begin(); + while (iter != mRefIndex.end()) + { + if (iter->second>=index) + { + if (iter->second >= index+count) + { + iter->second -= count; + ++iter; + } + else + mRefIndex.erase(iter++); + } + else + ++iter; + } +} + +void CSMWorld::RefCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) +{ + auto record = std::make_unique>(); + + record->mState = Record::State_ModifiedOnly; + record->mModified.blank(); + + record->get().mId = id; + record->get().mIdNum = extractIdNum(id); + + Collection >::appendRecord(std::move(record)); +} + +void CSMWorld::RefCollection::cloneRecord (const std::string& origin, + const std::string& destination, + const UniversalId::Type type) +{ + auto copy = std::make_unique>(); + + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + + copy->get().mId = destination; + copy->get().mIdNum = extractIdNum(destination); + + insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() +} + +int CSMWorld::RefCollection::searchId(std::string_view id) const +{ + return searchId(extractIdNum(id)); +} + +void CSMWorld::RefCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) +{ + int index = getAppendIndex(/*id*/"", type); // for CellRef records id is ignored + + mRefIndex.insert(std::make_pair(static_cast*>(record.get())->get().mIdNum, index)); + + Collection >::insertRecord(std::move(record), index, type); // add records only +} + +void CSMWorld::RefCollection::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) +{ + int size = getAppendIndex(/*id*/"", type); // for CellRef records id is ignored + unsigned int idNum = static_cast*>(record.get())->get().mIdNum; + + Collection >::insertRecord(std::move(record), index, type); // add records only + + if (index < size-1) + { + for (std::map::iterator iter(mRefIndex.begin()); iter != mRefIndex.end(); ++iter) + { + if (iter->second >= index) + ++(iter->second); + } + } + + mRefIndex.insert(std::make_pair(idNum, index)); +} diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index d031398d3f..affd66af32 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -2,6 +2,7 @@ #define CSM_WOLRD_REFCOLLECTION_H #include +#include #include "../doc/stage.hpp" @@ -14,12 +15,27 @@ namespace CSMWorld struct Cell; class UniversalId; + template<> + void Collection >::removeRows (int index, int count); + + template<> + void Collection >::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type); + /// \brief References in cells class RefCollection : public Collection { Collection& mCells; + std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum + int mNextId; + unsigned int extractIdNum(std::string_view id) const; + + int getIntIndex (unsigned int id) const; + + int searchId (unsigned int id) const; + public: // MSVC needs the constructor for a class inheriting a template to be defined in header RefCollection (Collection& cells) @@ -27,10 +43,28 @@ namespace CSMWorld {} void load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages); + std::map& cache, CSMDoc::Messages& messages); ///< Load a sequence of references. std::string getNewId(); + + virtual void removeRows (int index, int count); + + virtual void appendBlankRecord (const std::string& id, + UniversalId::Type type = UniversalId::Type_None); + + virtual void cloneRecord (const std::string& origin, + const std::string& destination, + const UniversalId::Type type); + + virtual int searchId(std::string_view id) const; + + virtual void appendRecord (std::unique_ptr record, + UniversalId::Type type = UniversalId::Type_None); + + virtual void insertRecord (std::unique_ptr record, + int index, + UniversalId::Type type = UniversalId::Type_None); }; } diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index d85fcc068f..a30dcadd0b 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -1,16 +1,14 @@ #include "refidadapterimp.hpp" -#include #include -#include -#include -#include +#include +#include #include "nestedtablewrapper.hpp" CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) -: InventoryColumns (columns) {} +: InventoryColumns (columns), mEffects(nullptr) {} CSMWorld::PotionRefIdAdapter::PotionRefIdAdapter (const PotionColumns& columns, const RefIdColumn *autoCalc) @@ -56,7 +54,9 @@ void CSMWorld::PotionRefIdAdapter::setData (const RefIdColumn *column, RefIdData CSMWorld::IngredientColumns::IngredientColumns (const InventoryColumns& columns) -: InventoryColumns (columns) {} +: InventoryColumns (columns) +, mEffects(nullptr) +{} CSMWorld::IngredientRefIdAdapter::IngredientRefIdAdapter (const IngredientColumns& columns) : InventoryRefIdAdapter (UniversalId::Type_Ingredient, columns), @@ -585,7 +585,13 @@ void CSMWorld::DoorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& } CSMWorld::LightColumns::LightColumns (const InventoryColumns& columns) -: InventoryColumns (columns) {} +: InventoryColumns (columns) +, mTime(nullptr) +, mRadius(nullptr) +, mColor(nullptr) +, mSound(nullptr) +, mEmitterType(nullptr) +{} CSMWorld::LightRefIdAdapter::LightRefIdAdapter (const LightColumns& columns) : InventoryRefIdAdapter (UniversalId::Type_Light, columns), mColumns (columns) @@ -1100,7 +1106,6 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column case 5: return static_cast(record.get().mNpdt.mReputation); case 6: return static_cast(record.get().mNpdt.mRank); case 7: return record.get().mNpdt.mGold; - case 8: return record.get().mPersistent == true; default: return QVariant(); // throw an exception here? } else @@ -1114,7 +1119,6 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column case 5: return static_cast(record.get().mNpdt.mReputation); case 6: return static_cast(record.get().mNpdt.mRank); case 7: return record.get().mNpdt.mGold; - case 8: return record.get().mPersistent == true; default: return QVariant(); // throw an exception here? } } @@ -1139,7 +1143,6 @@ void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; case 7: npc.mNpdt.mGold = value.toInt(); break; - case 8: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } else @@ -1153,7 +1156,6 @@ void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; case 7: npc.mNpdt.mGold = value.toInt(); break; - case 8: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } @@ -1162,7 +1164,7 @@ void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, int CSMWorld::NpcMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { - return 9; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold, Persist + return 8; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold } int CSMWorld::NpcMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const @@ -1454,7 +1456,15 @@ int CSMWorld::CreatureMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *co } CSMWorld::WeaponColumns::WeaponColumns (const EnchantableColumns& columns) -: EnchantableColumns (columns) {} +: EnchantableColumns (columns) +, mType(nullptr) +, mHealth(nullptr) +, mSpeed(nullptr) +, mReach(nullptr) +, mChop{nullptr} +, mSlash{nullptr} +, mThrust{nullptr} +{} CSMWorld::WeaponRefIdAdapter::WeaponRefIdAdapter (const WeaponColumns& columns) : EnchantableRefIdAdapter (UniversalId::Type_Weapon, columns), mColumns (columns) diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index 7695e9acec..99ce882688 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -5,11 +5,11 @@ #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include "columnbase.hpp" #include "record.hpp" @@ -25,6 +25,12 @@ namespace CSMWorld const RefIdColumn *mId; const RefIdColumn *mModified; const RefIdColumn *mType; + const RefIdColumn *mBlocked; + + BaseColumns () : mId(nullptr) + , mModified(nullptr) + , mType(nullptr) + , mBlocked(nullptr) {} }; /// \brief Base adapter for all refereceable record types @@ -90,6 +96,9 @@ namespace CSMWorld if (column==mBase.mType) return static_cast (mType); + if (column==mBase.mBlocked) + return (record.get().mRecordFlags & ESM::FLAG_Blocked) != 0; + return QVariant(); } @@ -102,6 +111,17 @@ namespace CSMWorld if (column==mBase.mModified) record.mState = static_cast (value.toInt()); + else if (column==mBase.mBlocked) + { + RecordT record2 = record.get(); + + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Blocked; + else + record2.mRecordFlags &= ~ESM::FLAG_Blocked; + + record.setModified(record2); + } } template @@ -110,12 +130,21 @@ namespace CSMWorld return mType; } + // NOTE: Body Part should not have persistence (but BodyPart is not listed in the Objects + // table at the moment). + // + // Spellmaking - not persistent - currently not part of objects table + // Enchanting - not persistent - currently not part of objects table + // + // Leveled Creature - no model, so not persistent + // Leveled Item - no model, so not persistent struct ModelColumns : public BaseColumns { const RefIdColumn *mModel; + const RefIdColumn *mPersistence; - ModelColumns (const BaseColumns& base) : BaseColumns (base) {} + ModelColumns (const BaseColumns& base) : BaseColumns (base), mModel(nullptr), mPersistence(nullptr) {} }; /// \brief Adapter for IDs with models (all but levelled lists) @@ -151,6 +180,9 @@ namespace CSMWorld if (column==mModel.mModel) return QString::fromUtf8 (record.get().mModel.c_str()); + if (column==mModel.mPersistence) + return (record.get().mRecordFlags & ESM::FLAG_Persistent) != 0; + return BaseRefIdAdapter::getData (column, data, index); } @@ -164,6 +196,13 @@ namespace CSMWorld RecordT record2 = record.get(); if (column==mModel.mModel) record2.mModel = value.toString().toUtf8().constData(); + else if (column==mModel.mPersistence) + { + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Persistent; + else + record2.mRecordFlags &= ~ESM::FLAG_Persistent; + } else { BaseRefIdAdapter::setData (column, data, index, value); @@ -178,7 +217,11 @@ namespace CSMWorld const RefIdColumn *mName; const RefIdColumn *mScript; - NameColumns (const ModelColumns& base) : ModelColumns (base) {} + NameColumns (const ModelColumns& base) + : ModelColumns (base) + , mName(nullptr) + , mScript(nullptr) + {} }; /// \brief Adapter for IDs with names (all but levelled lists and statics) @@ -247,7 +290,12 @@ namespace CSMWorld const RefIdColumn *mWeight; const RefIdColumn *mValue; - InventoryColumns (const NameColumns& base) : NameColumns (base) {} + InventoryColumns (const NameColumns& base) + : NameColumns (base) + , mIcon(nullptr) + , mWeight(nullptr) + , mValue(nullptr) + {} }; /// \brief Adapter for IDs that can go into an inventory @@ -375,7 +423,7 @@ namespace CSMWorld IngredEffectRefIdAdapter(); - virtual ~IngredEffectRefIdAdapter(); + ~IngredEffectRefIdAdapter() override; void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; @@ -405,7 +453,11 @@ namespace CSMWorld const RefIdColumn *mEnchantment; const RefIdColumn *mEnchantmentPoints; - EnchantableColumns (const InventoryColumns& base) : InventoryColumns (base) {} + EnchantableColumns (const InventoryColumns& base) + : InventoryColumns (base) + , mEnchantment(nullptr) + , mEnchantmentPoints(nullptr) + {} }; /// \brief Adapter for enchantable IDs @@ -474,7 +526,11 @@ namespace CSMWorld const RefIdColumn *mQuality; const RefIdColumn *mUses; - ToolColumns (const InventoryColumns& base) : InventoryColumns (base) {} + ToolColumns (const InventoryColumns& base) + : InventoryColumns (base) + , mQuality(nullptr) + , mUses(nullptr) + {} }; /// \brief Adapter for tools with limited uses IDs (lockpick, repair, probes) @@ -549,7 +605,17 @@ namespace CSMWorld const RefIdColumn *mAiPackages; std::map mServices; - ActorColumns (const NameColumns& base) : NameColumns (base) {} + ActorColumns (const NameColumns& base) + : NameColumns (base) + , mHello(nullptr) + , mFlee(nullptr) + , mFight(nullptr) + , mAlarm(nullptr) + , mInventory(nullptr) + , mSpells(nullptr) + , mDestinations(nullptr) + , mAiPackages(nullptr) + {} }; /// \brief Adapter for actor IDs (handles common AI functionality) @@ -971,7 +1037,7 @@ namespace CSMWorld public: NpcMiscRefIdAdapter (); - virtual ~NpcMiscRefIdAdapter(); + ~NpcMiscRefIdAdapter() override; void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; @@ -1062,7 +1128,7 @@ namespace CSMWorld public: CreatureMiscRefIdAdapter (); - virtual ~CreatureMiscRefIdAdapter(); + ~CreatureMiscRefIdAdapter() override; void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; @@ -1464,7 +1530,7 @@ namespace CSMWorld ESM::Transport::Dest newRow; newRow.mPos = newPos; - newRow.mCellName = ""; + newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); @@ -1615,8 +1681,8 @@ namespace CSMWorld newRow.mWander.mTimeOfDay = 0; for (int i = 0; i < 8; ++i) newRow.mWander.mIdle[i] = 0; - newRow.mWander.mShouldRepeat = 0; - newRow.mCellName = ""; + newRow.mWander.mShouldRepeat = 1; + newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); @@ -1721,9 +1787,15 @@ namespace CSMWorld return static_cast(content.mWander.mIdle[subColIndex-4]); else return QVariant(); - case 12: // wander repeat + case 12: // repeat if (content.mType == ESM::AI_Wander) return content.mWander.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Travel) + return content.mTravel.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + return content.mTarget.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Activate) + return content.mActivate.mShouldRepeat != 0; else return QVariant(); case 13: // activate name @@ -1807,6 +1879,7 @@ namespace CSMWorld content.mWander.mDuration = static_cast(value.toInt()); else return; // return without saving + break; case 3: if (content.mType == ESM::AI_Wander) content.mWander.mTimeOfDay = static_cast(value.toInt()); @@ -1831,20 +1904,32 @@ namespace CSMWorld case 12: if (content.mType == ESM::AI_Wander) content.mWander.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Travel) + content.mTravel.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + content.mTarget.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Activate) + content.mActivate.mShouldRepeat = static_cast(value.toInt()); else return; // return without saving break; // always save case 13: // NAME32 if (content.mType == ESM::AI_Activate) - content.mActivate.mName.assign(value.toString().toUtf8().constData()); + { + const QByteArray name = value.toString().toUtf8(); + content.mActivate.mName.assign(std::string_view(name.constData(), name.size())); + } else return; // return without saving break; // always save case 14: // NAME32 if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) - content.mTarget.mId.assign(value.toString().toUtf8().constData()); + { + const QByteArray id = value.toString().toUtf8(); + content.mTarget.mId.assign(std::string_view(id.constData(), id.size())); + } else return; // return without saving @@ -1858,18 +1943,18 @@ namespace CSMWorld break; // always save case 16: if (content.mType == ESM::AI_Travel) - content.mTravel.mZ = value.toFloat(); + content.mTravel.mX = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) - content.mTarget.mZ = value.toFloat(); + content.mTarget.mX = value.toFloat(); else return; // return without saving break; // always save case 17: if (content.mType == ESM::AI_Travel) - content.mTravel.mZ = value.toFloat(); + content.mTravel.mY = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) - content.mTarget.mZ = value.toFloat(); + content.mTarget.mY = value.toFloat(); else return; // return without saving @@ -1931,8 +2016,8 @@ namespace CSMWorld ESM::PartReference newPart; newPart.mPart = 0; // 0 == head - newPart.mMale = ""; - newPart.mFemale = ""; + newPart.mMale.clear(); + newPart.mFemale.clear(); if (position >= (int)list.size()) list.push_back(newPart); @@ -2054,7 +2139,11 @@ namespace CSMWorld const RefIdColumn *mLevList; const RefIdColumn *mNestedListLevList; - LevListColumns (const BaseColumns& base) : BaseColumns (base) {} + LevListColumns (const BaseColumns& base) + : BaseColumns (base) + , mLevList(nullptr) + , mNestedListLevList(nullptr) + {} }; template @@ -2276,7 +2365,7 @@ namespace CSMWorld std::vector& list = leveled.mList; ESM::LevelledListBase::LevelItem newItem; - newItem.mId = ""; + newItem.mId.clear(); newItem.mLevel = 0; if (position >= (int)list.size()) diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 535a31dddb..2e0c7e2028 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -2,8 +2,9 @@ #include #include +#include -#include +#include #include "refidadapter.hpp" #include "refidadapterimp.hpp" @@ -49,15 +50,22 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Blocked, ColumnBase::Display_Boolean, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); + baseColumns.mBlocked = &mColumns.back(); ModelColumns modelColumns (baseColumns); + mColumns.emplace_back(Columns::ColumnId_Persistent, ColumnBase::Display_Boolean); + modelColumns.mPersistence = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); modelColumns.mModel = &mColumns.back(); NameColumns nameColumns (modelColumns); - mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String); + // Only items that can be placed in a container have the 32 character limit, but enforce + // that for all referenceable types for now. + mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String32); nameColumns.mName = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Script, ColumnBase::Display_Script); nameColumns.mScript = &mColumns.back(); @@ -229,9 +237,9 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String)); + new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String32)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String)); + new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String32)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiTargetCell, CSMWorld::ColumnBase::Display_String)); mColumns.back().addColumn( @@ -350,7 +358,7 @@ CSMWorld::RefIdCollection::RefIdCollection() }; // for re-use in NPC records - const RefIdColumn *essential = 0; + const RefIdColumn *essential = nullptr; for (int i=0; sCreatureFlagTable[i].mName!=-1; ++i) { @@ -477,6 +485,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_Class, ColumnBase::Display_Class); npcColumns.mClass = &mColumns.back(); + // NAME32 enforced in IdCompletionDelegate::createEditor() mColumns.emplace_back(Columns::ColumnId_Faction, ColumnBase::Display_Faction); npcColumns.mFaction = &mColumns.back(); @@ -549,8 +558,6 @@ CSMWorld::RefIdCollection::RefIdCollection() new RefIdColumn (Columns::ColumnId_NpcRank, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_NpcPersistence, CSMWorld::ColumnBase::Display_Boolean)); WeaponColumns weaponColumns (enchantableColumns); @@ -764,7 +771,6 @@ void CSMWorld::RefIdCollection::setNestedData(int row, int column, const QVarian const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedData(&mColumns.at (column), mData, localIndex.first, data, subRow, subColumn); - return; } void CSMWorld::RefIdCollection::removeRows (int index, int count) @@ -778,7 +784,6 @@ void CSMWorld::RefIdCollection::removeNestedRows(int row, int column, int subRow const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.removeNestedRow(&mColumns.at (column), mData, localIndex.first, subRow); - return; } void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) @@ -786,7 +791,7 @@ void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, Univer mData.appendRecord (type, id, false); } -int CSMWorld::RefIdCollection::searchId (const std::string& id) const +int CSMWorld::RefIdCollection::searchId(std::string_view id) const { RefIdData::LocalIndex localIndex = mData.searchId (id); @@ -796,18 +801,18 @@ int CSMWorld::RefIdCollection::searchId (const std::string& id) const return mData.localToGlobalIndex (localIndex); } -void CSMWorld::RefIdCollection::replace (int index, const RecordBase& record) +void CSMWorld::RefIdCollection::replace (int index, std::unique_ptr record) { - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord (mData.globalToLocalIndex (index)).assign (*record.release()); } void CSMWorld::RefIdCollection::cloneRecord(const std::string& origin, const std::string& destination, const CSMWorld::UniversalId::Type type) { - std::unique_ptr newRecord(mData.getRecord(mData.searchId(origin)).modifiedCopy()); + std::unique_ptr newRecord = mData.getRecord(mData.searchId(origin)).modifiedCopy(); mAdapters.find(type)->second->setId(*newRecord, destination); - mData.insertRecord(*newRecord, type, destination); + mData.insertRecord(std::move(newRecord), type, destination); } bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) @@ -816,16 +821,16 @@ bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) return false; } -void CSMWorld::RefIdCollection::appendRecord (const RecordBase& record, +void CSMWorld::RefIdCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) { - std::string id = findAdapter (type).getId (record); + std::string id = findAdapter (type).getId (*record.get()); int index = mData.getAppendIndex (type); mData.appendRecord (type, id, false); - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord (mData.globalToLocalIndex (index)).assign (*record.release()); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (const std::string& id) const @@ -895,7 +900,6 @@ void CSMWorld::RefIdCollection::addNestedRow(int row, int col, int position) const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(col), localIndex.second); nestedAdapter.addNestedRow(&mColumns.at(col), mData, localIndex.first, position); - return; } void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) @@ -904,7 +908,6 @@ void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWor const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedTable(&mColumns.at(column), mData, localIndex.first, nestedTable); - return; } CSMWorld::NestedTableWrapperBase* CSMWorld::RefIdCollection::nestedTable(int row, int column) const diff --git a/apps/opencs/model/world/refidcollection.hpp b/apps/opencs/model/world/refidcollection.hpp index e85263ac13..ee17bb3214 100644 --- a/apps/opencs/model/world/refidcollection.hpp +++ b/apps/opencs/model/world/refidcollection.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "columnbase.hpp" #include "collectionbase.hpp" @@ -58,7 +59,7 @@ namespace CSMWorld RefIdCollection(); - virtual ~RefIdCollection(); + ~RefIdCollection() override; int getSize() const override; @@ -85,16 +86,16 @@ namespace CSMWorld void appendBlankRecord (const std::string& id, UniversalId::Type type) override; ///< \param type Will be ignored, unless the collection supports multiple record types - int searchId (const std::string& id) const override; + int searchId(std::string_view id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - void replace (int index, const RecordBase& record) override; + void replace (int index, std::unique_ptr record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. - void appendRecord (const RecordBase& record, UniversalId::Type type) override; + void appendRecord (std::unique_ptr record, UniversalId::Type type) override; ///< If the record type does not match, an exception is thrown. /// ///< \param type Will be ignored, unless the collection supports multiple record types diff --git a/apps/opencs/model/world/refiddata.cpp b/apps/opencs/model/world/refiddata.cpp index e2ffbcca6b..3bd8bfd5fc 100644 --- a/apps/opencs/model/world/refiddata.cpp +++ b/apps/opencs/model/world/refiddata.cpp @@ -2,6 +2,7 @@ #include #include +#include CSMWorld::RefIdDataContainerBase::~RefIdDataContainerBase() {} @@ -74,8 +75,7 @@ int CSMWorld::RefIdData::localToGlobalIndex (const LocalIndex& index) return globalIndex; } -CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId ( - const std::string& id) const +CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase (id); @@ -87,6 +87,39 @@ CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId ( return iter->second; } +unsigned int CSMWorld::RefIdData::getRecordFlags (const std::string& id) const +{ + LocalIndex localIndex = searchId (id); + + switch (localIndex.second) + { + case UniversalId::Type_Activator: return mActivators.getRecordFlags(localIndex.first); + case UniversalId::Type_Potion: return mPotions.getRecordFlags(localIndex.first); + case UniversalId::Type_Apparatus: return mApparati.getRecordFlags(localIndex.first); + case UniversalId::Type_Armor: return mArmors.getRecordFlags(localIndex.first); + case UniversalId::Type_Book: return mBooks.getRecordFlags(localIndex.first); + case UniversalId::Type_Clothing: return mClothing.getRecordFlags(localIndex.first); + case UniversalId::Type_Container: return mContainers.getRecordFlags(localIndex.first); + case UniversalId::Type_Creature: return mCreatures.getRecordFlags(localIndex.first); + case UniversalId::Type_Door: return mDoors.getRecordFlags(localIndex.first); + case UniversalId::Type_Ingredient: return mIngredients.getRecordFlags(localIndex.first); + case UniversalId::Type_CreatureLevelledList: return mCreatureLevelledLists.getRecordFlags(localIndex.first); + case UniversalId::Type_ItemLevelledList: return mItemLevelledLists.getRecordFlags(localIndex.first); + case UniversalId::Type_Light: return mLights.getRecordFlags(localIndex.first); + case UniversalId::Type_Lockpick: return mLockpicks.getRecordFlags(localIndex.first); + case UniversalId::Type_Miscellaneous: return mMiscellaneous.getRecordFlags(localIndex.first); + case UniversalId::Type_Npc: return mNpcs.getRecordFlags(localIndex.first); + case UniversalId::Type_Probe: return mProbes.getRecordFlags(localIndex.first); + case UniversalId::Type_Repair: return mRepairs.getRecordFlags(localIndex.first); + case UniversalId::Type_Static: return mStatics.getRecordFlags(localIndex.first); + case UniversalId::Type_Weapon: return mWeapons.getRecordFlags(localIndex.first); + default: + break; + } + + return 0; +} + void CSMWorld::RefIdData::erase (int index, int count) { LocalIndex localIndex = globalToLocalIndex (index); @@ -367,7 +400,7 @@ const CSMWorld::RefIdDataContainer< ESM::Static >& CSMWorld::RefIdData::getStati return mStatics; } -void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, const std::string& id) +void CSMWorld::RefIdData::insertRecord (std::unique_ptr record, CSMWorld::UniversalId::Type type, const std::string& id) { std::map::iterator iter = mRecordContainers.find (type); @@ -375,7 +408,7 @@ void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld:: if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); - iter->second->insertRecord(record); + iter->second->insertRecord(std::move(record)); mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), LocalIndex (iter->second->getSize()-1, type))); @@ -387,9 +420,7 @@ void CSMWorld::RefIdData::copyTo (int index, RefIdData& target) const RefIdDataContainerBase *source = mRecordContainers.find (localIndex.second)->second; - std::string id = source->getId (localIndex.first); - - std::unique_ptr newRecord (source->getRecord (localIndex.first).modifiedCopy()); - - target.insertRecord (*newRecord, localIndex.second, id); + target.insertRecord(source->getRecord(localIndex.first).modifiedCopy(), + localIndex.second, + source->getId(localIndex.first)); } diff --git a/apps/opencs/model/world/refiddata.hpp b/apps/opencs/model/world/refiddata.hpp index 1480bb71d9..0b9fc66b53 100644 --- a/apps/opencs/model/world/refiddata.hpp +++ b/apps/opencs/model/world/refiddata.hpp @@ -3,27 +3,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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include @@ -47,9 +50,11 @@ namespace CSMWorld virtual RecordBase& getRecord (int index)= 0; + virtual unsigned int getRecordFlags (int index) const = 0; + virtual void appendRecord (const std::string& id, bool base) = 0; - virtual void insertRecord (RecordBase& record) = 0; + virtual void insertRecord (std::unique_ptr record) = 0; virtual int load (ESM::ESMReader& reader, bool base) = 0; ///< \return index of a loaded record or -1 if no record was loaded @@ -64,7 +69,7 @@ namespace CSMWorld template struct RefIdDataContainer : public RefIdDataContainerBase { - std::vector > mContainer; + std::vector > > mContainer; int getSize() const override; @@ -72,9 +77,11 @@ namespace CSMWorld RecordBase& getRecord (int index) override; + unsigned int getRecordFlags (int index) const override; + void appendRecord (const std::string& id, bool base) override; - void insertRecord (RecordBase& record) override; + void insertRecord (std::unique_ptr record) override; int load (ESM::ESMReader& reader, bool base) override; ///< \return index of a loaded record or -1 if no record was loaded @@ -87,10 +94,13 @@ namespace CSMWorld }; template - void RefIdDataContainer::insertRecord(RecordBase& record) + void RefIdDataContainer::insertRecord(std::unique_ptr record) { - Record& newRecord = dynamic_cast& >(record); - mContainer.push_back(newRecord); + assert(record != nullptr); + // convert base pointer to record type pointer + std::unique_ptr> typedRecord(&dynamic_cast&>(*record)); + record.release(); + mContainer.push_back(std::move(typedRecord)); } template @@ -102,27 +112,33 @@ namespace CSMWorld template const RecordBase& RefIdDataContainer::getRecord (int index) const { - return mContainer.at (index); + return *mContainer.at (index); } template RecordBase& RefIdDataContainer::getRecord (int index) { - return mContainer.at (index); + return *mContainer.at (index); + } + + template + unsigned int RefIdDataContainer::getRecordFlags (int index) const + { + return mContainer.at (index)->get().mRecordFlags; } template void RefIdDataContainer::appendRecord (const std::string& id, bool base) { - Record record; + auto record = std::make_unique>(); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - record.mBase.mId = id; - record.mModified.mId = id; - (base ? record.mBase : record.mModified).blank(); + record->mBase.mId = id; + record->mModified.mId = id; + (base ? record->mBase : record->mModified).blank(); - mContainer.push_back (record); + mContainer.push_back (std::move(record)); } template @@ -137,7 +153,7 @@ namespace CSMWorld int numRecords = static_cast(mContainer.size()); for (; index < numRecords; ++index) { - if (Misc::StringUtils::ciEqual(mContainer[index].get().mId, record.mId)) + if (Misc::StringUtils::ciEqual(mContainer[index]->get().mId, record.mId)) { break; } @@ -155,7 +171,7 @@ namespace CSMWorld // Flag the record as Deleted even for a base content file. // RefIdData is responsible for its erasure. - mContainer[index].mState = RecordBase::State_Deleted; + mContainer[index]->mState = RecordBase::State_Deleted; } else { @@ -164,22 +180,22 @@ namespace CSMWorld appendRecord(record.mId, base); if (base) { - mContainer.back().mBase = record; + mContainer.back()->mBase = record; } else { - mContainer.back().mModified = record; + mContainer.back()->mModified = record; } } else if (!base) { - mContainer[index].setModified(record); + mContainer[index]->setModified(record); } else { // Overwrite - mContainer[index].setModified(record); - mContainer[index].merge(); + mContainer[index]->setModified(record); + mContainer[index]->merge(); } } @@ -198,18 +214,18 @@ namespace CSMWorld template std::string RefIdDataContainer::getId (int index) const { - return mContainer.at (index).get().mId; + return mContainer.at (index)->get().mId; } template void RefIdDataContainer::save (int index, ESM::ESMWriter& writer) const { - Record record = mContainer.at(index); + const Record& record = *mContainer.at(index); if (record.isModified() || record.mState == RecordBase::State_Deleted) { RecordT esmRecord = record.get(); - writer.startRecord(esmRecord.sRecordId); + writer.startRecord(esmRecord.sRecordId, esmRecord.mRecordFlags); esmRecord.save(writer, record.mState == RecordBase::State_Deleted); writer.endRecord(esmRecord.sRecordId); } @@ -262,17 +278,19 @@ namespace CSMWorld int localToGlobalIndex (const LocalIndex& index) const; - LocalIndex searchId (const std::string& id) const; + LocalIndex searchId(std::string_view id) const; void erase (int index, int count); - void insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, + void insertRecord (std::unique_ptr record, CSMWorld::UniversalId::Type type, const std::string& id); const RecordBase& getRecord (const LocalIndex& index) const; RecordBase& getRecord (const LocalIndex& index); + unsigned int getRecordFlags(const std::string& id) const; + void appendRecord (UniversalId::Type type, const std::string& id, bool base); int getAppendIndex (UniversalId::Type type) const; diff --git a/apps/opencs/model/world/regionmap.cpp b/apps/opencs/model/world/regionmap.cpp index 6dbbac97fb..557a8303b5 100644 --- a/apps/opencs/model/world/regionmap.cpp +++ b/apps/opencs/model/world/regionmap.cpp @@ -2,6 +2,7 @@ #include #include +#include #include diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index b40ab13892..cd9f58e848 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -20,13 +21,11 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const * mFiles.clear(); mIndex.clear(); - int baseSize = mBaseDirectory.size(); + size_t baseSize = mBaseDirectory.size(); - const std::map& index = vfs->getIndex(); - for (std::map::const_iterator it = index.begin(); it != index.end(); ++it) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator("")) { - std::string filepath = it->first; - if (static_cast (filepath.size())(mFiles.size()); } std::string CSMWorld::Resources::getId (int index) const @@ -83,7 +82,7 @@ int CSMWorld::Resources::getIndex (const std::string& id) const return index; } -int CSMWorld::Resources::searchId (const std::string& id) const +int CSMWorld::Resources::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase (id); diff --git a/apps/opencs/model/world/resources.hpp b/apps/opencs/model/world/resources.hpp index 5e9872ea84..2de13a259e 100644 --- a/apps/opencs/model/world/resources.hpp +++ b/apps/opencs/model/world/resources.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "universalid.hpp" @@ -25,9 +26,9 @@ namespace CSMWorld /// \param type Type of resources in this table. Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, - const char * const *extensions = 0); + const char * const *extensions = nullptr); - void recreate(const VFS::Manager* vfs, const char * const *extensions = 0); + void recreate(const VFS::Manager* vfs, const char * const *extensions = nullptr); int getSize() const; @@ -35,7 +36,7 @@ namespace CSMWorld int getIndex (const std::string& id) const; - int searchId (const std::string& id) const; + int searchId(std::string_view id) const; UniversalId::Type getType() const; }; diff --git a/apps/opencs/model/world/resourcesmanager.cpp b/apps/opencs/model/world/resourcesmanager.cpp index 1af9c5e9b1..378ba7c6bb 100644 --- a/apps/opencs/model/world/resourcesmanager.cpp +++ b/apps/opencs/model/world/resourcesmanager.cpp @@ -17,7 +17,7 @@ void CSMWorld::ResourcesManager::addResources (const Resources& resources) const char * const * CSMWorld::ResourcesManager::getMeshExtensions() { // maybe we could go over the osgDB::Registry to list all supported node formats - static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", 0 }; + static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae", 0 }; return sMeshTypes; } diff --git a/apps/opencs/model/world/resourcetable.hpp b/apps/opencs/model/world/resourcetable.hpp index 8a3fab8a21..34d8298d9a 100644 --- a/apps/opencs/model/world/resourcetable.hpp +++ b/apps/opencs/model/world/resourcetable.hpp @@ -16,7 +16,7 @@ namespace CSMWorld /// \note The feature Feature_Constant will be added implicitly. ResourceTable (const Resources *resources, unsigned int features = 0); - virtual ~ResourceTable(); + ~ResourceTable() override; int rowCount (const QModelIndex & parent = QModelIndex()) const override; diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 344ae322e9..2b6a9231b1 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -1,6 +1,7 @@ #include "scriptcontext.hpp" #include +#include #include @@ -102,11 +103,6 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); } -bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const -{ - return mData.getJournals().searchId (name)!=-1; -} - void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; diff --git a/apps/opencs/model/world/scriptcontext.hpp b/apps/opencs/model/world/scriptcontext.hpp index 8e1a5e57b8..cb08fc70bd 100644 --- a/apps/opencs/model/world/scriptcontext.hpp +++ b/apps/opencs/model/world/scriptcontext.hpp @@ -39,9 +39,6 @@ namespace CSMWorld bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? - void invalidateIds(); void clear(); diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 486f3770aa..1f11e21bdc 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -1,6 +1,5 @@ #include "universalid.hpp" -#include #include #include #include @@ -405,8 +404,3 @@ bool CSMWorld::operator< (const UniversalId& left, const UniversalId& right) { return left.isLess (right); } - -std::ostream& CSMWorld::operator< (std::ostream& stream, const CSMWorld::UniversalId& universalId) -{ - return stream << universalId.toString(); -} diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index accd1b78da..09fd8f3bb9 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -2,7 +2,6 @@ #define CSM_WOLRD_UNIVERSALID_H #include -#include #include #include @@ -199,8 +198,6 @@ namespace CSMWorld bool operator!= (const UniversalId& left, const UniversalId& right); bool operator< (const UniversalId& left, const UniversalId& right); - - std::ostream& operator< (std::ostream& stream, const UniversalId& universalId); } Q_DECLARE_METATYPE (CSMWorld::UniversalId) diff --git a/apps/opencs/view/doc/adjusterwidget.hpp b/apps/opencs/view/doc/adjusterwidget.hpp index 91e308236f..cec9ca2291 100644 --- a/apps/opencs/view/doc/adjusterwidget.hpp +++ b/apps/opencs/view/doc/adjusterwidget.hpp @@ -32,7 +32,7 @@ namespace CSVDoc public: - AdjusterWidget (QWidget *parent = 0); + AdjusterWidget (QWidget *parent = nullptr); void setLocalData (const boost::filesystem::path& localData); void setAction (ContentAction action); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 7a3fe398f4..1eca8d68a6 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -1,15 +1,7 @@ #include "filedialog.hpp" -#include #include #include -#include -#include -#include -#include -#include -#include -#include #include "components/contentselector/model/esmfile.hpp" #include "components/contentselector/view/contentselector.hpp" @@ -18,19 +10,24 @@ #include "adjusterwidget.hpp" CSVDoc::FileDialog::FileDialog(QWidget *parent) : - QDialog(parent), mSelector (0), mAction(ContentAction_Undefined), mFileWidget (0), mAdjusterWidget (0), mDialogBuilt(false) + QDialog(parent), mSelector (nullptr), mAction(ContentAction_Undefined), mFileWidget (nullptr), mAdjusterWidget (nullptr), mDialogBuilt(false) { ui.setupUi (this); resize(400, 400); setObjectName ("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget (this); } -void CSVDoc::FileDialog::addFiles(const QString &path) +void CSVDoc::FileDialog::addFiles(const std::vector& dataDirs) { - mSelector->addFiles(path); + for (auto iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) + { + QString path = QString::fromUtf8(iter->string().c_str()); + mSelector->addFiles(path); + } + mSelector->sortFiles(); } void CSVDoc::FileDialog::setEncoding(const QString &encoding) @@ -161,7 +158,7 @@ void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString &name, bool) bool isNew = (mAction == ContentAction_New); if (isNew) - success = success && !(name.isEmpty()); + success = !name.isEmpty(); else if (success) { ContentSelectorModel::EsmFile *file = mSelector->selectedFiles().back(); diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index bec2c68695..6a15b46b7c 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -42,10 +42,10 @@ namespace CSVDoc public: - explicit FileDialog(QWidget *parent = 0); + explicit FileDialog(QWidget *parent = nullptr); void showDialog (ContentAction action); - void addFiles (const QString &path); + void addFiles(const std::vector& dataDirs); void setEncoding (const QString &encoding); void clearFiles (); diff --git a/apps/opencs/view/doc/filewidget.hpp b/apps/opencs/view/doc/filewidget.hpp index ab06f37f18..626b8d77d8 100644 --- a/apps/opencs/view/doc/filewidget.hpp +++ b/apps/opencs/view/doc/filewidget.hpp @@ -23,7 +23,7 @@ namespace CSVDoc public: - FileWidget (QWidget *parent = 0); + FileWidget (QWidget *parent = nullptr); void setType (bool addon); diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.cpp b/apps/opencs/view/doc/globaldebugprofilemenu.cpp index f0d9655dd0..c898b819c2 100644 --- a/apps/opencs/view/doc/globaldebugprofilemenu.cpp +++ b/apps/opencs/view/doc/globaldebugprofilemenu.cpp @@ -13,7 +13,7 @@ void CSVDoc::GlobalDebugProfileMenu::rebuild() clear(); delete mActions; - mActions = 0; + mActions = nullptr; int idColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Id); int stateColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); @@ -48,7 +48,7 @@ void CSVDoc::GlobalDebugProfileMenu::rebuild() CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent) -: QMenu (parent), mDebugProfiles (debugProfiles), mActions (0) +: QMenu (parent), mDebugProfiles (debugProfiles), mActions (nullptr) { rebuild(); diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.hpp b/apps/opencs/view/doc/globaldebugprofilemenu.hpp index 0d7906ccef..e12ee306a1 100644 --- a/apps/opencs/view/doc/globaldebugprofilemenu.hpp +++ b/apps/opencs/view/doc/globaldebugprofilemenu.hpp @@ -26,7 +26,7 @@ namespace CSVDoc public: - GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = 0); + GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = nullptr); void updateActions (bool running); diff --git a/apps/opencs/view/doc/loader.cpp b/apps/opencs/view/doc/loader.cpp index 49a53e1794..2420b87c6c 100644 --- a/apps/opencs/view/doc/loader.cpp +++ b/apps/opencs/view/doc/loader.cpp @@ -17,7 +17,7 @@ void CSVDoc::LoadingDocument::closeEvent (QCloseEvent *event) } CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) -: mDocument (document), mAborted (false), mMessages (0), mTotalRecords (0) +: mDocument (document), mTotalRecordsLabel (0), mRecordsLabel (0), mAborted (false), mMessages (nullptr), mRecords(0) { setWindowTitle (QString::fromUtf8((std::string("Opening ") + document->getSavePath().filename().string()).c_str())); @@ -25,26 +25,25 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) mLayout = new QVBoxLayout (this); - // file progress - mFile = new QLabel (this); + // total progress + mTotalRecordsLabel = new QLabel (this); - mLayout->addWidget (mFile); + mLayout->addWidget (mTotalRecordsLabel); - mFileProgress = new QProgressBar (this); + mTotalProgress = new QProgressBar (this); - mLayout->addWidget (mFileProgress); + mLayout->addWidget (mTotalProgress); - int size = static_cast (document->getContentFiles().size())+1; - if (document->isNew()) - --size; + mTotalProgress->setMinimum (0); + mTotalProgress->setMaximum (document->getData().getTotalRecords(document->getContentFiles())); + mTotalProgress->setTextVisible (true); + mTotalProgress->setValue (0); + mTotalRecords = 0; - mFileProgress->setMinimum (0); - mFileProgress->setMaximum (size); - mFileProgress->setTextVisible (true); - mFileProgress->setValue (0); + mFilesLoaded = 0; // record progress - mLayout->addWidget (mRecords = new QLabel ("Records", this)); + mLayout->addWidget (mRecordsLabel = new QLabel ("Records", this)); mRecordProgress = new QProgressBar (this); @@ -74,29 +73,32 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) connect (mButtons, SIGNAL (rejected()), this, SLOT (cancel())); } -void CSVDoc::LoadingDocument::nextStage (const std::string& name, int totalRecords) +void CSVDoc::LoadingDocument::nextStage (const std::string& name, int fileRecords) { - mFile->setText (QString::fromUtf8 (("Loading: " + name).c_str())); + ++mFilesLoaded; + size_t numFiles = mDocument->getContentFiles().size(); - mFileProgress->setValue (mFileProgress->value()+1); + mTotalRecordsLabel->setText (QString::fromUtf8 (("Loading: "+name + +" ("+std::to_string(mFilesLoaded)+" of "+std::to_string((numFiles))+")").c_str())); + + mTotalRecords = mTotalProgress->value(); mRecordProgress->setValue (0); - mRecordProgress->setMaximum (totalRecords>0 ? totalRecords : 1); + mRecordProgress->setMaximum (fileRecords>0 ? fileRecords : 1); - mTotalRecords = totalRecords; + mRecords = fileRecords; } void CSVDoc::LoadingDocument::nextRecord (int records) { - if (records<=mTotalRecords) + if (records <= mRecords) { - mRecordProgress->setValue (records); - - std::ostringstream stream; + mTotalProgress->setValue (mTotalRecords+records); - stream << "Records: " << records << " of " << mTotalRecords; + mRecordProgress->setValue(records); - mRecords->setText (QString::fromUtf8 (stream.str().c_str())); + mRecordsLabel->setText(QString::fromStdString( + "Records: "+std::to_string(records)+" of "+std::to_string(mRecords))); } } @@ -176,12 +178,12 @@ void CSVDoc::Loader::loadingStopped (CSMDoc::Document *document, bool completed, } void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, - int totalRecords) + int fileRecords) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) - iter->second->nextStage (name, totalRecords); + iter->second->nextStage (name, fileRecords); } void CSVDoc::Loader::nextRecord (CSMDoc::Document *document, int records) diff --git a/apps/opencs/view/doc/loader.hpp b/apps/opencs/view/doc/loader.hpp index 24cbee7884..80d986283d 100644 --- a/apps/opencs/view/doc/loader.hpp +++ b/apps/opencs/view/doc/loader.hpp @@ -5,7 +5,6 @@ #include #include -#include class QLabel; class QProgressBar; @@ -25,16 +24,18 @@ namespace CSVDoc Q_OBJECT CSMDoc::Document *mDocument; - QLabel *mFile; - QLabel *mRecords; - QProgressBar *mFileProgress; + QLabel *mTotalRecordsLabel; + QLabel *mRecordsLabel; + QProgressBar *mTotalProgress; QProgressBar *mRecordProgress; bool mAborted; QDialogButtonBox *mButtons; QLabel *mError; QListWidget *mMessages; QVBoxLayout *mLayout; + int mRecords; int mTotalRecords; + int mFilesLoaded; private: @@ -75,7 +76,7 @@ namespace CSVDoc Loader(); - virtual ~Loader(); + ~Loader() override; signals: diff --git a/apps/opencs/view/doc/newgame.cpp b/apps/opencs/view/doc/newgame.cpp index 7b247652ed..dc7e12423e 100644 --- a/apps/opencs/view/doc/newgame.cpp +++ b/apps/opencs/view/doc/newgame.cpp @@ -1,7 +1,6 @@ #include "newgame.hpp" -#include -#include +#include #include #include #include diff --git a/apps/opencs/view/doc/operation.cpp b/apps/opencs/view/doc/operation.cpp index e5c1e7b89e..c3714a001e 100644 --- a/apps/opencs/view/doc/operation.cpp +++ b/apps/opencs/view/doc/operation.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "../../model/doc/document.hpp" diff --git a/apps/opencs/view/doc/operation.hpp b/apps/opencs/view/doc/operation.hpp index 48839fada4..1d07464604 100644 --- a/apps/opencs/view/doc/operation.hpp +++ b/apps/opencs/view/doc/operation.hpp @@ -28,7 +28,7 @@ namespace CSVDoc public: Operation (int type, QWidget *parent); - ~Operation(); + ~Operation() override; void setProgress (int current, int max, int threads); diff --git a/apps/opencs/view/doc/operations.cpp b/apps/opencs/view/doc/operations.cpp index 7ee4b87260..2177679b00 100644 --- a/apps/opencs/view/doc/operations.cpp +++ b/apps/opencs/view/doc/operations.cpp @@ -1,7 +1,6 @@ #include "operations.hpp" #include -#include #include "operation.hpp" @@ -30,7 +29,7 @@ void CSVDoc::Operations::setProgress (int current, int max, int type, int thread return; } - int oldCount = mOperations.size(); + int oldCount = static_cast(mOperations.size()); int newCount = oldCount + 1; Operation *operation = new Operation (type, this); @@ -51,7 +50,7 @@ void CSVDoc::Operations::quitOperation (int type) for (std::vector::iterator iter (mOperations.begin()); iter!=mOperations.end(); ++iter) if ((*iter)->getType()==type) { - int oldCount = mOperations.size(); + int oldCount = static_cast(mOperations.size()); int newCount = oldCount - 1; mLayout->removeItem ((*iter)->getLayout()); diff --git a/apps/opencs/view/doc/sizehint.hpp b/apps/opencs/view/doc/sizehint.hpp index 1b3c52eb8a..949265e31a 100644 --- a/apps/opencs/view/doc/sizehint.hpp +++ b/apps/opencs/view/doc/sizehint.hpp @@ -11,8 +11,8 @@ namespace CSVDoc QSize mSize; public: - SizeHintWidget(QWidget *parent = 0); - ~SizeHintWidget(); + SizeHintWidget(QWidget *parent = nullptr); + ~SizeHintWidget() override; QSize sizeHint() const override; void setSizeHint(const QSize &size); diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 3a1950a6e3..df4701d8c1 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -1,7 +1,6 @@ #include "startup.hpp" -#include -#include +#include #include #include #include diff --git a/apps/opencs/view/doc/subview.cpp b/apps/opencs/view/doc/subview.cpp index 82cbe835e7..a566a6e26f 100644 --- a/apps/opencs/view/doc/subview.cpp +++ b/apps/opencs/view/doc/subview.cpp @@ -2,7 +2,6 @@ #include "view.hpp" -#include #include #include diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index ac7c8ebf97..68658248eb 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -6,13 +6,9 @@ #include #include #include -#include -#include #include #include #include -#include -#include #include #include @@ -25,6 +21,8 @@ #include "../world/subviews.hpp" #include "../world/scenesubview.hpp" #include "../world/tablesubview.hpp" +#include "../world/dialoguesubview.hpp" +#include "../world/scriptsubview.hpp" #include "../tools/subviews.hpp" @@ -65,6 +63,8 @@ void CSVDoc::View::setupFileMenu() QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); connect (save, SIGNAL (triggered()), this, SLOT (save())); mSave = save; + + file->addSeparator(); QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); connect (verify, SIGNAL (triggered()), this, SLOT (verify())); @@ -80,6 +80,8 @@ void CSVDoc::View::setupFileMenu() QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); connect (meta, SIGNAL (triggered()), this, SLOT (addMetaDataSubView())); + file->addSeparator(); + QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); connect (close, SIGNAL (triggered()), this, SLOT (close())); @@ -156,17 +158,16 @@ void CSVDoc::View::setupWorldMenu() { QMenu *world = menuBar()->addMenu (tr ("World")); - QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); - connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); - - QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); - connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); - QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); connect (referenceables, SIGNAL (triggered()), this, SLOT (addReferenceablesSubView())); QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); + + world->addSeparator(); + + QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); + connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); QAction *lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); @@ -177,7 +178,10 @@ void CSVDoc::View::setupWorldMenu() QAction *grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); - world->addSeparator(); // items that don't represent single record lists follow here + world->addSeparator(); + + QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); + connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); QAction *regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); @@ -187,14 +191,19 @@ void CSVDoc::View::setupMechanicsMenu() { QMenu *mechanics = menuBar()->addMenu (tr ("Mechanics")); + QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); + connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); + connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); + QAction* globals = createMenuEntry(CSMWorld::UniversalId::Type_Globals, mechanics, "document-mechanics-globals"); connect (globals, SIGNAL (triggered()), this, SLOT (addGlobalsSubView())); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); connect (gmsts, SIGNAL (triggered()), this, SLOT (addGmstsSubView())); - - QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); - connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + mechanics->addSeparator(); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); @@ -204,9 +213,6 @@ void CSVDoc::View::setupMechanicsMenu() QAction* magicEffects = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); connect (magicEffects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); - - QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); - connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); } void CSVDoc::View::setupCharacterMenu() @@ -227,21 +233,25 @@ void CSVDoc::View::setupCharacterMenu() QAction* birthsigns = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); connect (birthsigns, SIGNAL (triggered()), this, SLOT (addBirthsignsSubView())); + + QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); + connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); + characters->addSeparator(); + QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); connect (topics, SIGNAL (triggered()), this, SLOT (addTopicsSubView())); - QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); - connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); - QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); + + characters->addSeparator(); + + QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); + connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); QAction* journalInfos = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); - - QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); - connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); } void CSVDoc::View::setupAssetsMenu() @@ -438,8 +448,8 @@ void CSVDoc::View::updateActions() for (std::vector::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter) (*iter)->setEnabled (editing); - mUndo->setEnabled (editing & mDocument->getUndoStack().canUndo()); - mRedo->setEnabled (editing & mDocument->getUndoStack().canRedo()); + mUndo->setEnabled (editing && mDocument->getUndoStack().canUndo()); + mRedo->setEnabled (editing && mDocument->getUndoStack().canRedo()); mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running); mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying)); @@ -649,6 +659,17 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin this, SLOT(onRequestFocus(const std::string&))); } + if (CSMPrefs::State::get()["ID Tables"]["subview-new-window"].isTrue()) + { + CSVWorld::DialogueSubView* dialogueView = dynamic_cast(view); + if (dialogueView) + dialogueView->setFloating(true); + + CSVWorld::ScriptSubView* scriptView = dynamic_cast(view); + if (scriptView) + scriptView->setFloating(true); + } + view->show(); if (!hint.empty()) @@ -733,7 +754,7 @@ void CSVDoc::View::infoAbout() #endif // Get current year - time_t now = time(NULL); + time_t now = time(nullptr); struct tm tstruct; char copyrightInfo[40]; tstruct = *localtime(&now); @@ -749,7 +770,7 @@ void CSVDoc::View::infoAbout() "%4https://openmw.org" "%5https://forum.openmw.org" "%6https://gitlab.com/OpenMW/openmw/issues" - "%7irc://irc.freenode.net/#openmw" + "%7ircs://irc.libera.chat/#openmw" "" "

") .arg(versionInfo diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 322bcdfb7f..c0d036daae 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -110,7 +110,7 @@ namespace CSVDoc ///< The ownership of \a document is not transferred to *this. - virtual ~View(); + ~View() override; const CSMDoc::Document *getDocument() const; diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 8cca3a849e..b417a5b499 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -4,14 +4,12 @@ #include #include -#include #include #include #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" -#include "../../model/world/universalid.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/prefs/state.hpp" diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 70431107f6..fe136027ca 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -43,7 +43,7 @@ namespace CSVDoc ViewManager& operator= (const ViewManager&); void updateIndices(); - bool notifySaveOnClose (View *view = 0); + bool notifySaveOnClose (View *view = nullptr); bool showModifiedDocumentMessageBox (View *view); bool showSaveInProgressMessageBox (View *view); bool removeDocument(View *view); @@ -52,7 +52,7 @@ namespace CSVDoc ViewManager (CSMDoc::DocumentManager& documentManager); - virtual ~ViewManager(); + ~ViewManager() override; View *addView (CSMDoc::Document *document); ///< The ownership of the returned view is not transferred. diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 6b585591fa..6db8751a0c 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -1,6 +1,7 @@ #include "editwidget.hpp" -#include +#include + #include #include #include @@ -42,6 +43,8 @@ CSVFilter::EditWidget::EditWidget (CSMWorld::Data& data, QWidget *parent) addAction (mHelpAction); auto* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); + + setText("!string(\"ID\", \".*\")"); } void CSVFilter::EditWidget::textChanged (const QString& text) diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index b47a884a38..c15e4ef913 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -3,7 +3,6 @@ #include #include -#include #include "../../model/filter/parser.hpp" #include "../../model/filter/node.hpp" @@ -30,7 +29,7 @@ namespace CSVFilter public: - EditWidget (CSMWorld::Data& data, QWidget *parent = 0); + EditWidget (CSMWorld::Data& data, QWidget *parent = nullptr); void createFilterRequest(std::vector > >& filterSource, Qt::DropAction action); diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index 94aa80b844..e01f77ce6b 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -4,7 +4,6 @@ #include #include -#include #include "../../model/filter/node.hpp" #include "../../model/world/universalid.hpp" @@ -25,7 +24,7 @@ namespace CSVFilter RecordFilterBox *mRecordFilterBox; public: - FilterBox (CSMWorld::Data& data, QWidget *parent = 0); + FilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); void setRecordFilter (const std::string& filter); diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 77a07c92bc..b6e04b2758 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -2,9 +2,6 @@ #define CSV_FILTER_RECORDFILTERBOX_H #include -#include - -#include #include "../../model/filter/node.hpp" @@ -25,7 +22,7 @@ namespace CSVFilter public: - RecordFilterBox (CSMWorld::Data& data, QWidget *parent = 0); + RecordFilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); void setFilter (const std::string& filter); diff --git a/apps/opencs/view/prefs/dialogue.cpp b/apps/opencs/view/prefs/dialogue.cpp index 7e41fcf822..2dd92901ea 100644 --- a/apps/opencs/view/prefs/dialogue.cpp +++ b/apps/opencs/view/prefs/dialogue.cpp @@ -1,9 +1,7 @@ #include "dialogue.hpp" #include -#include #include -#include #include #include #include diff --git a/apps/opencs/view/prefs/dialogue.hpp b/apps/opencs/view/prefs/dialogue.hpp index 2e09756496..00b2e10ec9 100644 --- a/apps/opencs/view/prefs/dialogue.hpp +++ b/apps/opencs/view/prefs/dialogue.hpp @@ -30,7 +30,7 @@ namespace CSVPrefs Dialogue(); - virtual ~Dialogue(); + ~Dialogue() override; protected: diff --git a/apps/opencs/view/prefs/keybindingpage.cpp b/apps/opencs/view/prefs/keybindingpage.cpp index eed5c0eb8e..39c9f78ec1 100644 --- a/apps/opencs/view/prefs/keybindingpage.cpp +++ b/apps/opencs/view/prefs/keybindingpage.cpp @@ -16,9 +16,9 @@ namespace CSVPrefs { KeyBindingPage::KeyBindingPage(CSMPrefs::Category& category, QWidget* parent) : PageBase(category, parent) - , mStackedLayout(0) - , mPageLayout(0) - , mPageSelector(0) + , mStackedLayout(nullptr) + , mPageLayout(nullptr) + , mPageSelector(nullptr) { // Need one widget for scroll area QWidget* topWidget = new QWidget(); diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index d6077a65a5..d33c6f3f28 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include #include @@ -96,7 +96,7 @@ namespace CSVRender for (int i = 0; i < ESM::PRT_Count; ++i) { auto type = (ESM::PartReferenceType) i; - std::string partId = mActorData->getPart(type); + const std::string_view partId = mActorData->getPart(type); attachBodyPart(type, getBodyPartMesh(partId)); } } @@ -111,11 +111,11 @@ namespace CSVRender if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(mesh); - SceneUtil::attach(instance, mSkeleton, boneName, node->second); + SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } - std::string Actor::getBodyPartMesh(const std::string& bodyPartId) + std::string Actor::getBodyPartMesh(std::string_view bodyPartId) { const auto& bodyParts = mData.getBodyParts(); diff --git a/apps/opencs/view/render/actor.hpp b/apps/opencs/view/render/actor.hpp index 2f19454f78..414cd438d7 100644 --- a/apps/opencs/view/render/actor.hpp +++ b/apps/opencs/view/render/actor.hpp @@ -2,12 +2,13 @@ #define OPENCS_VIEW_RENDER_ACTOR_H #include +#include #include #include -#include +#include #include #include "../../model/world/actoradapter.hpp" @@ -54,7 +55,7 @@ namespace CSVRender void loadBodyParts(); void attachBodyPart(ESM::PartReferenceType, const std::string& mesh); - std::string getBodyPartMesh(const std::string& bodyPartId); + std::string getBodyPartMesh(std::string_view bodyPartId); static const std::string MeshPrefix; diff --git a/apps/opencs/view/render/brushdraw.cpp b/apps/opencs/view/render/brushdraw.cpp index 255a13a12e..6b33e336ea 100644 --- a/apps/opencs/view/render/brushdraw.cpp +++ b/apps/opencs/view/render/brushdraw.cpp @@ -18,6 +18,8 @@ CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textur mBrushDrawNode = new osg::Group(); mGeometry = new osg::Geometry(); mBrushDrawNode->addChild(mGeometry); + mBrushDrawNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mBrushDrawNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mParentNode->addChild(mBrushDrawNode); if (mTextureMode) mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_TEXTURE_SIZE); @@ -122,7 +124,14 @@ void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::V const float brushOutlineHeight (1.0f); float diameter = radius * 2; int resolution = static_cast(2.f * diameter / mLandSizeFactor); //half a vertex resolution - float resAdjustedLandSizeFactor = mLandSizeFactor / 2; + float resAdjustedLandSizeFactor = mLandSizeFactor / 2; //128 + + if (resolution > 128) // limit accuracy for performance + { + resolution = 128; + resAdjustedLandSizeFactor = diameter / resolution; + } + osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < resolution; i++) @@ -215,7 +224,8 @@ void CSVRender::BrushDraw::buildCircleGeometry(const float& radius, const osg::V osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); - const int amountOfPoints = (osg::PI * 2.0f) * radius / 20; + + const int amountOfPoints = 128; const float step ((osg::PI * 2.0f) / static_cast(amountOfPoints)); const float brushOutlineHeight (1.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); diff --git a/apps/opencs/view/render/brushdraw.hpp b/apps/opencs/view/render/brushdraw.hpp index 0551631cd9..f95a0c5a7c 100644 --- a/apps/opencs/view/render/brushdraw.hpp +++ b/apps/opencs/view/render/brushdraw.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "../widget/brushshapes.hpp" namespace CSVRender diff --git a/apps/opencs/view/render/cameracontroller.cpp b/apps/opencs/view/render/cameracontroller.cpp index 5dbb7a28c1..f21224d73d 100644 --- a/apps/opencs/view/render/cameracontroller.cpp +++ b/apps/opencs/view/render/cameracontroller.cpp @@ -126,7 +126,7 @@ namespace CSVRender { // Try again without any mask boundsVisitor.reset(); - boundsVisitor.setTraversalMask(~0); + boundsVisitor.setTraversalMask(~0u); root->accept(boundsVisitor); // Last resort, set a default @@ -458,7 +458,7 @@ namespace CSVRender , mDown(false) , mRollLeft(false) , mRollRight(false) - , mPickingMask(~0) + , mPickingMask(~0u) , mCenter(0,0,0) , mDistance(0) , mOrbitSpeed(osg::PI / 4) diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 23b5aa91e4..9ece61a979 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -1,21 +1,14 @@ #include "cell.hpp" #include -#include -#include #include #include -#include -#include -#include +#include +#include #include #include "../../model/world/idtable.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/refcollection.hpp" -#include "../../model/world/cellcoordinates.hpp" #include "cellwater.hpp" #include "cellborder.hpp" @@ -25,6 +18,7 @@ #include "pathgrid.hpp" #include "terrainstorage.hpp" #include "object.hpp" +#include "instancedragmodes.hpp" namespace CSVRender { @@ -90,7 +84,7 @@ bool CSVRender::Cell::addObjects (int start, int end) { std::string id = Misc::StringUtils::lowerCase (collection.getRecord (i).get().mId); - std::unique_ptr object (new Object (mData, mCellNode, id, false)); + auto object = std::make_unique(mData, mCellNode, id, false); if (mSubModeElementMask & Mask_Reference) object->setSubMode (mSubMode); @@ -133,14 +127,14 @@ void CSVRender::Cell::updateLand() } else { - mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, - mData.getResourceSystem().get(), mTerrainStorage, Mask_Terrain)); + mTerrain = std::make_unique(mCellNode, mCellNode, + mData.getResourceSystem().get(), mTerrainStorage, Mask_Terrain); } mTerrain->loadCell(esmLand.mX, esmLand.mY); if (!mCellBorder) - mCellBorder.reset(new CellBorder(mCellNode, mCoordinates)); + mCellBorder = std::make_unique(mCellNode, mCoordinates); mCellBorder->buildShape(esmLand); @@ -191,8 +185,8 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st updateLand(); - mPathgrid.reset(new Pathgrid(mData, mCellNode, mId, mCoordinates)); - mCellWater.reset(new CellWater(mData, mCellNode, mId, mCoordinates)); + mPathgrid = std::make_unique(mData, mCellNode, mId, mCoordinates); + mCellWater = std::make_unique(mData, mCellNode, mId, mCoordinates); } } @@ -496,6 +490,50 @@ void CSVRender::Cell::selectAllWithSameParentId (int elementMask) } } +void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) +{ + if (dragMode == DragMode_Select_Only || dragMode == DragMode_Select_Add) + object->setSelected(true); + + else if (dragMode == DragMode_Select_Remove) + object->setSelected(false); + + else if (dragMode == DragMode_Select_Invert) + object->setSelected (!object->getSelected()); +} + +void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) +{ + for (auto& object : mObjects) + { + if (dragMode == DragMode_Select_Only) object.second->setSelected (false); + + if ( ( object.second->getPosition().pos[0] > pointA[0] && object.second->getPosition().pos[0] < pointB[0] ) || + ( object.second->getPosition().pos[0] > pointB[0] && object.second->getPosition().pos[0] < pointA[0] )) + { + if ( ( object.second->getPosition().pos[1] > pointA[1] && object.second->getPosition().pos[1] < pointB[1] ) || + ( object.second->getPosition().pos[1] > pointB[1] && object.second->getPosition().pos[1] < pointA[1] )) + { + if ( ( object.second->getPosition().pos[2] > pointA[2] && object.second->getPosition().pos[2] < pointB[2] ) || + ( object.second->getPosition().pos[2] > pointB[2] && object.second->getPosition().pos[2] < pointA[2] )) + handleSelectDrag(object.second, dragMode); + } + + } + } +} + +void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) +{ + for (auto& object : mObjects) + { + if (dragMode == DragMode_Select_Only) object.second->setSelected (false); + + float distanceFromObject = (point - object.second->getPosition().asVec3()).length(); + if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode); + } +} + void CSVRender::Cell::setCellArrows (int mask) { for (int i=0; i<4; ++i) @@ -504,12 +542,12 @@ void CSVRender::Cell::setCellArrows (int mask) bool enable = mask & direction; - if (enable!=(mCellArrows[i].get()!=0)) + if (enable!=(mCellArrows[i].get()!=nullptr)) { if (enable) - mCellArrows[i].reset (new CellArrow (mCellNode, direction, mCoordinates)); + mCellArrows[i] = std::make_unique(mCellNode, direction, mCoordinates); else - mCellArrows[i].reset (0); + mCellArrows[i].reset (nullptr); } } } @@ -528,7 +566,7 @@ void CSVRender::Cell::setCellMarker() } if (!isInteriorCell) { - mCellMarker.reset(new CellMarker(mCellNode, mCoordinates, cellExists)); + mCellMarker = std::make_unique(mCellNode, mCoordinates, cellExists); } } diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index 281ac67356..12f6f0ccdb 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -10,6 +10,7 @@ #include "../../model/world/cellcoordinates.hpp" #include "terrainstorage.hpp" +#include "instancedragmodes.hpp" class QModelIndex; @@ -17,7 +18,6 @@ namespace osg { class Group; class Geometry; - class Geode; } namespace CSMWorld @@ -152,6 +152,12 @@ namespace CSVRender // already selected void selectAllWithSameParentId (int elementMask); + void handleSelectDrag(Object* object, DragMode dragMode); + + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); + + void selectWithinDistance(const osg::Vec3d& pointA, float distance, DragMode dragMode); + void setCellArrows (int mask); /// \brief Set marker for this cell. diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index b6fee15459..9ad4932698 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -1,14 +1,11 @@ - #include "cellarrow.hpp" #include #include -#include #include #include #include "../../model/prefs/state.hpp" -#include "../../model/prefs/shortcutmanager.hpp" #include @@ -23,7 +20,7 @@ CSVRender::CellArrow *CSVRender::CellArrowTag::getCellArrow() const return mArrow; } -QString CSVRender::CellArrowTag::getToolTip (bool hideBasics) const +QString CSVRender::CellArrowTag::getToolTip(bool hideBasics, const WorldspaceHitResult& /*hit*/) const { QString text ("Direction: "); @@ -60,7 +57,7 @@ void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; - const int offset = cellSize / 2 + 800; + const int offset = cellSize / 2 + 600; int x = mCoordinates.getX()*cellSize + cellSize/2; int y = mCoordinates.getY()*cellSize + cellSize/2; @@ -92,9 +89,9 @@ void CSVRender::CellArrow::buildShape() { osg::ref_ptr geometry (new osg::Geometry); - const int arrowWidth = 4000; - const int arrowLength = 1500; - const int arrowHeight = 500; + const int arrowWidth = 2700; + const int arrowLength = 1350; + const int arrowHeight = 300; osg::Vec3Array *vertices = new osg::Vec3Array; for (int i2=0; i2<2; ++i2) @@ -151,18 +148,15 @@ void CSVRender::CellArrow::buildShape() osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (1.0f, 0.0f, 0.0f, 1.0f)); + colours->push_back (osg::Vec4f (0.11f, 0.6f, 0.95f, 1.0f)); for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.8f, (i==2 || i==5) ? 0.6f : 0.4f, 0.0f, 1.0f)); + colours->push_back (osg::Vec4f (0.08f, 0.44f, 0.7f, 1.0f)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable (geometry); - - mBaseNode->addChild (geode); + mBaseNode->addChild (geometry); } CSVRender::CellArrow::CellArrow (osg::Group *cellNode, Direction direction, diff --git a/apps/opencs/view/render/cellarrow.hpp b/apps/opencs/view/render/cellarrow.hpp index 9a49b80db0..ed71410610 100644 --- a/apps/opencs/view/render/cellarrow.hpp +++ b/apps/opencs/view/render/cellarrow.hpp @@ -27,7 +27,7 @@ namespace CSVRender CellArrow *getCellArrow() const; - QString getToolTip (bool hideBasics) const override; + QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; }; diff --git a/apps/opencs/view/render/cellborder.cpp b/apps/opencs/view/render/cellborder.cpp index 6073807ce0..b93b5d1fcf 100644 --- a/apps/opencs/view/render/cellborder.cpp +++ b/apps/opencs/view/render/cellborder.cpp @@ -2,26 +2,33 @@ #include #include -#include #include #include -#include +#include #include "mask.hpp" #include "../../model/world/cellcoordinates.hpp" const int CSVRender::CellBorder::CellSize = ESM::Land::REAL_SIZE; -const int CSVRender::CellBorder::VertexCount = (ESM::Land::LAND_SIZE * 4) - 3; + +/* + The number of vertices per cell border is equal to the number of vertices per edge + minus the duplicated corner vertices. An additional vertex to close the loop is NOT needed. +*/ +const int CSVRender::CellBorder::VertexCount = (ESM::Land::LAND_SIZE * 4) - 4; CSVRender::CellBorder::CellBorder(osg::Group* cellNode, const CSMWorld::CellCoordinates& coords) : mParentNode(cellNode) { + mBorderGeometry = new osg::Geometry(); + mBaseNode = new osg::PositionAttitudeTransform(); mBaseNode->setNodeMask(Mask_CellBorder); mBaseNode->setPosition(osg::Vec3f(coords.getX() * CellSize, coords.getY() * CellSize, 10)); + mBaseNode->addChild(mBorderGeometry); mParentNode->addChild(mBaseNode); } @@ -38,56 +45,59 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) if (!landData) return; - osg::ref_ptr geometry = new osg::Geometry(); + mBaseNode->removeChild(mBorderGeometry); + mBorderGeometry = new osg::Geometry(); - // Vertices osg::ref_ptr vertices = new osg::Vec3Array(); - int x = 0, y = 0; - for (; x < ESM::Land::LAND_SIZE; ++x) + int x = 0; + int y = 0; + + /* + 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)])); x = ESM::Land::LAND_SIZE - 1; - for (; y < ESM::Land::LAND_SIZE; ++y) + 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) + for (; x > 0; --x) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); x = 0; - for (; y >= 0; --y) + for (; y > 0; --y) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - geometry->setVertexArray(vertices); + mBorderGeometry->setVertexArray(vertices); - // Color osg::ref_ptr colors = new osg::Vec4Array(); colors->push_back(osg::Vec4f(0.f, 0.5f, 0.f, 1.f)); - geometry->setColorArray(colors, osg::Array::BIND_PER_PRIMITIVE_SET); + mBorderGeometry->setColorArray(colors, osg::Array::BIND_PER_PRIMITIVE_SET); - // Primitive osg::ref_ptr primitives = - new osg::DrawElementsUShort(osg::PrimitiveSet::LINE_STRIP, VertexCount+1); + new osg::DrawElementsUShort(osg::PrimitiveSet::LINE_STRIP, VertexCount + 1); + // Assign one primitive to each vertex. for (size_t i = 0; i < VertexCount; ++i) primitives->setElement(i, i); + // Assign the last primitive to the first vertex to close the loop. primitives->setElement(VertexCount, 0); - geometry->addPrimitiveSet(primitives); - geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - + mBorderGeometry->addPrimitiveSet(primitives); + mBorderGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - osg::ref_ptr geode = new osg::Geode(); - geode->addDrawable(geometry); - mBaseNode->addChild(geode); + mBaseNode->addChild(mBorderGeometry); } size_t CSVRender::CellBorder::landIndex(int x, int y) { - return y * ESM::Land::LAND_SIZE + x; + return static_cast(y) * ESM::Land::LAND_SIZE + x; } float CSVRender::CellBorder::scaleToWorld(int value) diff --git a/apps/opencs/view/render/cellborder.hpp b/apps/opencs/view/render/cellborder.hpp index c91aa46c69..be2e18eeee 100644 --- a/apps/opencs/view/render/cellborder.hpp +++ b/apps/opencs/view/render/cellborder.hpp @@ -7,6 +7,7 @@ namespace osg { + class Geometry; class Group; class PositionAttitudeTransform; } @@ -47,7 +48,7 @@ namespace CSVRender osg::Group* mParentNode; osg::ref_ptr mBaseNode; - + osg::ref_ptr mBorderGeometry; }; } diff --git a/apps/opencs/view/render/cellmarker.cpp b/apps/opencs/view/render/cellmarker.cpp index 3de96ab023..ddd4f4caf3 100644 --- a/apps/opencs/view/render/cellmarker.cpp +++ b/apps/opencs/view/render/cellmarker.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -44,9 +43,7 @@ void CSVRender::CellMarker::buildMarker() markerText->setText(coordinatesText); // Add text to marker node. - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable(markerText); - mMarkerNode->addChild(geode); + mMarkerNode->addChild(markerText); } void CSVRender::CellMarker::positionMarker() diff --git a/apps/opencs/view/render/cellwater.cpp b/apps/opencs/view/render/cellwater.cpp index 4351788609..b9391b7697 100644 --- a/apps/opencs/view/render/cellwater.cpp +++ b/apps/opencs/view/render/cellwater.cpp @@ -1,11 +1,10 @@ #include "cellwater.hpp" -#include #include #include #include -#include +#include #include #include #include @@ -27,9 +26,9 @@ namespace CSVRender : mData(data) , mId(id) , mParentNode(cellNode) - , mWaterTransform(0) - , mWaterNode(0) - , mWaterGeometry(0) + , mWaterTransform(nullptr) + , mWaterGroup(nullptr) + , mWaterGeometry(nullptr) , mDeleted(false) , mExterior(false) , mHasWater(false) @@ -41,8 +40,8 @@ namespace CSVRender mWaterTransform->setNodeMask(Mask_Water); mParentNode->addChild(mWaterTransform); - mWaterNode = new osg::Geode(); - mWaterTransform->addChild(mWaterNode); + mWaterGroup = new osg::Group(); + mWaterTransform->addChild(mWaterGroup); int cellIndex = mData.getCells().searchId(mId); if (cellIndex > -1) @@ -136,8 +135,8 @@ namespace CSVRender if (mWaterGeometry) { - mWaterNode->removeDrawable(mWaterGeometry); - mWaterGeometry = 0; + mWaterGroup->removeChild(mWaterGeometry); + mWaterGeometry = nullptr; } if (mDeleted || !mHasWater) @@ -177,6 +176,6 @@ namespace CSVRender mWaterGeometry->getStateSet()->setTextureAttributeAndModes(0, waterTexture, osg::StateAttribute::ON); - mWaterNode->addDrawable(mWaterGeometry); + mWaterGroup->addChild(mWaterGeometry); } } diff --git a/apps/opencs/view/render/cellwater.hpp b/apps/opencs/view/render/cellwater.hpp index 47e5867071..be1786955b 100644 --- a/apps/opencs/view/render/cellwater.hpp +++ b/apps/opencs/view/render/cellwater.hpp @@ -12,7 +12,6 @@ namespace osg { - class Geode; class Geometry; class Group; class PositionAttitudeTransform; @@ -60,7 +59,7 @@ namespace CSVRender osg::Group* mParentNode; osg::ref_ptr mWaterTransform; - osg::ref_ptr mWaterNode; + osg::ref_ptr mWaterGroup; osg::ref_ptr mWaterGeometry; bool mDeleted; diff --git a/apps/opencs/view/render/commands.cpp b/apps/opencs/view/render/commands.cpp new file mode 100644 index 0000000000..515948489e --- /dev/null +++ b/apps/opencs/view/render/commands.cpp @@ -0,0 +1,38 @@ +#include "commands.hpp" + +#include + +#include "terrainshapemode.hpp" +#include "worldspacewidget.hpp" + +CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent) + : mWorldspaceWidget(worldspaceWidget) +{ } + +void CSVRender::DrawTerrainSelectionCommand::redo() +{ + tryUpdate(); +} + +void CSVRender::DrawTerrainSelectionCommand::undo() +{ + tryUpdate(); +} + +void CSVRender::DrawTerrainSelectionCommand::tryUpdate() +{ + if (!mWorldspaceWidget) + { + Log(Debug::Verbose) << "Can't update terrain selection, no WorldspaceWidget found!"; + return; + } + + auto terrainMode = dynamic_cast(mWorldspaceWidget->getEditMode()); + if (!terrainMode) + { + Log(Debug::Verbose) << "Can't update terrain selection in current EditMode"; + return; + } + + terrainMode->getTerrainSelection()->update(); +} diff --git a/apps/opencs/view/render/commands.hpp b/apps/opencs/view/render/commands.hpp new file mode 100644 index 0000000000..62b7fbfdcd --- /dev/null +++ b/apps/opencs/view/render/commands.hpp @@ -0,0 +1,42 @@ +#ifndef CSV_RENDER_COMMANDS_HPP +#define CSV_RENDER_COMMANDS_HPP + +#include + +#include + +#include "worldspacewidget.hpp" + +namespace CSVRender +{ + class TerrainSelection; + + /* + Current solution to force a redrawing of the terrain-selection grid + when undoing/redoing changes in the editor. + This only triggers a simple redraw of the grid, so only use it in + conjunction with actual data changes which deform the grid. + + Please note that this command needs to be put onto the QUndoStack twice: + at the start and at the end of the related terrain manipulation. + This makes sure that the grid is always updated after all changes have + been undone or redone -- but it also means that the selection is redrawn + once at the beginning of either action. Future refinement may solve that. + */ + class DrawTerrainSelectionCommand : public QUndoCommand + { + + private: + QPointer mWorldspaceWidget; + + public: + DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent = nullptr); + + void redo() override; + void undo() override; + + void tryUpdate(); + }; +} + +#endif diff --git a/apps/opencs/view/render/editmode.hpp b/apps/opencs/view/render/editmode.hpp index c0482c81a6..52c35811d2 100644 --- a/apps/opencs/view/render/editmode.hpp +++ b/apps/opencs/view/render/editmode.hpp @@ -30,7 +30,7 @@ namespace CSVRender public: EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, - const QString& tooltip = "", QWidget *parent = 0); + const QString& tooltip = "", QWidget *parent = nullptr); unsigned int getInteractionMask() const; diff --git a/apps/opencs/view/render/instancedragmodes.hpp b/apps/opencs/view/render/instancedragmodes.hpp new file mode 100644 index 0000000000..2629a9d2f9 --- /dev/null +++ b/apps/opencs/view/render/instancedragmodes.hpp @@ -0,0 +1,21 @@ +#ifndef CSV_WIDGET_INSTANCEDRAGMODES_H +#define CSV_WIDGET_INSTANCEDRAGMODES_H + +namespace CSVRender +{ + enum DragMode + { + DragMode_None, + DragMode_Move, + DragMode_Rotate, + DragMode_Scale, + DragMode_Select_Only, + DragMode_Select_Add, + DragMode_Select_Remove, + DragMode_Select_Invert, + DragMode_Move_Snap, + DragMode_Rotate_Snap, + DragMode_Scale_Snap + }; +} +#endif diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 987dea437b..a84b26ca51 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -64,6 +64,11 @@ osg::Quat CSVRender::InstanceMode::eulerToQuat(const osg::Vec3f& euler) const return zr * yr * xr; } +float CSVRender::InstanceMode::roundFloatToMult(const float val, const double mult) const +{ + return round(val / mult) * mult; +} + osg::Vec3f CSVRender::InstanceMode::getSelectionCenter(const std::vector >& selection) const { osg::Vec3f center = osg::Vec3f(0, 0, 0); @@ -96,9 +101,36 @@ osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos) return pos * combined; } +osg::Vec3f CSVRender::InstanceMode::getProjectionSpaceCoords(const osg::Vec3f& pos) +{ + osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); + osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); + osg::Matrix combined = viewMatrix * projMatrix; + + return pos * combined; +} + +osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart) +{ + osg::Matrix viewMatrix; + viewMatrix.invert(getWorldspaceWidget().getCamera()->getViewMatrix()); + osg::Matrix projMatrix; + projMatrix.invert(getWorldspaceWidget().getCamera()->getProjectionMatrix()); + osg::Matrix combined = projMatrix * viewMatrix; + + /* calculate viewport normalized coordinates + note: is there a reason to use getCamera()->getViewport()->computeWindowMatrix() instead? */ + float x = (point.x() * 2) / getWorldspaceWidget().getCamera()->getViewport()->width() - 1.0f; + float y = 1.0f - (point.y() * 2) / getWorldspaceWidget().getCamera()->getViewport()->height(); + + osg::Vec3f mousePlanePoint = osg::Vec3f(x, y, dragStart.z()) * combined; + + return mousePlanePoint; +} + CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent) : EditMode (worldspaceWidget, QIcon (":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, "Instance editing", - parent), mSubMode (0), mSubModeId ("move"), mSelectionMode (0), mDragMode (DragMode_None), + parent), mSubMode (nullptr), mSubModeId ("move"), mSelectionMode (nullptr), mDragMode (DragMode_None), mDragAxis (-1), mLocked (false), mUnitScaleDist(1), mParentNode (parentNode) { connect(this, SIGNAL(requestFocus(const std::string&)), @@ -129,15 +161,13 @@ void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) "
  • Use {scene-edit-primary} to rotate instances freely
  • " "
  • Use {scene-edit-secondary} to rotate instances within the grid
  • " "
  • The center of the view acts as the axis of rotation
  • " - "
" - "Grid rotate not implemented yet"); + ""); mSubMode->addButton (":scenetoolbar/transform-scale", "scale", "Scale selected instances" "
  • Use {scene-edit-primary} to scale instances freely
  • " "
  • Use {scene-edit-secondary} to scale instances along the grid
  • " "
  • The scaling rate is based on how close the start of a drag is to the center of the screen
  • " - "
" - "Grid scale not implemented yet"); + ""); mSubMode->setButton (mSubModeId); @@ -146,7 +176,7 @@ void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) } if (!mSelectionMode) - mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget()); + mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget(), mParentNode); mDragMode = DragMode_None; @@ -169,14 +199,14 @@ void CSVRender::InstanceMode::deactivate (CSVWidget::SceneToolbar *toolbar) { toolbar->removeTool (mSelectionMode); delete mSelectionMode; - mSelectionMode = 0; + mSelectionMode = nullptr; } if (mSubMode) { toolbar->removeTool (mSubMode); delete mSubMode; - mSubMode = 0; + mSubMode = nullptr; } EditMode::deactivate (toolbar); @@ -270,6 +300,8 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) return false; } + mObjectsAtDragStart.clear(); + for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { @@ -278,6 +310,12 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) if (mSubModeId == "move") { objectTag->mObject->setEdited (Object::Override_Position); + float x = objectTag->mObject->getPosition().pos[0]; + float y = objectTag->mObject->getPosition().pos[1]; + float z = objectTag->mObject->getPosition().pos[2]; + osg::Vec3f thisPoint(x, y, z); + mDragStart = getMousePlaneCoords(pos, getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart.emplace_back(thisPoint); mDragMode = DragMode_Move; } else if (mSubModeId == "rotate") @@ -316,43 +354,128 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) bool CSVRender::InstanceMode::secondaryEditStartDrag (const QPoint& pos) { - if (mLocked) + if (mDragMode != DragMode_None || mLocked) return false; - return false; -} - -void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) -{ - osg::Vec3f offset; - osg::Quat rotation; + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); - std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); - - if (mDragMode == DragMode_Move) + std::vector > selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) { - osg::Vec3f eye, centre, up; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); - - if (diffY) - { - offset += up * diffY * speedFactor; - } - if (diffX) + // Only change selection at the start of drag if no object is already selected + if (hit.tag && CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { - offset += ((centre-eye) ^ up) * diffX * speedFactor; + getWorldspaceWidget().clearSelection(Mask_Reference); + if (CSVRender::ObjectTag* objectTag = dynamic_cast (hit.tag.get())) + { + CSVRender::Object* object = objectTag->mObject; + object->setSelected(true); + } } - if (mDragAxis!=-1) + selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return false; + } + + mObjectsAtDragStart.clear(); + + for (std::vector >::iterator iter(selection.begin()); + iter != selection.end(); ++iter) + { + if (CSVRender::ObjectTag* objectTag = dynamic_cast (iter->get())) { - for (int i=0; i<3; ++i) + if (mSubModeId == "move") + { + objectTag->mObject->setEdited(Object::Override_Position); + float x = objectTag->mObject->getPosition().pos[0]; + float y = objectTag->mObject->getPosition().pos[1]; + float z = objectTag->mObject->getPosition().pos[2]; + osg::Vec3f thisPoint(x, y, z); + + mDragStart = getMousePlaneCoords(pos, getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart.emplace_back(thisPoint); + mDragMode = DragMode_Move_Snap; + } + else if (mSubModeId == "rotate") { - if (i!=mDragAxis) - offset[i] = 0; + objectTag->mObject->setEdited(Object::Override_Rotation); + mDragMode = DragMode_Rotate_Snap; + } + else if (mSubModeId == "scale") + { + objectTag->mObject->setEdited(Object::Override_Scale); + mDragMode = DragMode_Scale_Snap; + + // Calculate scale factor + std::vector > editedSelection = getWorldspaceWidget().getEdited(Mask_Reference); + osg::Vec3f center = getScreenCoords(getSelectionCenter(editedSelection)); + + int widgetHeight = getWorldspaceWidget().height(); + + float dx = pos.x() - center.x(); + float dy = (widgetHeight - pos.y()) - center.y(); + + mUnitScaleDist = std::sqrt(dx * dx + dy * dy); } } } - else if (mDragMode == DragMode_Rotate) + + if (CSVRender::ObjectMarkerTag* objectTag = dynamic_cast (hit.tag.get())) + { + mDragAxis = objectTag->mAxis; + } + else + mDragAxis = -1; + + return true; +} + +bool CSVRender::InstanceMode::primarySelectStartDrag (const QPoint& pos) +{ + if (mDragMode!=DragMode_None || mLocked) + return false; + + std::string primarySelectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + + if ( primarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; + else if ( primarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; + else if ( primarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; + else if ( primarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; + + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + mSelectionMode->setDragStart(hit.worldPos); + + return true; +} + +bool CSVRender::InstanceMode::secondarySelectStartDrag (const QPoint& pos) +{ + if (mDragMode!=DragMode_None || mLocked) + return false; + + std::string secondarySelectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if ( secondarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; + else if ( secondarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; + else if ( secondarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; + else if ( secondarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; + + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + mSelectionMode->setDragStart(hit.worldPos); + + return true; +} + +void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +{ + osg::Vec3f offset; + osg::Quat rotation; + + std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); + + if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) {} + else if (mDragMode == DragMode_Rotate || mDragMode == DragMode_Rotate_Snap) { osg::Vec3f eye, centre, up; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); @@ -416,7 +539,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou rotation = osg::Quat(angle, axis); } - else if (mDragMode == DragMode_Scale) + else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { osg::Vec3f center = getScreenCoords(getSelectionCenter(selection)); @@ -432,23 +555,64 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou // Only uniform scaling is currently supported offset = osg::Vec3f(scale, scale, scale); } + else if (mSelectionMode->getCurrentId() == "cube-centre") + { + osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); + mSelectionMode->drawSelectionCubeCentre (mousePlanePoint); + return; + } + else if (mSelectionMode->getCurrentId() == "cube-corner") + { + osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); + mSelectionMode->drawSelectionCubeCorner (mousePlanePoint); + return; + } + else if (mSelectionMode->getCurrentId() == "sphere") + { + osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); + mSelectionMode->drawSelectionSphere (mousePlanePoint); + return; + } + + int i = 0; // Apply - for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) + for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter, i++) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { - if (mDragMode == DragMode_Move) + if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { ESM::Position position = objectTag->mObject->getPosition(); - for (int i=0; i<3; ++i) + osg::Vec3f mousePos = getMousePlaneCoords(pos, getProjectionSpaceCoords(mDragStart)); + float addToX = mousePos.x() - mDragStart.x(); + float addToY = mousePos.y() - mDragStart.y(); + float addToZ = mousePos.z() - mDragStart.z(); + position.pos[0] = mObjectsAtDragStart[i].x() + addToX; + position.pos[1] = mObjectsAtDragStart[i].y() + addToY; + position.pos[2] = mObjectsAtDragStart[i].z() + addToZ; + + if (mDragMode == DragMode_Move_Snap) { - position.pos[i] += offset[i]; + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-movement"].toDouble(); + position.pos[0] = CSVRender::InstanceMode::roundFloatToMult(position.pos[0], snap); + position.pos[1] = CSVRender::InstanceMode::roundFloatToMult(position.pos[1], snap); + position.pos[2] = CSVRender::InstanceMode::roundFloatToMult(position.pos[2], snap); + } + + // XYZ-locking + if (mDragAxis != -1) + { + for (int j = 0; j < 3; ++j) + { + if (j != mDragAxis) + position.pos[j] = mObjectsAtDragStart[i][j]; + } } objectTag->mObject->setPosition(position.pos); } - else if (mDragMode == DragMode_Rotate) + else if (mDragMode == DragMode_Rotate || mDragMode == DragMode_Rotate_Snap) { ESM::Position position = objectTag->mObject->getPosition(); @@ -466,7 +630,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou objectTag->mObject->setRotation(position.rot); } - else if (mDragMode == DragMode_Scale) + else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { // Reset scale objectTag->mObject->setEdited(0); @@ -475,6 +639,11 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou float scale = objectTag->mObject->getScale(); scale *= offset.x(); + if (mDragMode == DragMode_Scale_Snap) + { + scale = CSVRender::InstanceMode::roundFloatToMult(scale, CSMPrefs::get()["3D Scene Editing"]["gridsnap-scale"].toDouble()); + } + objectTag->mObject->setScale (scale); } } @@ -495,7 +664,25 @@ void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) case DragMode_Move: description = "Move Instances"; break; case DragMode_Rotate: description = "Rotate Instances"; break; case DragMode_Scale: description = "Scale Instances"; break; - + case DragMode_Select_Only : + handleSelectDrag(pos); + return; + break; + case DragMode_Select_Add : + handleSelectDrag(pos); + return; + break; + case DragMode_Select_Remove : + handleSelectDrag(pos); + return; + break; + case DragMode_Select_Invert : + handleSelectDrag(pos); + return; + break; + case DragMode_Move_Snap: description = "Move Instances"; break; + case DragMode_Rotate_Snap: description = "Rotate Instances"; break; + case DragMode_Scale_Snap: description = "Scale Instances"; break; case DragMode_None: break; } @@ -507,10 +694,22 @@ void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { + if (mDragMode == DragMode_Rotate_Snap) + { + ESM::Position position = objectTag->mObject->getPosition(); + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-rotation"].toDouble(); + position.rot[0] = CSVRender::InstanceMode::roundFloatToMult(position.rot[0], osg::DegreesToRadians(snap)); + position.rot[1] = CSVRender::InstanceMode::roundFloatToMult(position.rot[1], osg::DegreesToRadians(snap)); + position.rot[2] = CSVRender::InstanceMode::roundFloatToMult(position.rot[2], osg::DegreesToRadians(snap)); + + objectTag->mObject->setRotation(position.rot); + } + objectTag->mObject->apply (macro); } } + mObjectsAtDragStart.clear(); mDragMode = DragMode_None; } @@ -522,7 +721,7 @@ void CSVRender::InstanceMode::dragAborted() void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) { - if (mDragMode==DragMode_Move) + if (mDragMode==DragMode_Move || mDragMode==DragMode_Move_Snap) { osg::Vec3f eye; osg::Vec3f centre; @@ -537,15 +736,29 @@ void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); + int j = 0; + for (std::vector >::iterator iter (selection.begin()); - iter!=selection.end(); ++iter) + iter!=selection.end(); ++iter, j++) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { ESM::Position position = objectTag->mObject->getPosition(); for (int i=0; i<3; ++i) position.pos[i] += offset[i]; + + if (mDragMode == DragMode_Move_Snap) + { + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-movement"].toDouble(); + position.pos[0] = CSVRender::InstanceMode::roundFloatToMult(position.pos[0], snap); + position.pos[1] = CSVRender::InstanceMode::roundFloatToMult(position.pos[1], snap); + position.pos[2] = CSVRender::InstanceMode::roundFloatToMult(position.pos[2], snap); + } + objectTag->mObject->setPosition (position.pos); + osg::Vec3f thisPoint(position.pos[0], position.pos[1], position.pos[2]); + mDragStart = getMousePlaneCoords(getWorldspaceWidget().mapFromGlobal(QCursor::pos()), getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart[j] = thisPoint; } } } @@ -680,6 +893,13 @@ void CSVRender::InstanceMode::subModeChanged (const std::string& id) getWorldspaceWidget().setSubMode (getSubModeFromId (id), Mask_Reference); } +void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos) +{ + osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); + mSelectionMode->dragEnded (mousePlanePoint, mDragMode); + mDragMode = DragMode_None; +} + void CSVRender::InstanceMode::deleteSelectedInstances(bool active) { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); @@ -698,39 +918,15 @@ void CSVRender::InstanceMode::deleteSelectedInstances(bool active) getWorldspaceWidget().clearSelection (Mask_Reference); } -void CSVRender::InstanceMode::dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight) +void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) { - osg::Vec3d point = object->getPosition().asVec3(); - - osg::Vec3d start = point; - start.z() += objectHeight; - osg::Vec3d end = point; - end.z() = std::numeric_limits::lowest(); - - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( - osgUtil::Intersector::MODEL, start, end) ); - intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); - osgUtil::IntersectionVisitor visitor(intersector); - - if (dropMode == TerrainSep) - visitor.setTraversalMask(Mask_Terrain); - if (dropMode == CollisionSep) - visitor.setTraversalMask(Mask_Terrain | Mask_Reference); - - mParentNode->accept(visitor); - - osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); - if (it != intersector->getIntersections().end()) - { - osgUtil::LineSegmentIntersector::Intersection intersection = *it; - ESM::Position position = object->getPosition(); - object->setEdited (Object::Override_Position); - position.pos[2] = intersection.getWorldIntersectPoint().z() + objectHeight; - object->setPosition(position.pos); - } + object->setEdited(Object::Override_Position); + ESM::Position position = object->getPosition(); + position.pos[2] -= dropHeight; + object->setPosition(position.pos); } -float CSVRender::InstanceMode::getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight) +float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight) { osg::Vec3d point = object->getPosition().asVec3(); @@ -744,9 +940,9 @@ float CSVRender::InstanceMode::getDropHeight(DropMode dropMode, CSVRender::Objec intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); - if (dropMode == Terrain) + if (dropMode & Terrain) visitor.setTraversalMask(Mask_Terrain); - if (dropMode == Collision) + if (dropMode & Collision) visitor.setTraversalMask(Mask_Terrain | Mask_Reference); mParentNode->accept(visitor); @@ -774,12 +970,12 @@ void CSVRender::InstanceMode::dropSelectedInstancesToTerrain() void CSVRender::InstanceMode::dropSelectedInstancesToCollisionSeparately() { - handleDropMethod(TerrainSep, "Drop instances to next collision level separately"); + handleDropMethod(CollisionSep, "Drop instances to next collision level separately"); } void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately() { - handleDropMethod(CollisionSep, "Drop instances to terrain level separately"); + handleDropMethod(TerrainSep, "Drop instances to terrain level separately"); } void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg) @@ -793,52 +989,44 @@ void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString comman CSMWorld::CommandMacro macro (undoStack, commandMsg); - DropObjectDataHandler dropObjectDataHandler(&getWorldspaceWidget()); + DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget()); - switch (dropMode) + if(dropMode & Separate) { - case Terrain: - case Collision: - { - float smallestDropHeight = std::numeric_limits::max(); - int counter = 0; - for(osg::ref_ptr tag: selection) - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) - { - float thisDrop = getDropHeight(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]); - if (thisDrop < smallestDropHeight) - smallestDropHeight = thisDrop; - counter++; - } - for(osg::ref_ptr tag: selection) - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) - { - objectTag->mObject->setEdited (Object::Override_Position); - ESM::Position position = objectTag->mObject->getPosition(); - position.pos[2] -= smallestDropHeight; - objectTag->mObject->setPosition(position.pos); - objectTag->mObject->apply (macro); - } - } - break; - - case TerrainSep: - case CollisionSep: - { - int counter = 0; - for(osg::ref_ptr tag: selection) - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) - { - dropInstance(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]); - objectTag->mObject->apply (macro); - counter++; - } - } - break; + 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); + } } } -CSVRender::DropObjectDataHandler::DropObjectDataHandler(WorldspaceWidget* worldspacewidget) +CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget) : mWorldspaceWidget(worldspacewidget) { std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); @@ -865,7 +1053,7 @@ CSVRender::DropObjectDataHandler::DropObjectDataHandler(WorldspaceWidget* worlds } } -CSVRender::DropObjectDataHandler::~DropObjectDataHandler() +CSVRender::DropObjectHeightHandler::~DropObjectHeightHandler() { std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); int counter = 0; diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 29955feef6..feb307807a 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -9,6 +9,7 @@ #include #include "editmode.hpp" +#include "instancedragmodes.hpp" namespace CSVWidget { @@ -25,20 +26,15 @@ namespace CSVRender { Q_OBJECT - enum DragMode - { - DragMode_None, - DragMode_Move, - DragMode_Rotate, - DragMode_Scale - }; - enum DropMode { - Collision, - Terrain, - CollisionSep, - TerrainSep + Separate = 0b1, + + Collision = 0b10, + Terrain = 0b100, + + CollisionSep = Collision | Separate, + TerrainSep = Terrain | Separate, }; CSVWidget::SceneToolMode *mSubMode; @@ -49,20 +45,27 @@ namespace CSVRender bool mLocked; float mUnitScaleDist; osg::ref_ptr mParentNode; + osg::Vec3f mDragStart; + std::vector mObjectsAtDragStart; int getSubModeFromId (const std::string& id) const; osg::Vec3f quatToEuler(const osg::Quat& quat) const; osg::Quat eulerToQuat(const osg::Vec3f& euler) const; + float roundFloatToMult(const float val, const double mult) const; + osg::Vec3f getSelectionCenter(const std::vector >& selection) const; osg::Vec3f getScreenCoords(const osg::Vec3f& pos); - void dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight); - float getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); + osg::Vec3f getProjectionSpaceCoords(const osg::Vec3f& pos); + osg::Vec3f 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); public: - InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent = 0); + InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent = nullptr); void activate (CSVWidget::SceneToolbar *toolbar) override; @@ -84,6 +87,10 @@ namespace CSVRender bool secondaryEditStartDrag (const QPoint& pos) override; + bool primarySelectStartDrag(const QPoint& pos) override; + + bool secondarySelectStartDrag(const QPoint& pos) override; + void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; void dragCompleted(const QPoint& pos) override; @@ -116,11 +123,11 @@ namespace CSVRender }; /// \brief Helper class to handle object mask data in safe way - class DropObjectDataHandler + class DropObjectHeightHandler { public: - DropObjectDataHandler(WorldspaceWidget* worldspacewidget); - ~DropObjectDataHandler(); + DropObjectHeightHandler(WorldspaceWidget* worldspacewidget); + ~DropObjectHeightHandler(); std::vector mObjectHeights; private: diff --git a/apps/opencs/view/render/instancemovemode.cpp b/apps/opencs/view/render/instancemovemode.cpp index 723af811de..5591035492 100644 --- a/apps/opencs/view/render/instancemovemode.cpp +++ b/apps/opencs/view/render/instancemovemode.cpp @@ -6,7 +6,6 @@ CSVRender::InstanceMoveMode::InstanceMoveMode (QWidget *parent) "Move selected instances" "
  • Use {scene-edit-primary} to move instances around freely
  • " "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " - "
" - "Grid move not implemented yet", + "", parent) {} diff --git a/apps/opencs/view/render/instancemovemode.hpp b/apps/opencs/view/render/instancemovemode.hpp index bd0e28dac8..62e6b6a1f8 100644 --- a/apps/opencs/view/render/instancemovemode.hpp +++ b/apps/opencs/view/render/instancemovemode.hpp @@ -11,7 +11,7 @@ namespace CSVRender public: - InstanceMoveMode (QWidget *parent = 0); + InstanceMoveMode (QWidget *parent = nullptr); }; } diff --git a/apps/opencs/view/render/instanceselectionmode.cpp b/apps/opencs/view/render/instanceselectionmode.cpp index bf8ede0eb4..545d35d93b 100644 --- a/apps/opencs/view/render/instanceselectionmode.cpp +++ b/apps/opencs/view/render/instanceselectionmode.cpp @@ -2,17 +2,24 @@ #include #include +#include + +#include +#include +#include +#include #include "../../model/world/idtable.hpp" #include "../../model/world/commands.hpp" +#include "instancedragmodes.hpp" #include "worldspacewidget.hpp" #include "object.hpp" namespace CSVRender { - InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget) - : SelectionMode(parent, worldspaceWidget, Mask_Reference) + InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode) + : SelectionMode(parent, worldspaceWidget, Mask_Reference), mParentNode(cellNode) { mSelectSame = new QAction("Extend selection to instances with same object ID", this); mDeleteSelection = new QAction("Delete selected instances", this); @@ -21,6 +28,342 @@ namespace CSVRender connect(mDeleteSelection, SIGNAL(triggered()), this, SLOT(deleteSelection())); } + InstanceSelectionMode::~InstanceSelectionMode() + { + mParentNode->removeChild(mBaseNode); + } + + void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart) + { + mDragStart = dragStart; + } + + const osg::Vec3d& InstanceSelectionMode::getDragStart() + { + return mDragStart; + } + + void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode) + { + float dragDistance = (mDragStart - dragEndPoint).length(); + if (mBaseNode) mParentNode->removeChild (mBaseNode); + if (getCurrentId() == "cube-centre") + { + osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance); + osg::Vec3d pointB(mDragStart[0] + dragDistance, mDragStart[1] + dragDistance, mDragStart[2] + dragDistance); + getWorldspaceWidget().selectInsideCube(pointA, pointB, dragMode); + } + else if (getCurrentId() == "cube-corner") + { + getWorldspaceWidget().selectInsideCube(mDragStart, dragEndPoint, dragMode); + } + else if (getCurrentId() == "sphere") + { + getWorldspaceWidget().selectWithinDistance(mDragStart, dragDistance, dragMode); + } + } + + void InstanceSelectionMode::drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint) + { + float dragDistance = (mDragStart - mousePlanePoint).length(); + drawSelectionCube(mDragStart, dragDistance); + } + + void InstanceSelectionMode::drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint) + { + drawSelectionBox(mDragStart, mousePlanePoint); + } + + void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB) + { + if (mBaseNode) mParentNode->removeChild (mBaseNode); + mBaseNode = new osg::PositionAttitudeTransform; + mBaseNode->setPosition(pointA); + + osg::ref_ptr geometry (new osg::Geometry); + + osg::Vec3Array *vertices = new osg::Vec3Array; + vertices->push_back (osg::Vec3f (0.0f, 0.0f, 0.0f)); + vertices->push_back (osg::Vec3f (0.0f, 0.0f, pointB[2] - pointA[2])); + vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], 0.0f)); + vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2])); + + vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, 0.0f)); + vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2])); + vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f)); + vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2])); + + geometry->setVertexArray (vertices); + + osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + + // top + primitives->push_back (2); + primitives->push_back (1); + primitives->push_back (0); + + primitives->push_back (3); + primitives->push_back (1); + primitives->push_back (2); + + // bottom + primitives->push_back (4); + primitives->push_back (5); + primitives->push_back (6); + + primitives->push_back (6); + primitives->push_back (5); + primitives->push_back (7); + + // sides + primitives->push_back (1); + primitives->push_back (4); + primitives->push_back (0); + + primitives->push_back (4); + primitives->push_back (1); + primitives->push_back (5); + + primitives->push_back (4); + primitives->push_back (2); + primitives->push_back (0); + + primitives->push_back (6); + primitives->push_back (2); + primitives->push_back (4); + + primitives->push_back (6); + primitives->push_back (3); + primitives->push_back (2); + + primitives->push_back (7); + primitives->push_back (3); + primitives->push_back (6); + + primitives->push_back (1); + primitives->push_back (3); + primitives->push_back (5); + + primitives->push_back (5); + primitives->push_back (3); + primitives->push_back (7); + + geometry->addPrimitiveSet (primitives); + + osg::Vec4Array *colours = new osg::Vec4Array; + + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + + geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + + geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + + mBaseNode->addChild (geometry); + mParentNode->addChild(mBaseNode); + } + + void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius) + { + if (mBaseNode) mParentNode->removeChild (mBaseNode); + mBaseNode = new osg::PositionAttitudeTransform; + mBaseNode->setPosition(point); + + osg::ref_ptr geometry (new osg::Geometry); + + osg::Vec3Array *vertices = new osg::Vec3Array; + for (int i = 0; i < 2; ++i) + { + float height = i ? -radius : radius; + vertices->push_back (osg::Vec3f (height, -radius, -radius)); + vertices->push_back (osg::Vec3f (height, -radius, radius)); + vertices->push_back (osg::Vec3f (height, radius, -radius)); + vertices->push_back (osg::Vec3f (height, radius, radius)); + } + + geometry->setVertexArray (vertices); + + osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + + // top + primitives->push_back (2); + primitives->push_back (1); + primitives->push_back (0); + + primitives->push_back (3); + primitives->push_back (1); + primitives->push_back (2); + + // bottom + primitives->push_back (4); + primitives->push_back (5); + primitives->push_back (6); + + primitives->push_back (6); + primitives->push_back (5); + primitives->push_back (7); + + // sides + primitives->push_back (1); + primitives->push_back (4); + primitives->push_back (0); + + primitives->push_back (4); + primitives->push_back (1); + primitives->push_back (5); + + primitives->push_back (4); + primitives->push_back (2); + primitives->push_back (0); + + primitives->push_back (6); + primitives->push_back (2); + primitives->push_back (4); + + primitives->push_back (6); + primitives->push_back (3); + primitives->push_back (2); + + primitives->push_back (7); + primitives->push_back (3); + primitives->push_back (6); + + primitives->push_back (1); + primitives->push_back (3); + primitives->push_back (5); + + primitives->push_back (5); + primitives->push_back (3); + primitives->push_back (7); + + geometry->addPrimitiveSet (primitives); + + osg::Vec4Array *colours = new osg::Vec4Array; + + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + + geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + + geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + + mBaseNode->addChild (geometry); + mParentNode->addChild(mBaseNode); + } + + void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3f& mousePlanePoint) + { + float dragDistance = (mDragStart - mousePlanePoint).length(); + drawSelectionSphere(mDragStart, dragDistance); + } + + void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius) + { + if (mBaseNode) mParentNode->removeChild (mBaseNode); + mBaseNode = new osg::PositionAttitudeTransform; + mBaseNode->setPosition(point); + + osg::ref_ptr geometry (new osg::Geometry); + + osg::Vec3Array *vertices = new osg::Vec3Array; + constexpr int resolution = 32; + float radiusPerResolution = radius / resolution; + float reciprocalResolution = 1.0f / resolution; + float doubleReciprocalRes = reciprocalResolution * 2; + + osg::Vec4Array *colours = new osg::Vec4Array; + + for (int i = 0; i <= resolution; i += 2) + { + float iShifted = (static_cast(i) - resolution / 2.0f); // i - 16 = -16 ... 16 + float xPercentile = iShifted * doubleReciprocalRes; + float x = xPercentile * radius; + float thisRadius = sqrt (radius * radius - x * x); + + //the next row + float iShifted2 = (static_cast(i + 1) - resolution / 2.0f); + float xPercentile2 = iShifted2 * doubleReciprocalRes; + float x2 = xPercentile2 * radius; + float thisRadius2 = sqrt (radius * radius - x2 * x2); + + for (int j = 0; j < resolution; ++j) + { + float vertexX = thisRadius * sin(j * reciprocalResolution * osg::PI * 2); + float vertexY = i * radiusPerResolution * 2 - radius; + float vertexZ = thisRadius * cos(j * reciprocalResolution * osg::PI * 2); + float heightPercentage = (vertexZ + radius) / (radius * 2); + vertices->push_back (osg::Vec3f (vertexX, vertexY, vertexZ)); + colours->push_back (osg::Vec4f (heightPercentage, heightPercentage, heightPercentage, 0.3f)); + + float vertexNextRowX = thisRadius2 * sin(j * reciprocalResolution * osg::PI * 2); + float vertexNextRowY = (i + 1) * radiusPerResolution * 2 - radius; + float vertexNextRowZ = thisRadius2 * cos(j * reciprocalResolution * osg::PI * 2); + float heightPercentageNextRow = (vertexZ + radius) / (radius * 2); + vertices->push_back (osg::Vec3f (vertexNextRowX, vertexNextRowY, vertexNextRowZ)); + colours->push_back (osg::Vec4f (heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f)); + } + } + + geometry->setVertexArray (vertices); + + osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLE_STRIP, 0); + + for (int i = 0; i < resolution; ++i) + { + //Even + for (int j = 0; j < resolution * 2; ++j) + { + if (i * resolution * 2 + j > static_cast(vertices->size()) - 1) continue; + primitives->push_back (i * resolution * 2 + j); + } + if (i * resolution * 2 > static_cast(vertices->size()) - 1) continue; + primitives->push_back (i * resolution * 2); + primitives->push_back (i * resolution * 2 + 1); + + //Odd + for (int j = 1; j < resolution * 2 - 2; j += 2) + { + if ((i + 1) * resolution * 2 + j - 1 > static_cast(vertices->size()) - 1) continue; + primitives->push_back ((i + 1) * resolution * 2 + j - 1); + primitives->push_back (i * resolution * 2 + j + 2); + } + if ((i + 2) * resolution * 2 - 2 > static_cast(vertices->size()) - 1) continue; + primitives->push_back ((i + 2) * resolution * 2 - 2); + primitives->push_back (i * resolution * 2 + 1); + primitives->push_back ((i + 1) * resolution * 2); + } + + geometry->addPrimitiveSet (primitives); + + geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + + geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + + mBaseNode->addChild (geometry); + mParentNode->addChild(mBaseNode); + } + bool InstanceSelectionMode::createContextMenu(QMenu* menu) { if (menu) diff --git a/apps/opencs/view/render/instanceselectionmode.hpp b/apps/opencs/view/render/instanceselectionmode.hpp index a238116710..81795d5d3e 100644 --- a/apps/opencs/view/render/instanceselectionmode.hpp +++ b/apps/opencs/view/render/instanceselectionmode.hpp @@ -1,7 +1,13 @@ #ifndef CSV_RENDER_INSTANCE_SELECTION_MODE_H #define CSV_RENDER_INSTANCE_SELECTION_MODE_H +#include + +#include +#include + #include "selectionmode.hpp" +#include "instancedragmodes.hpp" namespace CSVRender { @@ -11,8 +17,25 @@ namespace CSVRender public: - InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget); + InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode); + + ~InstanceSelectionMode(); + + /// Store the worldspace-coordinate when drag begins + void setDragStart(const osg::Vec3d& dragStart); + /// Store the worldspace-coordinate when drag begins + const osg::Vec3d& getDragStart(); + + /// Store the screen-coordinate when drag begins + void setScreenDragStart(const QPoint& dragStartPoint); + + /// Apply instance selection changes + void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode); + + void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint ); + void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint ); + void drawSelectionSphere(const osg::Vec3f& mousePlanePoint ); protected: /// Add context menu items to \a menu. @@ -25,8 +48,15 @@ namespace CSVRender private: + void drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB); + void drawSelectionCube(const osg::Vec3d& point, float radius); + void drawSelectionSphere(const osg::Vec3d& point, float radius); + QAction* mDeleteSelection; QAction* mSelectSame; + osg::Vec3d mDragStart; + osg::Group* mParentNode; + osg::ref_ptr mBaseNode; private slots: diff --git a/apps/opencs/view/render/lighting.cpp b/apps/opencs/view/render/lighting.cpp index 82ad43e6ad..c7bee79289 100644 --- a/apps/opencs/view/render/lighting.cpp +++ b/apps/opencs/view/render/lighting.cpp @@ -3,9 +3,12 @@ #include #include #include +#include #include +#include "../../model/prefs/state.hpp" + class DayNightSwitchVisitor : public osg::NodeVisitor { public: @@ -16,8 +19,33 @@ public: void apply(osg::Switch &switchNode) override { - if (switchNode.getName() == Constants::NightDayLabel) - switchNode.setSingleChildOn(mIndex); + constexpr int NoIndex = -1; + + int initialIndex = NoIndex; + if (!switchNode.getUserValue("initialIndex", initialIndex)) + { + for (size_t i = 0; i < switchNode.getValueList().size(); ++i) + { + if (switchNode.getValueList()[i]) + { + initialIndex = i; + break; + } + } + + if (initialIndex != NoIndex) + switchNode.setUserValue("initialIndex", initialIndex); + } + + if (CSMPrefs::get()["Rendering"]["scene-day-night-switch-nodes"].isTrue()) + { + if (switchNode.getName() == Constants::NightDayLabel) + switchNode.setSingleChildOn(mIndex); + } + else if (initialIndex != NoIndex) + { + switchNode.setSingleChildOn(initialIndex); + } traverse(switchNode); } diff --git a/apps/opencs/view/render/lighting.hpp b/apps/opencs/view/render/lighting.hpp index 66b0eec000..d9d90767f0 100644 --- a/apps/opencs/view/render/lighting.hpp +++ b/apps/opencs/view/render/lighting.hpp @@ -16,7 +16,7 @@ namespace CSVRender { public: - Lighting() : mRootNode(0) {} + Lighting() : mRootNode(nullptr) {} virtual ~Lighting(); virtual void activate (osg::Group* rootNode, bool isExterior) = 0; diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index deeab49963..818be8b228 100644 --- a/apps/opencs/view/render/mask.hpp +++ b/apps/opencs/view/render/mask.hpp @@ -8,7 +8,7 @@ namespace CSVRender /// @note See the respective file in OpenMW (apps/openmw/mwrender/vismask.hpp) /// for general usage hints about node masks. /// @copydoc MWRender::VisMask - enum Mask + enum Mask : unsigned int { // elements that are part of the actual scene Mask_Reference = 0x2, diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index f9d2c8872e..6ab24928e6 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -10,17 +9,13 @@ #include #include -#include #include #include #include #include "../../model/world/data.hpp" -#include "../../model/world/ref.hpp" -#include "../../model/world/refidcollection.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/universalid.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/cellcoordinates.hpp" #include "../../model/prefs/state.hpp" @@ -43,15 +38,15 @@ const float CSVRender::Object::MarkerHeadLength = 50; namespace { - osg::ref_ptr createErrorCube() + osg::ref_ptr createErrorCube() { osg::ref_ptr shape(new osg::Box(osg::Vec3f(0,0,0), 50.f)); osg::ref_ptr shapedrawable(new osg::ShapeDrawable); shapedrawable->setShape(shape); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable(shapedrawable); - return geode; + osg::ref_ptr group (new osg::Group); + group->addChild(shapedrawable); + return group; } } @@ -61,7 +56,7 @@ CSVRender::ObjectTag::ObjectTag (Object* object) : TagBase (Mask_Reference), mObject (object) {} -QString CSVRender::ObjectTag::getToolTip (bool hideBasics) const +QString CSVRender::ObjectTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& /*hit*/) const { return QString::fromUtf8 (mObject->getReferenceableId().c_str()); } @@ -117,7 +112,7 @@ void CSVRender::Object::update() { if (recordType == CSMWorld::UniversalId::Type_Npc || recordType == CSMWorld::UniversalId::Type_Creature) { - if (!mActor) mActor.reset(new Actor(mReferenceableId, mData)); + if (!mActor) mActor = std::make_unique(mReferenceableId, mData); mActor->update(); mBaseNode->addChild(mActor->getBaseNode()); } @@ -140,7 +135,7 @@ void CSVRender::Object::update() if (light) { bool isExterior = false; // FIXME - SceneUtil::addLight(mBaseNode, light, Mask_ParticleSystem, Mask_Lighting, isExterior); + SceneUtil::addLight(mBaseNode, light, Mask_Lighting, isExterior); } } @@ -296,10 +291,10 @@ osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker (int axis) setupCommonMarkerState(geometry); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable (geometry); + osg::ref_ptr group (new osg::Group); + group->addChild(geometry); - return geode; + return group; } osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) @@ -308,7 +303,7 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) const float OuterRadius = InnerRadius + MarkerShaftWidth; const float SegmentDistance = 100.f; - const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance))); + const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; @@ -382,10 +377,10 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) setupCommonMarkerState(geometry); - osg::ref_ptr geode = new osg::Geode(); - geode->addDrawable (geometry); + osg::ref_ptr group = new osg::Group(); + group->addChild(geometry); - return geode; + return group; } void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geometry) @@ -413,7 +408,7 @@ osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int CSVRender::Object::Object (CSMWorld::Data& data, osg::Group* parentNode, const std::string& id, bool referenceable, bool forceBaseToZero) -: mData (data), mBaseNode(0), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero), +: mData (data), mBaseNode(nullptr), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero), mScaleOverride (1), mOverrideFlags (0), mSubMode (-1), mMarkerTransparency(0.5f) { mRootNode = new osg::PositionAttitudeTransform; @@ -682,18 +677,20 @@ void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) int cellColumn = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_Cell)); - int refNumColumn = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_RefNum)); + int origCellColumn = collection.findColumnIndex(static_cast ( + CSMWorld::Columns::ColumnId_OriginalCell)); if (cellIndex != originalIndex) { /// \todo figure out worldspace (not important until multiple worldspaces are supported) + std::string origCellId = CSMWorld::CellCoordinates(originalIndex).getId(""); std::string cellId = CSMWorld::CellCoordinates (cellIndex).getId (""); commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, cellColumn), QString::fromUtf8 (cellId.c_str()))); - commands.push (new CSMWorld::ModifyCommand( *model, - model->index (recordIndex, refNumColumn), 0)); + model->index (recordIndex, origCellColumn), QString::fromUtf8 (origCellId.c_str()))); + commands.push(new CSMWorld::ModifyCommand(*model, + model->index(recordIndex, cellColumn), QString::fromUtf8(cellId.c_str()))); + // NOTE: refnum is not modified for moving a reference to another cell } } diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index a19d642234..6c477b57d8 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -20,7 +20,6 @@ namespace osg class PositionAttitudeTransform; class Group; class Node; - class Geode; } namespace osgFX @@ -54,7 +53,7 @@ namespace CSVRender Object* mObject; - QString getToolTip (bool hideBasics) const override; + QString getToolTip (bool hideBasics, const WorldspaceHitResult& hit) const override; }; class ObjectMarkerTag : public ObjectTag diff --git a/apps/opencs/view/render/orbitcameramode.cpp b/apps/opencs/view/render/orbitcameramode.cpp index ba25beaba9..c81402ed1e 100644 --- a/apps/opencs/view/render/orbitcameramode.cpp +++ b/apps/opencs/view/render/orbitcameramode.cpp @@ -3,7 +3,6 @@ #include #include "../../model/prefs/shortcut.hpp" -#include "../../model/prefs/shortcuteventhandler.hpp" #include "worldspacewidget.hpp" @@ -13,7 +12,7 @@ namespace CSVRender QWidget* parent) : ModeButton(icon, tooltip, parent) , mWorldspaceWidget(worldspaceWidget) - , mCenterOnSelection(0) + , mCenterOnSelection(nullptr) { mCenterShortcut = new CSMPrefs::Shortcut("orbit-center-selection", worldspaceWidget); mCenterShortcut->enable(false); @@ -35,7 +34,7 @@ namespace CSVRender void OrbitCameraMode::deactivate(CSVWidget::SceneToolbar* toolbar) { - mCenterShortcut->associateAction(0); + mCenterShortcut->associateAction(nullptr); mCenterShortcut->enable(false); } diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index b5d9234e42..f0299c5ace 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -4,21 +4,15 @@ #include #include -#include -#include - -#include #include #include "../../model/prefs/shortcut.hpp" -#include "../../model/world/tablemimedata.hpp" #include "../../model/world/idtable.hpp" #include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolmode.hpp" -#include "../widget/scenetooltoggle2.hpp" #include "editmode.hpp" #include "mask.hpp" @@ -59,8 +53,8 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() { modified = true; - std::unique_ptr cell (new Cell (mDocument.getData(), mRootNode, - iter->first.getId (mWorldspace), deleted)); + auto cell = std::make_unique(mDocument.getData(), mRootNode, + iter->first.getId (mWorldspace), deleted); delete iter->second; iter->second = cell.release(); @@ -447,9 +441,7 @@ void CSVRender::PagedWorldspaceWidget::addCellToScene ( bool deleted = index==-1 || cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted; - std::unique_ptr cell ( - new Cell (mDocument.getData(), mRootNode, coordinates.getId (mWorldspace), - deleted)); + auto cell = std::make_unique(mDocument.getData(), mRootNode, coordinates.getId (mWorldspace), deleted); EditMode *editMode = getEditMode(); cell->setSubMode (editMode->getSubMode(), editMode->getInteractionMask()); @@ -768,6 +760,22 @@ void CSVRender::PagedWorldspaceWidget::selectAllWithSameParentId (int elementMas flagAsModified(); } +void CSVRender::PagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) +{ + for (auto& cell : mCells) + { + cell.second->selectInsideCube (pointA, pointB, dragMode); + } +} + +void CSVRender::PagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) +{ + for (auto& cell : mCells) + { + cell.second->selectWithinDistance (point, distance, dragMode); + } +} + std::string CSVRender::PagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const { CSMWorld::CellCoordinates cellCoordinates ( @@ -787,7 +795,7 @@ CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const osg::Vec3d& poi if (searchResult != mCells.end()) return searchResult->second; else - return 0; + return nullptr; } CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index d17670cfa7..beab0c575b 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -7,6 +7,7 @@ #include "worldspacewidget.hpp" #include "cell.hpp" +#include "instancedragmodes.hpp" namespace CSVWidget { @@ -120,6 +121,10 @@ namespace CSVRender /// \param elementMask Elements to be affected by the select operation void selectAllWithSameParentId (int elementMask) override; + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; + + void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; + std::string getCellId (const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) const override; diff --git a/apps/opencs/view/render/pathgrid.cpp b/apps/opencs/view/render/pathgrid.cpp index 7f0454d8fe..a45a4b427e 100644 --- a/apps/opencs/view/render/pathgrid.cpp +++ b/apps/opencs/view/render/pathgrid.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -11,11 +10,11 @@ #include -#include "../../model/world/cell.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtree.hpp" +#include "worldspacewidget.hpp" namespace CSVRender { @@ -40,10 +39,13 @@ namespace CSVRender return mPathgrid; } - QString PathgridTag::getToolTip(bool hideBasics) const + QString PathgridTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& hit) const { QString text("Pathgrid: "); text += mPathgrid->getId().c_str(); + text += " ("; + text += QString::number(SceneUtil::getPathgridNode(static_cast(hit.index0))); + text += ")"; return text; } @@ -60,8 +62,8 @@ namespace CSVRender , mRemoveGeometry(false) , mUseOffset(true) , mParent(parent) - , mPathgridGeometry(0) - , mDragGeometry(0) + , mPathgridGeometry(nullptr) + , mDragGeometry(nullptr) , mTag(new PathgridTag(this)) { const float CoordScalar = ESM::Land::REAL_SIZE; @@ -73,8 +75,8 @@ namespace CSVRender mBaseNode->setNodeMask(Mask_Pathgrid); mParent->addChild(mBaseNode); - mPathgridGeode = new osg::Geode(); - mBaseNode->addChild(mPathgridGeode); + mPathgridGroup = new osg::Group(); + mBaseNode->addChild(mPathgridGroup); recreateGeometry(); @@ -218,8 +220,8 @@ namespace CSVRender mUseOffset = false; mMoveOffset.set(0, 0, 0); - mPathgridGeode->removeDrawable(mDragGeometry); - mDragGeometry = 0; + mPathgridGroup->removeChild(mDragGeometry); + mDragGeometry = nullptr; } void Pathgrid::applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos) @@ -520,7 +522,7 @@ namespace CSVRender removePathgridGeometry(); mPathgridGeometry = SceneUtil::createPathgridGeometry(*source); - mPathgridGeode->addDrawable(mPathgridGeometry); + mPathgridGroup->addChild(mPathgridGeometry); createSelectedGeometry(*source); } @@ -549,15 +551,15 @@ namespace CSVRender removeSelectedGeometry(); mSelectedGeometry = SceneUtil::createPathgridSelectedWireframe(source, mSelected); - mPathgridGeode->addDrawable(mSelectedGeometry); + mPathgridGroup->addChild(mSelectedGeometry); } void Pathgrid::removePathgridGeometry() { if (mPathgridGeometry) { - mPathgridGeode->removeDrawable(mPathgridGeometry); - mPathgridGeometry = 0; + mPathgridGroup->removeChild(mPathgridGeometry); + mPathgridGeometry = nullptr; } } @@ -565,15 +567,15 @@ namespace CSVRender { if (mSelectedGeometry) { - mPathgridGeode->removeDrawable(mSelectedGeometry); - mSelectedGeometry = 0; + mPathgridGroup->removeChild(mSelectedGeometry); + mSelectedGeometry = nullptr; } } void Pathgrid::createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid) { if (mDragGeometry) - mPathgridGeode->removeDrawable(mDragGeometry); + mPathgridGroup->removeChild(mDragGeometry); mDragGeometry = new osg::Geometry(); @@ -601,7 +603,7 @@ namespace CSVRender mDragGeometry->addPrimitiveSet(indices); mDragGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - mPathgridGeode->addDrawable(mDragGeometry); + mPathgridGroup->addChild(mDragGeometry); } const CSMWorld::Pathgrid* Pathgrid::getPathgridSource() @@ -612,7 +614,7 @@ namespace CSVRender return &mPathgridCollection.getRecord(index).get(); } - return 0; + return nullptr; } int Pathgrid::edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2) diff --git a/apps/opencs/view/render/pathgrid.hpp b/apps/opencs/view/render/pathgrid.hpp index 8f5d45a487..284b98d0b6 100644 --- a/apps/opencs/view/render/pathgrid.hpp +++ b/apps/opencs/view/render/pathgrid.hpp @@ -15,7 +15,6 @@ namespace osg { - class Geode; class Geometry; class Group; class PositionAttitudeTransform; @@ -40,7 +39,7 @@ namespace CSVRender Pathgrid* getPathgrid () const; - QString getToolTip (bool hideBasics) const override; + QString getToolTip (bool hideBasics, const WorldspaceHitResult& hit) const override; private: @@ -107,7 +106,7 @@ namespace CSVRender osg::Group* mParent; osg::ref_ptr mBaseNode; - osg::ref_ptr mPathgridGeode; + osg::ref_ptr mPathgridGroup; osg::ref_ptr mPathgridGeometry; osg::ref_ptr mSelectedGeometry; osg::ref_ptr mDragGeometry; diff --git a/apps/opencs/view/render/pathgridmode.cpp b/apps/opencs/view/render/pathgridmode.cpp index 8863ad235c..c267f5762d 100644 --- a/apps/opencs/view/render/pathgridmode.cpp +++ b/apps/opencs/view/render/pathgridmode.cpp @@ -1,6 +1,5 @@ #include "pathgridmode.hpp" -#include #include #include @@ -9,8 +8,6 @@ #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" -#include "../../model/world/idtable.hpp" -#include "../../model/world/idtree.hpp" #include "../widget/scenetoolbar.hpp" @@ -27,7 +24,7 @@ namespace CSVRender getTooltip(), parent) , mDragMode(DragMode_None) , mFromNode(0) - , mSelectionMode(0) + , mSelectionMode(nullptr) { } @@ -59,7 +56,7 @@ namespace CSVRender { toolbar->removeTool (mSelectionMode); delete mSelectionMode; - mSelectionMode = 0; + mSelectionMode = nullptr; } } @@ -214,7 +211,7 @@ namespace CSVRender Cell* cell = getWorldspaceWidget().getCell(hit.worldPos); if (cell && cell->getPathgrid()) { - PathgridTag* tag = 0; + PathgridTag* tag = nullptr; if (hit.tag && (tag = dynamic_cast(hit.tag.get())) && tag->getPathgrid()->getId() == mEdgeId) { unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); diff --git a/apps/opencs/view/render/pathgridmode.hpp b/apps/opencs/view/render/pathgridmode.hpp index 6d8f96e8c3..cc61dfe9b0 100644 --- a/apps/opencs/view/render/pathgridmode.hpp +++ b/apps/opencs/view/render/pathgridmode.hpp @@ -15,7 +15,7 @@ namespace CSVRender public: - PathgridMode(WorldspaceWidget* worldspace, QWidget* parent=0); + PathgridMode(WorldspaceWidget* worldspace, QWidget* parent=nullptr); void activate(CSVWidget::SceneToolbar* toolbar) override; diff --git a/apps/opencs/view/render/previewwidget.hpp b/apps/opencs/view/render/previewwidget.hpp index 630ccf293d..a8d73729a4 100644 --- a/apps/opencs/view/render/previewwidget.hpp +++ b/apps/opencs/view/render/previewwidget.hpp @@ -29,7 +29,7 @@ namespace CSVRender public: PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, - QWidget *parent = 0); + QWidget *parent = nullptr); signals: diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index f3186e76a9..578723ccd8 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -3,9 +3,7 @@ #include #include -#include -#include -#include +#include #include #include @@ -25,7 +23,6 @@ #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" -#include "../../model/prefs/shortcuteventhandler.hpp" #include "lighting.hpp" #include "mask.hpp" @@ -36,7 +33,7 @@ namespace CSVRender RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) - , mRootNode(0) + , mRootNode(nullptr) { osgViewer::CompositeViewer& viewer = CompositeViewer::get(); @@ -45,7 +42,7 @@ RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) //ds->setNumMultiSamples(8); osg::ref_ptr traits = new osg::GraphicsContext::Traits; - traits->windowName = ""; + traits->windowName.clear(); traits->windowDecoration = true; traits->x = 0; traits->y = 0; @@ -69,7 +66,6 @@ RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) setLayout(layout); mView->getCamera()->setGraphicsContext(window); - mView->getCamera()->setClearColor( osg::Vec4(0.2, 0.2, 0.6, 1.0) ); mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) ); SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; @@ -121,7 +117,7 @@ void RenderWidget::flagAsModified() mView->requestRedraw(); } -void RenderWidget::setVisibilityMask(int mask) +void RenderWidget::setVisibilityMask(unsigned int mask) { mView->getCamera()->setCullMask(mask | Mask_ParticleSystem | Mask_Lighting); } @@ -212,6 +208,25 @@ SceneWidget::SceneWidget(std::shared_ptr resourceSyste mOrbitCamControl->setConstRoll( CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue() ); + // set up gradient view or configured clear color + QColor bgColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); + + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { + QColor gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + mGradientCamera = createGradientCamera(bgColour, gradientColour); + + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + else { + mView->getCamera()->setClearColor(osg::Vec4( + bgColour.redF(), + bgColour.greenF(), + bgColour.blueF(), + 1.0f + )); + } + // we handle lighting manually mView->setLightingMode(osgViewer::View::NO_LIGHT); @@ -249,6 +264,79 @@ SceneWidget::~SceneWidget() mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); } + +osg::ref_ptr SceneWidget::createGradientRectangle(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr geometry = new osg::Geometry; + + osg::ref_ptr vertices = new osg::Vec3Array; + + vertices->push_back(osg::Vec3(0.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(0.0f, 1.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 1.0f, -1.0f)); + + geometry->setVertexArray(vertices); + + osg::ref_ptr primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + + // triangle 1 + primitives->push_back (0); + primitives->push_back (1); + primitives->push_back (2); + + // triangle 2 + primitives->push_back (2); + primitives->push_back (1); + primitives->push_back (3); + + geometry->addPrimitiveSet(primitives); + + osg::ref_ptr colours = new osg::Vec4ubArray; + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); + + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + return geometry; +} + + +osg::ref_ptr SceneWidget::createGradientCamera(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr camera = new osg::Camera(); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1.0f, 0, 1.0f)); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setViewMatrix(osg::Matrix::identity()); + + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + camera->setAllowEventFocus(false); + + // draw subgraph before main camera view. + camera->setRenderOrder(osg::Camera::PRE_RENDER); + + camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); + + camera->addChild(gradientQuad); + return camera; +} + + +void SceneWidget::updateGradientCamera(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); + // Replaces previous rectangle + mGradientCamera->setChild(0, gradientRect.get()); +} + void SceneWidget::setLighting(Lighting *lighting) { if (mLighting) @@ -257,7 +345,7 @@ void SceneWidget::setLighting(Lighting *lighting) mLighting = lighting; mLighting->activate (mRootNode, mIsExterior); - osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : 0); + osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : nullptr); setAmbient(ambient); flagAsModified(); @@ -276,12 +364,59 @@ void SceneWidget::setAmbient(const osg::Vec4f& ambient) void SceneWidget::selectLightingMode (const std::string& mode) { - if (mode=="day") - setLighting (&mLightingDay); - else if (mode=="night") - setLighting (&mLightingNight); - else if (mode=="bright") - setLighting (&mLightingBright); + QColor backgroundColour; + QColor gradientColour; + if (mode == "day") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + setLighting(&mLightingDay); + } + else if (mode == "night") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-night-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-night-gradient-colour"].toColor(); + setLighting(&mLightingNight); + } + else if (mode == "bright") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-bright-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-bright-gradient-colour"].toColor(); + setLighting(&mLightingBright); + } + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { + if (mGradientCamera.get() != nullptr) { + // we can go ahead and update since this camera still exists + updateGradientCamera(backgroundColour, gradientColour); + + if (!mView->getCamera()->containsNode(mGradientCamera.get())) + { + // need to re-attach the gradient camera + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + } + else { + // need to create the gradient camera + mGradientCamera = createGradientCamera(backgroundColour, gradientColour); + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + } + else { + // Fall back to using the clear color for the camera + mView->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mView->getCamera()->setClearColor(osg::Vec4( + backgroundColour.redF(), + backgroundColour.greenF(), + backgroundColour.blueF(), + 1.0f + )); + if (mGradientCamera.get() != nullptr && mView->getCamera()->containsNode(mGradientCamera.get())) { + // Remove the child to prevent the gradient from rendering + mView->getCamera()->removeChild(mGradientCamera.get()); + } + } } CSVWidget::SceneToolMode *SceneWidget::makeLightingSelector (CSVWidget::SceneToolbar *parent) @@ -413,6 +548,11 @@ void SceneWidget::settingChanged (const CSMPrefs::Setting *setting) { updateCameraParameters(); } + else if (*setting == "Rendering/scene-day-night-switch-nodes") + { + if (mLighting) + setLighting(mLighting); + } } void RenderWidget::updateCameraParameters(double overrideAspect) diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 6a94254b99..922776e9fb 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -55,7 +55,7 @@ namespace CSVRender /// Initiates a request to redraw the view void flagAsModified(); - void setVisibilityMask(int mask); + void setVisibilityMask(unsigned int mask); osg::Camera *getCamera(); @@ -100,10 +100,15 @@ namespace CSVRender void mouseMoveEvent (QMouseEvent *event) override; void wheelEvent (QWheelEvent *event) override; + osg::ref_ptr createGradientRectangle(QColor bgColour, QColor gradientColour); + osg::ref_ptr createGradientCamera(QColor bgColour, QColor gradientColour); + void updateGradientCamera(QColor bgColour, QColor gradientColour); + std::shared_ptr mResourceSystem; Lighting* mLighting; - + + osg::ref_ptr mGradientCamera; osg::Vec4f mDefaultAmbient; bool mHasDefaultAmbient; bool mIsExterior; diff --git a/apps/opencs/view/render/selectionmode.cpp b/apps/opencs/view/render/selectionmode.cpp index b5ccda5ad4..e7e7d47b5a 100644 --- a/apps/opencs/view/render/selectionmode.cpp +++ b/apps/opencs/view/render/selectionmode.cpp @@ -15,30 +15,27 @@ namespace CSVRender { addButton(":scenetoolbar/selection-mode-cube", "cube-centre", "Centred cube" - "
  • Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} " - "(invert selection state) from the centre of the selection cube outwards
  • " + "
    • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " + "from the centre of the selection cube outwards.
    • " "
    • The selection cube is aligned to the word space axis
    • " "
    • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
    • " - "
    " - "Not implemented yet"); + "
"); addButton(":scenetoolbar/selection-mode-cube-corner", "cube-corner", "Cube corner to corner" - "
  • Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} " - "(invert selection state) from one corner of the selection cube to the opposite corner
  • " + "
    • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " + "from one corner of the selection cube to the opposite corner
    • " "
    • The selection cube is aligned to the word space axis
    • " "
    • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
    • " - "
    " - "Not implemented yet"); + "
"); addButton(":scenetoolbar/selection-mode-cube-sphere", "sphere", "Centred sphere" - "
  • Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} " - "(invert selection state) from the centre of the selection sphere outwards
  • " + "
    • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " + "from the centre of the selection sphere outwards
    • " "
    • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
    • " - "
    " - "Not implemented yet"); + "
"); mSelectAll = new QAction("Select all", this); mDeselectAll = new QAction("Clear selection", this); diff --git a/apps/opencs/view/render/tagbase.cpp b/apps/opencs/view/render/tagbase.cpp index 3ddd68690f..61a94215dd 100644 --- a/apps/opencs/view/render/tagbase.cpp +++ b/apps/opencs/view/render/tagbase.cpp @@ -8,7 +8,7 @@ CSVRender::Mask CSVRender::TagBase::getMask() const return mMask; } -QString CSVRender::TagBase::getToolTip (bool hideBasics) const +QString CSVRender::TagBase::getToolTip (bool hideBasics, const WorldspaceHitResult& /*hit*/) const { return ""; } diff --git a/apps/opencs/view/render/tagbase.hpp b/apps/opencs/view/render/tagbase.hpp index d1ecd2cfd9..50295d508d 100644 --- a/apps/opencs/view/render/tagbase.hpp +++ b/apps/opencs/view/render/tagbase.hpp @@ -9,6 +9,8 @@ namespace CSVRender { + struct WorldspaceHitResult; + class TagBase : public osg::Referenced { Mask mMask; @@ -19,7 +21,7 @@ namespace CSVRender Mask getMask() const; - virtual QString getToolTip (bool hideBasics) const; + virtual QString getToolTip (bool hideBasics, const WorldspaceHitResult& hit) const; }; } diff --git a/apps/opencs/view/render/terrainselection.cpp b/apps/opencs/view/render/terrainselection.cpp index 4e209af57b..dc1886d57c 100644 --- a/apps/opencs/view/render/terrainselection.cpp +++ b/apps/opencs/view/render/terrainselection.cpp @@ -4,11 +4,9 @@ #include #include -#include -#include +#include -#include "../../model/world/cellcoordinates.hpp" #include "../../model/world/columnimp.hpp" #include "../../model/world/idtable.hpp" @@ -16,11 +14,13 @@ #include "worldspacewidget.hpp" CSVRender::TerrainSelection::TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type): -mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mDraggedOperationFlag(false), mSelectionType(type) +mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mSelectionType(type) { mGeometry = new osg::Geometry(); mSelectionNode = new osg::Group(); + mSelectionNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mSelectionNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mSelectionNode->addChild(mGeometry); activate(); @@ -39,64 +39,28 @@ std::vector> CSVRender::TerrainSelection::getTerrainSelectio void CSVRender::TerrainSelection::onlySelect(const std::vector> &localPositions) { mSelection = localPositions; + update(); } -void CSVRender::TerrainSelection::addSelect(const std::pair &localPos) +void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions) { - if (std::find(mSelection.begin(), mSelection.end(), localPos) == mSelection.end()) - { - mSelection.emplace_back(localPos); - update(); - } + handleSelection(localPositions, SelectionMethod::AddSelect); } -void CSVRender::TerrainSelection::toggleSelect(const std::vector> &localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions) { - if (toggleInProgress) - { - for(auto const& localPos: localPositions) - { - auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); - mDraggedOperationFlag = true; + handleSelection(localPositions, SelectionMethod::RemoveSelect); +} - if (iterTemp == mTemporarySelection.end()) - { - auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); - if (iter != mSelection.end()) - { - mSelection.erase(iter); - } - else - { - mSelection.emplace_back(localPos); - } - } +void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions) +{ + handleSelection(localPositions, SelectionMethod::ToggleSelect); +} - mTemporarySelection.push_back(localPos); - } - } - else if (mDraggedOperationFlag == false) - { - for(auto const& localPos: localPositions) - { - const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); - if (iter != mSelection.end()) - { - mSelection.erase(iter); - } - else - { - mSelection.emplace_back(localPos); - } - } - } - else - { - mDraggedOperationFlag = false; - mTemporarySelection.clear(); - } - update(); +void CSVRender::TerrainSelection::clearTemporarySelection() +{ + mTemporarySelection.clear(); } void CSVRender::TerrainSelection::activate() @@ -143,26 +107,16 @@ void CSVRender::TerrainSelection::drawShapeSelection(const osg::ref_ptrpush_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1) + 2)); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1))); vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y) + 2)); - - const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); - if (north == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1) + 2)); - } - - const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); - if (east == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y) + 2)); - } + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y))); } } } @@ -195,8 +149,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2)+2)); + vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2))); + vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2))); } } @@ -207,8 +161,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1)+2)); + vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1))); + vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1))); } } @@ -219,8 +173,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i)+2)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i))); } } @@ -231,12 +185,63 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i)+2)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i))); + } + } + } + } +} + +void CSVRender::TerrainSelection::handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod) +{ + for (auto const& localPos : localPositions) + { + const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); + + switch (selectionMethod) + { + case SelectionMethod::OnlySelect: + break; + + case SelectionMethod::AddSelect: + if (iter == mSelection.end()) + { + mSelection.emplace_back(localPos); } + break; + + case SelectionMethod::RemoveSelect: + if (iter != mSelection.end()) + { + mSelection.erase(iter); + } + break; + + case SelectionMethod::ToggleSelect: + { + const auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); + if (iterTemp == mTemporarySelection.end()) + { + if (iter == mSelection.end()) + { + mSelection.emplace_back(localPos); + } + else + { + mSelection.erase(iter); + } + } + mTemporarySelection.emplace_back(localPos); + break; } + + default: + break; } } + + update(); } bool CSVRender::TerrainSelection::noCell(const std::string& cellId) @@ -283,11 +288,9 @@ int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global ver else if (isLandLoaded(CSMWorld::CellCoordinates::generateId(cellX, cellY))) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); - int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); - const CSMWorld::LandHeightsColumn::DataType mPointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - return mPointer[localY*ESM::Land::LAND_SIZE + localX]; + const ESM::Land::LandData* landData = document.getData().getLand().getRecord(cellId).get().getLandData(ESM::Land::DATA_VHGT); + return landData->mHeights[localY*ESM::Land::LAND_SIZE + localX]; } return landHeight; diff --git a/apps/opencs/view/render/terrainselection.hpp b/apps/opencs/view/render/terrainselection.hpp index 84ee6f25ac..4b4758e75e 100644 --- a/apps/opencs/view/render/terrainselection.hpp +++ b/apps/opencs/view/render/terrainselection.hpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include "../../model/world/cellcoordinates.hpp" namespace osg @@ -27,6 +27,14 @@ namespace CSVRender Shape }; + enum class SelectionMethod + { + OnlySelect, + AddSelect, + RemoveSelect, + ToggleSelect + }; + /// \brief Class handling the terrain selection data and rendering class TerrainSelection { @@ -36,8 +44,10 @@ namespace CSVRender ~TerrainSelection(); void onlySelect(const std::vector> &localPositions); - void addSelect(const std::pair &localPos); - void toggleSelect(const std::vector> &localPositions, bool toggleInProgress); + void addSelect(const std::vector>& localPositions); + void removeSelect(const std::vector>& localPositions); + void toggleSelect(const std::vector> &localPositions); + void clearTemporarySelection(); void activate(); void deactivate(); @@ -55,6 +65,8 @@ namespace CSVRender private: + void handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod); + bool noCell(const std::string& cellId); bool noLand(const std::string& cellId); @@ -62,7 +74,7 @@ namespace CSVRender bool noLandLoaded(const std::string& cellId); bool isLandLoaded(const std::string& cellId); - + osg::Group* mParentNode; WorldspaceWidget *mWorldspaceWidget; osg::ref_ptr mBaseNode; @@ -70,7 +82,6 @@ namespace CSVRender osg::ref_ptr mSelectionNode; std::vector> mSelection; // Global terrain selection coordinate in either vertex or texture units std::vector> mTemporarySelection; // Used during toggle to compare the most recent drag operation - bool mDraggedOperationFlag; //true during drag operation, false when click-operation TerrainSelectionType mSelectionType; }; } diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index 5664378ca9..bd023b9b76 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -2,41 +2,28 @@ #include #include -#include #include #include #include #include #include -#include -#include #include #include -#include +#include #include -#include "../widget/brushshapes.hpp" -#include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolshapebrush.hpp" -#include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" -#include "../../model/world/columnbase.hpp" -#include "../../model/world/commandmacro.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/land.hpp" -#include "../../model/world/resourcetable.hpp" #include "../../model/world/tablemimedata.hpp" -#include "../../model/world/universalid.hpp" #include "brushdraw.hpp" +#include "commands.hpp" #include "editmode.hpp" #include "pagedworldspacewidget.hpp" #include "mask.hpp" @@ -45,7 +32,7 @@ #include "worldspacewidget.hpp" CSVRender::TerrainShapeMode::TerrainShapeMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) -: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-shape"}, Mask_Terrain | Mask_Reference, "Terrain land editing", parent), +: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-shape"}, Mask_Terrain, "Terrain land editing", parent), mParentNode(parentNode) { } @@ -54,7 +41,7 @@ void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mTerrainShapeSelection) { - mTerrainShapeSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape)); + mTerrainShapeSelection = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape); } if(!mShapeBrushScenetool) @@ -69,7 +56,7 @@ void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) } if (!mBrushDraw) - mBrushDraw.reset(new BrushDraw(mParentNode)); + mBrushDraw = std::make_unique(mParentNode); EditMode::activate(toolbar); toolbar->addTool (mShapeBrushScenetool); @@ -99,7 +86,7 @@ void CSVRender::TerrainShapeMode::primaryOpenPressed (const WorldspaceHitResult& void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& hit) { - if (hit.hit && hit.tag == 0) + if (hit.hit && hit.tag == nullptr) { if (mShapeEditTool == ShapeEditTool_Flatten) setFlattenToolTargetHeight(hit); @@ -108,33 +95,25 @@ void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); applyTerrainEditChanges(); } - - if (mDragMode == InteractionType_PrimarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - } - - if (mDragMode == InteractionType_SecondarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - } } clearTransientEdits(); } void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == 0) + if(hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + mTerrainShapeSelection->clearTemporarySelection(); } } void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == 0) + if(hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + mTerrainShapeSelection->clearTemporarySelection(); } } @@ -144,7 +123,7 @@ bool CSVRender::TerrainShapeMode::primaryEditStartDrag (const QPoint& pos) mDragMode = InteractionType_PrimaryEdit; - if (hit.hit && hit.tag == 0) + if (hit.hit && hit.tag == nullptr) { mEditingPos = hit.worldPos; mIsEditing = true; @@ -164,26 +143,26 @@ bool CSVRender::TerrainShapeMode::primarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; - if (!hit.hit || hit.tag != 0) + if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + return true; } bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; - if (!hit.hit || hit.tag != 0) + if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + return true; } void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) @@ -202,13 +181,13 @@ void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == 0) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == 0) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); } } @@ -219,12 +198,17 @@ void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) applyTerrainEditChanges(); clearTransientEdits(); } + if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect) + { + mTerrainShapeSelection->clearTemporarySelection(); + } } void CSVRender::TerrainShapeMode::dragAborted() { clearTransientEdits(); + mDragMode = InteractionType_None; } void CSVRender::TerrainShapeMode::dragWheel (int diff, double speedFactor) @@ -288,6 +272,9 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() undoStack.beginMacro ("Edit shape and normal records"); + // One command at the beginning of the macro for redrawing the terrain-selection grid when undoing the changes. + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); + for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); @@ -356,6 +343,9 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() } pushNormalsEditToCommand(landNormalsNew, document, landTable, cellId); } + // One command at the end of the macro for redrawing the terrain-selection grid when redoing the changes. + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); + undoStack.endMacro(); clearTransientEdits(); } @@ -433,7 +423,9 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair float smoothedByDistance = 0.0f; if (mShapeEditTool == ShapeEditTool_Drag) smoothedByDistance = calculateBumpShape(distance, r, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) smoothedByDistance = calculateBumpShape(distance, r, r + mShapeEditToolStrength); - if (distance <= r) + + // Using floating-point radius here to prevent selecting too few vertices. + if (distance <= mBrushSize / 2.0f) { if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, smoothedByDistance); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) @@ -964,15 +956,15 @@ bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordi // Check for height limits on x-axis if (leftHeight - thisHeight > limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique(leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (leftHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique(leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (upHeight - thisHeight > limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (upHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Limit altered height value based on x or y, whichever is the smallest compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); @@ -993,15 +985,15 @@ bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordi // Check for height limits on x-axis if (rightHeight - thisHeight > limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique(rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (rightHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique(rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (downHeight - thisHeight > limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(downHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique(downHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (downHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(downHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique(downHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Limit altered height value based on x or y, whichever is the smallest compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); @@ -1036,16 +1028,41 @@ void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int glob return; int selectionX = globalSelectionX; int selectionY = globalSelectionY; - if (xIsAtCellBorder) + + /* + The northern and eastern edges don't belong to the current cell. + If the corresponding adjacent cell is not loaded, some special handling is necessary to select border vertices. + */ + if (xIsAtCellBorder && yIsAtCellBorder) + { + /* + Handle the NW, NE, and SE corner vertices. + NW corner: (+1, -1) offset to reach current cell. + NE corner: (-1, -1) offset to reach current cell. + SE corner: (-1, +1) offset to reach current cell. + */ + if (isInCellSelection(globalSelectionX - 1, globalSelectionY - 1) + || isInCellSelection(globalSelectionX + 1, globalSelectionY - 1) + || isInCellSelection(globalSelectionX - 1, globalSelectionY + 1)) + { + selections->emplace_back(globalSelectionX, globalSelectionY); + } + } + else if (xIsAtCellBorder) + { selectionX--; - if (yIsAtCellBorder) + } + else if (yIsAtCellBorder) + { selectionY--; + } + if (isInCellSelection(selectionX, selectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); } } -void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; @@ -1074,8 +1091,11 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& { int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); - int distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); - if (distance <= r) handleSelection(i, j, &selections); + float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); + + // Using floating-point radius here to prevent selecting too few vertices. + if (distance <= mBrushSize / 2.0f) + handleSelection(i, j, &selections); } } } @@ -1092,9 +1112,21 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& } } - if(selectMode == 0) mTerrainShapeSelection->onlySelect(selections); - if(selectMode == 1) mTerrainShapeSelection->toggleSelect(selections, dragOperation); + std::string selectAction; + if (selectMode == 0) + selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + else + selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if (selectAction == "Select only") + mTerrainShapeSelection->onlySelect(selections); + else if (selectAction == "Add to selection") + mTerrainShapeSelection->addSelect(selections); + else if (selectAction == "Remove from selection") + mTerrainShapeSelection->removeSelect(selections); + else if (selectAction == "Invert selection") + mTerrainShapeSelection->toggleSelect(selections); } void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, @@ -1266,8 +1298,7 @@ bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellI if (mode=="Create cell and land, then edit" && useTool) { - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand (cellTable, cellId)); + auto createCommand = std::make_unique(cellTable, cellId); int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); createCommand->addNestedValue (parentIndex, index, false); @@ -1398,6 +1429,11 @@ void CSVRender::TerrainShapeMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainShapeMode::getTerrainSelection() +{ + return mTerrainShapeSelection; +} + void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) { mBrushSize = brushSize; diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp index a88e60c9c4..8b40483be4 100644 --- a/apps/opencs/view/render/terrainshapemode.hpp +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -92,6 +92,8 @@ namespace CSVRender void dragMoveEvent (QDragMoveEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; + std::shared_ptr getTerrainSelection(); + private: /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse @@ -140,7 +142,7 @@ namespace CSVRender void handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections); /// Handle brush mechanics for terrain shape selection - void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation); + void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode); /// Push terrain shape edits to command macro void pushEditToCommand (const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, @@ -176,7 +178,7 @@ namespace CSVRender int mDragMode = InteractionType_None; osg::Group* mParentNode; bool mIsEditing = false; - std::unique_ptr mTerrainShapeSelection; + std::shared_ptr mTerrainShapeSelection; int mTotalDiffY = 0; std::vector mAlteredCells; osg::Vec3d mEditingPos; diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index d9cc3015e1..a3f1a91b62 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -1,9 +1,7 @@ #include "terrainstorage.hpp" -#include "../../model/world/land.hpp" -#include "../../model/world/landtexture.hpp" +#include -#include namespace CSVRender { @@ -48,15 +46,14 @@ namespace CSVRender float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { float height = 0.f; - osg::ref_ptr land = getLand (cellX, cellY); - if (land) - { - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; - if (data) height = getVertexHeight(data, inCellX, inCellY); - } - else return height; - return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; + int index = mData.getLand().searchId(CSMWorld::Land::createUniqueRecordId(cellX, cellY)); + if (index == -1) // no land! + return height; + + const ESM::Land::LandData* landData = mData.getLand().getRecord(index).get().getLandData(ESM::Land::DATA_VHGT); + height = landData->mHeights[inCellY*ESM::Land::LAND_SIZE + inCellX]; + return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; } float* TerrainStorage::getAlteredHeight(int inCellX, int inCellY) diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp index 762eb80036..dbb39a44ad 100644 --- a/apps/opencs/view/render/terrainstorage.hpp +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "../../model/world/data.hpp" diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index ae57118813..3127306522 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -1,34 +1,24 @@ #include "terraintexturemode.hpp" #include -#include #include #include -#include #include -#include -#include #include -#include - #include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetooltexturebrush.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" -#include "../../model/world/columnbase.hpp" -#include "../../model/world/commandmacro.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/land.hpp" #include "../../model/world/landtexture.hpp" -#include "../../model/world/resourcetable.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "../widget/brushshapes.hpp" @@ -71,11 +61,11 @@ void CSVRender::TerrainTextureMode::activate(CSVWidget::SceneToolbar* toolbar) if (!mTerrainTextureSelection) { - mTerrainTextureSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture)); + mTerrainTextureSelection = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture); } if (!mBrushDraw) - mBrushDraw.reset(new BrushDraw(mParentNode, true)); + mBrushDraw = std::make_unique(mParentNode, true); EditMode::activate(toolbar); toolbar->addTool (mTextureBrushScenetool); @@ -119,7 +109,7 @@ void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == 0) + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { undoStack.beginMacro ("Edit texture records"); if(allowLandTextureEditing(mCellId)) @@ -133,17 +123,19 @@ void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == 0) + if(hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + mTerrainTextureSelection->clearTemporarySelection(); } } void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == 0) + if(hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + mTerrainTextureSelection->clearTemporarySelection(); } } @@ -166,7 +158,7 @@ bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos) CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == 0) + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { undoStack.beginMacro ("Edit texture records"); mIsEditing = true; @@ -189,26 +181,26 @@ bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; - if (!hit.hit || hit.tag != 0) + if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + return true; } bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; - if (!hit.hit || hit.tag != 0) + if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + return true; } void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) @@ -222,7 +214,7 @@ void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diff CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == 0) + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { editTerrainTextureGrid(hit); } @@ -231,13 +223,13 @@ void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diff if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == 0) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); + if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == 0) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); + if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); } } @@ -257,6 +249,8 @@ void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) mIsEditing = false; } } + + mTerrainTextureSelection->clearTemporarySelection(); } void CSVRender::TerrainTextureMode::dragAborted() @@ -332,7 +326,7 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); - std::size_t hashlocation = mBrushTexture.find("#"); + std::size_t hashlocation = mBrushTexture.find('#'); std::string mBrushTextureInt = mBrushTexture.substr (hashlocation+1); int brushInt = stoi(mBrushTexture.substr (hashlocation+1))+1; // All indices are offset by +1 @@ -427,14 +421,8 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe { if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) { - int distanceX(0); - int distanceY(0); - if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; - if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; - if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize* abs(i_cell-cellX) + i; - if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; - if (i_cell == cellX) distanceX = abs(i-xHitInCell); - if (j_cell == cellY) distanceY = abs(j-yHitInCell); + int distanceX = abs(i-xHitInCell); + int distanceY = abs(j-yHitInCell); float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); float rf = static_cast(mBrushSize) / 2; if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt; @@ -508,7 +496,7 @@ bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int } -void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; @@ -571,8 +559,21 @@ void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::paironlySelect(selections); - if(selectMode == 1) mTerrainTextureSelection->toggleSelect(selections, dragOperation); + std::string selectAction; + + if (selectMode == 0) + selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + else + selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if (selectAction == "Select only") + mTerrainTextureSelection->onlySelect(selections); + else if (selectAction == "Add to selection") + mTerrainTextureSelection->addSelect(selections); + else if (selectAction == "Remove from selection") + mTerrainTextureSelection->removeSelect(selections); + else if (selectAction == "Invert selection") + mTerrainTextureSelection->toggleSelect(selections); } void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, @@ -612,7 +613,7 @@ void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName) newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter; } - catch (const std::exception& e) + catch (const std::exception&) { newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); freeIndexFound = true; @@ -657,8 +658,7 @@ bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId) if (mode=="Create cell and land, then edit") { - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand (cellTable, cellId)); + auto createCommand = std::make_unique(cellTable, cellId); int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); createCommand->addNestedValue (parentIndex, index, false); @@ -724,6 +724,11 @@ void CSVRender::TerrainTextureMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainTextureMode::getTerrainSelection() +{ + return mTerrainTextureSelection; +} + void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) { diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp index 31932df217..06c95e3d79 100644 --- a/apps/opencs/view/render/terraintexturemode.hpp +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -85,6 +85,8 @@ namespace CSVRender void mouseMoveEvent (QMouseEvent *event) override; + std::shared_ptr getTerrainSelection(); + private: /// \brief Handle brush mechanics, maths regarding worldspace hit etc. void editTerrainTextureGrid (const WorldspaceHitResult& hit); @@ -93,7 +95,7 @@ namespace CSVRender bool isInCellSelection(int globalSelectionX, int globalSelectionY); /// \brief Handle brush mechanics for texture selection - void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode, bool dragOperation); + void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode); /// \brief Push texture edits to command macro void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, @@ -115,7 +117,7 @@ namespace CSVRender int mDragMode; osg::Group* mParentNode; bool mIsEditing; - std::unique_ptr mTerrainTextureSelection; + std::shared_ptr mTerrainTextureSelection; const int cellSize {ESM::Land::REAL_SIZE}; const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE}; diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index b1088aa60e..d9ba551ccb 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -2,8 +2,6 @@ #include -#include - #include #include "../../model/doc/document.hpp" @@ -12,7 +10,6 @@ #include "../../model/world/idtable.hpp" #include "../../model/world/tablemimedata.hpp" -#include "../widget/scenetooltoggle.hpp" #include "../widget/scenetooltoggle2.hpp" #include "cameracontroller.hpp" @@ -57,7 +54,7 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& update(); - mCell.reset (new Cell (document.getData(), mRootNode, mCellId)); + mCell = std::make_unique(document.getData(), mRootNode, mCellId); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, @@ -106,7 +103,7 @@ bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vectorgetId(); - mCell.reset (new Cell (getDocument().getData(), mRootNode, mCellId)); + mCell = std::make_unique(getDocument().getData(), mRootNode, mCellId); mCamPositionSet = false; mOrbitCamControl->reset(); @@ -140,6 +137,16 @@ void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId (int elementM flagAsModified(); } +void CSVRender::UnpagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) +{ + mCell->selectInsideCube (pointA, pointB, dragMode); +} + +void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) +{ + mCell->selectWithinDistance (point, distance, dragMode); +} + std::string CSVRender::UnpagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const { return mCellId; diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index eec1b01f34..83233c3270 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -60,6 +60,10 @@ namespace CSVRender /// \param elementMask Elements to be affected by the select operation void selectAllWithSameParentId (int elementMask) override; + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; + + void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; + std::string getCellId (const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) const override; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 4535b5e8af..a145d13d30 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -2,13 +2,10 @@ #include -#include #include #include #include #include -#include -#include #include #include @@ -17,7 +14,6 @@ #include "../../model/world/idtable.hpp" #include "../../model/prefs/shortcut.hpp" -#include "../../model/prefs/shortcuteventhandler.hpp" #include "../../model/prefs/state.hpp" #include "../render/orbitcameramode.hpp" @@ -34,11 +30,11 @@ CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) : SceneWidget (document.getData().getResourceSystem(), parent, Qt::WindowFlags(), false) - , mSceneElements(0) - , mRun(0) + , mSceneElements(nullptr) + , mRun(nullptr) , mDocument(document) , mInteractionMask (0) - , mEditMode (0) + , mEditMode (nullptr) , mLocked (false) , mDragMode(InteractionType_None) , mDragging (false) @@ -163,7 +159,7 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { - std::vector > selection = getSelection(~0); + std::vector > selection = getSelection(~0u); for (std::vector >::iterator it = selection.begin(); it!=selection.end(); ++it) { @@ -435,7 +431,7 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo } // Something untagged, probably terrain - WorldspaceHitResult hit = { true, 0, 0, 0, 0, intersection.getWorldIntersectPoint() }; + WorldspaceHitResult hit = { true, nullptr, 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; @@ -449,10 +445,15 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo direction.normalize(); direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt(); - WorldspaceHitResult hit = { false, 0, 0, 0, 0, start + direction }; + WorldspaceHitResult hit = { false, nullptr, 0, 0, 0, start + direction }; return hit; } +CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() +{ + return dynamic_cast (mEditMode->getCurrent()); +} + void CSVRender::WorldspaceWidget::abortDrag() { if (mDragging) @@ -460,7 +461,6 @@ void CSVRender::WorldspaceWidget::abortDrag() EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.dragAborted(); - mDragging = false; mDragMode = InteractionType_None; } } @@ -593,7 +593,7 @@ void CSVRender::WorldspaceWidget::showToolTip() if (hit.tag) { bool hideBasics = CSMPrefs::get()["Tooltips"]["scene-hide-basic"].isTrue(); - QToolTip::showText (pos, hit.tag->getToolTip (hideBasics), this); + QToolTip::showText(pos, hit.tag->getToolTip(hideBasics, hit), this); } } } @@ -698,11 +698,6 @@ void CSVRender::WorldspaceWidget::handleInteractionPress (const WorldspaceHitRes editMode.primaryOpenPressed (hit); } -CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() -{ - return dynamic_cast (mEditMode->getCurrent()); -} - void CSVRender::WorldspaceWidget::primaryOpen(bool activate) { handleInteraction(InteractionType_PrimaryOpen, activate); diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 5ed3d01b35..cf244ce712 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -7,6 +7,7 @@ #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" +#include "instancedragmodes.hpp" #include "scenewidget.hpp" #include "mask.hpp" @@ -96,7 +97,7 @@ namespace CSVRender InteractionType_None }; - WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = 0); + WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = nullptr); ~WorldspaceWidget (); CSVWidget::SceneToolMode *makeNavigationSelector (CSVWidget::SceneToolbar *parent); @@ -160,6 +161,10 @@ namespace CSVRender /// \param elementMask Elements to be affected by the select operation virtual void selectAllWithSameParentId (int elementMask) = 0; + virtual void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0; + + virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0; + /// Return the next intersection with scene elements matched by /// \a interactionMask based on \a localPos and the camera vector. /// If there is no such intersection, instead a point "in front" of \a localPos will be @@ -184,6 +189,8 @@ namespace CSVRender /// Erase all overrides and restore the visual representation to its true state. virtual void reset (unsigned int elementMask) = 0; + EditMode *getEditMode(); + protected: /// Visual elements in a scene @@ -210,8 +217,6 @@ namespace CSVRender void settingChanged (const CSMPrefs::Setting *setting) override; - EditMode *getEditMode(); - bool getSpeedMode(); private: diff --git a/apps/opencs/view/tools/merge.cpp b/apps/opencs/view/tools/merge.cpp index c49044ccd4..f50a85f2fc 100644 --- a/apps/opencs/view/tools/merge.cpp +++ b/apps/opencs/view/tools/merge.cpp @@ -27,7 +27,7 @@ void CSVTools::Merge::keyPressEvent (QKeyEvent *event) } CSVTools::Merge::Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent) -: QWidget (parent), mDocument (0), mDocumentManager (documentManager) +: QWidget (parent), mDocument (nullptr), mDocumentManager (documentManager) { setWindowTitle ("Merge Content Files into a new Game File"); @@ -117,7 +117,7 @@ CSMDoc::Document *CSVTools::Merge::getDocument() const void CSVTools::Merge::cancel() { - mDocument = 0; + mDocument = nullptr; hide(); } diff --git a/apps/opencs/view/tools/merge.hpp b/apps/opencs/view/tools/merge.hpp index c82feba14d..c7c4585979 100644 --- a/apps/opencs/view/tools/merge.hpp +++ b/apps/opencs/view/tools/merge.hpp @@ -1,5 +1,5 @@ -#ifndef CSV_TOOLS_REPORTTABLE_H -#define CSV_TOOLS_REPORTTABLE_H +#ifndef CSV_TOOLS_MERGE_H +#define CSV_TOOLS_MERGE_H #include @@ -39,7 +39,7 @@ namespace CSVTools public: - Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent = 0); + Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent = nullptr); /// Configure dialogue for a new merge void configure (CSMDoc::Document *document); diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index 7b28f2b0fd..c1297d475b 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -25,7 +25,7 @@ namespace CSVTools { public: - RichTextDelegate (QObject *parent = 0); + RichTextDelegate (QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; @@ -142,7 +142,7 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, bool richTextDescription, int refreshState, QWidget *parent) : CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)), - mRefreshAction (0), mRefreshState (refreshState) + mRefreshAction (nullptr), mRefreshState (refreshState) { horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); horizontalHeader()->setStretchLastSection (true); @@ -159,7 +159,7 @@ CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, setModel (mProxyModel); setColumnHidden (2, true); - mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate (0, + mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate (nullptr, mDocument, this); setItemDelegateForColumn (0, mIdTypeDelegate); diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index 4c169a9862..f39dd6f857 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -62,7 +62,7 @@ namespace CSVTools /// 0 no refresh function exists. If the document current has the specified state /// the refresh function is disabled. ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, - bool richTextDescription, int refreshState = 0, QWidget *parent = 0); + bool richTextDescription, int refreshState = 0, QWidget *parent = nullptr); std::vector getDraggedRecords() const override; diff --git a/apps/opencs/view/tools/searchbox.cpp b/apps/opencs/view/tools/searchbox.cpp index e0f965e3c8..f89f82ef79 100644 --- a/apps/opencs/view/tools/searchbox.cpp +++ b/apps/opencs/view/tools/searchbox.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include "../../model/world/columns.hpp" diff --git a/apps/opencs/view/tools/searchbox.hpp b/apps/opencs/view/tools/searchbox.hpp index eff5296b4a..cbeb150d8b 100644 --- a/apps/opencs/view/tools/searchbox.hpp +++ b/apps/opencs/view/tools/searchbox.hpp @@ -41,7 +41,7 @@ namespace CSVTools public: - SearchBox (QWidget *parent = 0); + SearchBox (QWidget *parent = nullptr); void setSearchMode (bool enabled); diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index 9bada22af3..d687cbeb3f 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -30,13 +30,13 @@ void CSVTools::SearchSubView::replace (bool selection) bool autoDelete = CSMPrefs::get()["Search & Replace"]["auto-delete"].isTrue(); CSMTools::Search search (mSearch); - CSMWorld::IdTableBase *currentTable = 0; + CSMWorld::IdTableBase *currentTable = nullptr; // We are running through the indices in reverse order to avoid messing up multiple results // in a single string. for (std::vector::const_reverse_iterator iter (indices.rbegin()); iter!=indices.rend(); ++iter) { - CSMWorld::UniversalId id = model.getUniversalId (*iter); + const CSMWorld::UniversalId& id = model.getUniversalId (*iter); CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType (id.getType()); diff --git a/apps/opencs/view/widget/coloreditor.cpp b/apps/opencs/view/widget/coloreditor.cpp index 1cc649313b..1d1f4af1ce 100644 --- a/apps/opencs/view/widget/coloreditor.cpp +++ b/apps/opencs/view/widget/coloreditor.cpp @@ -1,8 +1,6 @@ #include "coloreditor.hpp" -#include -#include -#include +#include #include #include #include diff --git a/apps/opencs/view/widget/coloreditor.hpp b/apps/opencs/view/widget/coloreditor.hpp index d4a802ca2f..aa746da682 100644 --- a/apps/opencs/view/widget/coloreditor.hpp +++ b/apps/opencs/view/widget/coloreditor.hpp @@ -22,8 +22,8 @@ namespace CSVWidget QPoint calculatePopupPosition(); public: - ColorEditor(const QColor &color, QWidget *parent = 0, const bool popupOnStart = false); - ColorEditor(const int colorInt, QWidget *parent = 0, const bool popupOnStart = false); + ColorEditor(const QColor &color, QWidget *parent = nullptr, const bool popupOnStart = false); + ColorEditor(const int colorInt, QWidget *parent = nullptr, const bool popupOnStart = false); QColor color() const; @@ -41,7 +41,7 @@ namespace CSVWidget void showEvent(QShowEvent *event) override; private: - ColorEditor(QWidget *parent = 0, const bool popupOnStart = false); + ColorEditor(QWidget *parent = nullptr, const bool popupOnStart = false); private slots: void showPicker(); diff --git a/apps/opencs/view/widget/colorpickerpopup.cpp b/apps/opencs/view/widget/colorpickerpopup.cpp index 206a667276..0c98698ea6 100644 --- a/apps/opencs/view/widget/colorpickerpopup.cpp +++ b/apps/opencs/view/widget/colorpickerpopup.cpp @@ -4,14 +4,14 @@ #include #include #include -#include -#include +#include +#include CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget *parent) : QFrame(parent) { setWindowFlags(Qt::Popup); - setFrameStyle(QFrame::Box | QFrame::Plain); + setFrameStyle(QFrame::Box | static_cast(QFrame::Plain)); hide(); mColorPicker = new QColorDialog(this); diff --git a/apps/opencs/view/widget/completerpopup.hpp b/apps/opencs/view/widget/completerpopup.hpp index 62fdf5388f..96675f56f4 100644 --- a/apps/opencs/view/widget/completerpopup.hpp +++ b/apps/opencs/view/widget/completerpopup.hpp @@ -8,7 +8,7 @@ namespace CSVWidget class CompleterPopup : public QListView { public: - CompleterPopup(QWidget *parent = 0); + CompleterPopup(QWidget *parent = nullptr); int sizeHintForRow(int row) const override; }; diff --git a/apps/opencs/view/widget/droplineedit.hpp b/apps/opencs/view/widget/droplineedit.hpp index ed991af0dd..9110518736 100644 --- a/apps/opencs/view/widget/droplineedit.hpp +++ b/apps/opencs/view/widget/droplineedit.hpp @@ -26,7 +26,7 @@ namespace CSVWidget ///< The accepted Display type for this LineEdit. public: - DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent = 0); + DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent = nullptr); protected: void dragEnterEvent(QDragEnterEvent *event) override; diff --git a/apps/opencs/view/widget/modebutton.hpp b/apps/opencs/view/widget/modebutton.hpp index 1615ff298a..f595969231 100644 --- a/apps/opencs/view/widget/modebutton.hpp +++ b/apps/opencs/view/widget/modebutton.hpp @@ -17,7 +17,7 @@ namespace CSVWidget public: ModeButton (const QIcon& icon, const QString& tooltip = "", - QWidget *parent = 0); + QWidget *parent = nullptr); /// Default-Implementation: do nothing virtual void activate (SceneToolbar *toolbar); diff --git a/apps/opencs/view/widget/pushbutton.hpp b/apps/opencs/view/widget/pushbutton.hpp index 5522cd74f6..b3aaaebef2 100644 --- a/apps/opencs/view/widget/pushbutton.hpp +++ b/apps/opencs/view/widget/pushbutton.hpp @@ -48,11 +48,11 @@ namespace CSVWidget /// \param push Do not maintain a toggle state PushButton (const QIcon& icon, Type type, const QString& tooltip = "", - QWidget *parent = 0); + QWidget *parent = nullptr); /// \param push Do not maintain a toggle state PushButton (Type type, const QString& tooltip = "", - QWidget *parent = 0); + QWidget *parent = nullptr); bool hasKeepOpen() const; diff --git a/apps/opencs/view/widget/scenetoolbar.hpp b/apps/opencs/view/widget/scenetoolbar.hpp index d9998eefc0..70f5807652 100644 --- a/apps/opencs/view/widget/scenetoolbar.hpp +++ b/apps/opencs/view/widget/scenetoolbar.hpp @@ -23,11 +23,11 @@ namespace CSVWidget public: - SceneToolbar (int buttonSize, QWidget *parent = 0); + SceneToolbar (int buttonSize, QWidget *parent = nullptr); /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise /// insert tool after \a insertPoint. - void addTool (SceneTool *tool, SceneTool *insertPoint = 0); + void addTool (SceneTool *tool, SceneTool *insertPoint = nullptr); void removeTool (SceneTool *tool); diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index 7b2ff64db4..ae923f5b37 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -33,7 +32,7 @@ void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) toolTip += "

(left click to change mode)"; - if (createContextMenu (0)) + if (createContextMenu (nullptr)) toolTip += "
(right click to access context menu)"; setToolTip (toolTip); @@ -62,7 +61,7 @@ void CSVWidget::SceneToolMode::setButton (std::map::i CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) : SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), - mToolTip (toolTip), mFirst (0), mCurrent (0), mToolbar (parent) + mToolTip (toolTip), mFirst (nullptr), mCurrent (nullptr), mToolbar (parent) { mPanel = new QFrame (this, Qt::Popup); diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp index b532820367..24bcf3f136 100644 --- a/apps/opencs/view/widget/scenetoolrun.cpp +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -30,7 +30,7 @@ void CSVWidget::SceneToolRun::updateIcon() void CSVWidget::SceneToolRun::updatePanel() { - mTable->setRowCount (mProfiles.size()); + mTable->setRowCount (static_cast(mProfiles.size())); int i = 0; diff --git a/apps/opencs/view/widget/scenetoolshapebrush.cpp b/apps/opencs/view/widget/scenetoolshapebrush.cpp index 4b2d20004b..dcb2fe8e41 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.cpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.cpp @@ -1,25 +1,20 @@ #include "scenetoolshapebrush.hpp" -#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 "brushshapes.hpp" #include "scenetool.hpp" @@ -27,11 +22,6 @@ #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" CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString &title, QWidget *parent) diff --git a/apps/opencs/view/widget/scenetoolshapebrush.hpp b/apps/opencs/view/widget/scenetoolshapebrush.hpp index 76c0dfa04e..e616e6958e 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.hpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.hpp @@ -1,18 +1,14 @@ #ifndef CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #define CSV_WIDGET_SCENETOOLSHAPEBRUSH_H -#include #include #include #include -#include #include #include #include #include -#include -#include #include #ifndef Q_MOC_RUN @@ -54,7 +50,7 @@ namespace CSVWidget public: - ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent = 0); + ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); void configureButtonInitialSettings(QPushButton *button); const QString toolTipPoint = "Paint single point"; diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index 272a5de42e..e412392dfc 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -1,24 +1,19 @@ #include "scenetooltexturebrush.hpp" -#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 "scenetool.hpp" @@ -179,13 +174,13 @@ void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) undoStack.endMacro(); } - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + if (index != -1 && !landtexturesCollection.getRecord(rowInNew).isDeleted()) { mBrushTextureLabel = "Selected texture: " + newBrushTextureId + " "; - mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); + mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(rowInNew, landTextureFilename).value()); } else { - newBrushTextureId = ""; + newBrushTextureId.clear(); mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } diff --git a/apps/opencs/view/widget/scenetooltexturebrush.hpp b/apps/opencs/view/widget/scenetooltexturebrush.hpp index c6f0b5e527..929ed1ebae 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.hpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.hpp @@ -1,18 +1,15 @@ #ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H -#include #include -#include - -#include -#include -#include #include -#include -#include #include +#include +#include #include +#include +#include +#include #ifndef Q_MOC_RUN #include "brushshapes.hpp" @@ -57,7 +54,7 @@ namespace CSVWidget Q_OBJECT public: - TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = 0); + TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); void configureButtonInitialSettings(QPushButton *button); const QString toolTipPoint = "Paint single point"; diff --git a/apps/opencs/view/widget/scenetooltoggle.cpp b/apps/opencs/view/widget/scenetooltoggle.cpp index 5919a280af..04ac3322bf 100644 --- a/apps/opencs/view/widget/scenetooltoggle.cpp +++ b/apps/opencs/view/widget/scenetooltoggle.cpp @@ -80,7 +80,7 @@ QRect CSVWidget::SceneToolToggle::getIconBox (int index) const int y = index / xMax; int x = index % xMax; - int total = mButtons.size(); + int total = static_cast(mButtons.size()); int actualYIcons = total/xMax; @@ -115,7 +115,7 @@ QRect CSVWidget::SceneToolToggle::getIconBox (int index) const CSVWidget::SceneToolToggle::SceneToolToggle (SceneToolbar *parent, const QString& toolTip, const std::string& emptyIcon) : SceneTool (parent), mEmptyIcon (emptyIcon), mButtonSize (parent->getButtonSize()), - mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (0) + mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (nullptr) { mPanel = new QFrame (this, Qt::Popup); @@ -154,7 +154,7 @@ void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned in desc.mMask = mask; desc.mSmallIcon = smallIcon; desc.mName = name; - desc.mIndex = mButtons.size(); + desc.mIndex = static_cast(mButtons.size()); mButtons.insert (std::make_pair (button, desc)); diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp index 720da6a964..4042c1fccb 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.cpp +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "scenetoolbar.hpp" #include "pushbutton.hpp" @@ -57,7 +56,7 @@ CSVWidget::SceneToolToggle2::SceneToolToggle2 (SceneToolbar *parent, const QStri const std::string& compositeIcon, const std::string& singleIcon) : SceneTool (parent), mCompositeIcon (compositeIcon), mSingleIcon (singleIcon), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), - mFirst (0) + mFirst (nullptr) { mPanel = new QFrame (this, Qt::Popup); @@ -99,7 +98,7 @@ void CSVWidget::SceneToolToggle2::addButton (unsigned int id, unsigned int mask, desc.mButtonId = id; desc.mMask = mask; desc.mName = name; - desc.mIndex = mButtons.size(); + desc.mIndex = static_cast(mButtons.size()); mButtons.insert (std::make_pair (button, desc)); diff --git a/apps/opencs/view/world/cellcreator.cpp b/apps/opencs/view/world/cellcreator.cpp index 5b428a4b37..22c27c3d74 100644 --- a/apps/opencs/view/world/cellcreator.cpp +++ b/apps/opencs/view/world/cellcreator.cpp @@ -84,6 +84,13 @@ void CSVWorld::CellCreator::setType (int index) mYLabel->setVisible (index==1); mY->setVisible (index==1); + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + std::string text = mType->currentText().toStdString(); + if (text == "Interior Cell") + GenericCreator::setEditorMaxLength (64); + else + GenericCreator::setEditorMaxLength (32767); + update(); } diff --git a/apps/opencs/view/world/colordelegate.cpp b/apps/opencs/view/world/colordelegate.cpp index 15a07b42cf..8d5f5485df 100644 --- a/apps/opencs/view/world/colordelegate.cpp +++ b/apps/opencs/view/world/colordelegate.cpp @@ -1,9 +1,6 @@ #include "colordelegate.hpp" #include -#include - -#include "../widget/coloreditor.hpp" CSVWorld::ColorDelegate::ColorDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, diff --git a/apps/opencs/view/world/creator.cpp b/apps/opencs/view/world/creator.cpp index 7a93339c5b..53664c186a 100644 --- a/apps/opencs/view/world/creator.cpp +++ b/apps/opencs/view/world/creator.cpp @@ -17,5 +17,5 @@ CSVWorld::CreatorFactoryBase::~CreatorFactoryBase() {} CSVWorld::Creator *CSVWorld::NullCreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return 0; + return nullptr; } diff --git a/apps/opencs/view/world/creator.hpp b/apps/opencs/view/world/creator.hpp index 516f71f15c..7c61c6e6be 100644 --- a/apps/opencs/view/world/creator.hpp +++ b/apps/opencs/view/world/creator.hpp @@ -96,7 +96,7 @@ namespace CSVWorld Creator *CreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - std::unique_ptr creator (new CreatorT (document.getData(), document.getUndoStack(), id)); + auto creator = std::make_unique(document.getData(), document.getUndoStack(), id); creator->setScope (scope); diff --git a/apps/opencs/view/world/datadisplaydelegate.hpp b/apps/opencs/view/world/datadisplaydelegate.hpp index df06359a04..52e2294f03 100755 --- a/apps/opencs/view/world/datadisplaydelegate.hpp +++ b/apps/opencs/view/world/datadisplaydelegate.hpp @@ -1,7 +1,6 @@ #ifndef DATADISPLAYDELEGATE_HPP #define DATADISPLAYDELEGATE_HPP -#include #include "enumdelegate.hpp" namespace CSMPrefs diff --git a/apps/opencs/view/world/dialoguecreator.cpp b/apps/opencs/view/world/dialoguecreator.cpp index 7c6fb2e81f..9317aa95cd 100644 --- a/apps/opencs/view/world/dialoguecreator.cpp +++ b/apps/opencs/view/world/dialoguecreator.cpp @@ -1,12 +1,8 @@ #include "dialoguecreator.hpp" -#include +#include -#include "../../model/doc/document.hpp" - -#include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" void CSVWorld::DialogueCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const diff --git a/apps/opencs/view/world/dialoguespinbox.hpp b/apps/opencs/view/world/dialoguespinbox.hpp index b7c4889a50..90fe8d20cd 100644 --- a/apps/opencs/view/world/dialoguespinbox.hpp +++ b/apps/opencs/view/world/dialoguespinbox.hpp @@ -12,7 +12,7 @@ namespace CSVWorld public: - DialogueSpinBox (QWidget *parent = 0); + DialogueSpinBox (QWidget *parent = nullptr); protected: @@ -27,7 +27,7 @@ namespace CSVWorld public: - DialogueDoubleSpinBox (QWidget *parent = 0); + DialogueDoubleSpinBox (QWidget *parent = nullptr); protected: diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index e29fcb779a..459f55780a 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -4,22 +4,18 @@ #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 "../../model/world/nestedtableproxymodel.hpp" #include "../../model/world/columnbase.hpp" @@ -28,7 +24,6 @@ #include "../../model/world/columns.hpp" #include "../../model/world/record.hpp" #include "../../model/world/tablemimedata.hpp" -#include "../../model/world/idtree.hpp" #include "../../model/world/commands.hpp" #include "../../model/doc/document.hpp" @@ -134,7 +129,7 @@ void CSVWorld::DialogueDelegateDispatcherProxy::editorDataCommited() void CSVWorld::DialogueDelegateDispatcherProxy::setIndex(const QModelIndex& index) { - mIndexWrapper.reset(new refWrapper(index)); + mIndexWrapper = std::make_unique(index); } QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const @@ -498,7 +493,7 @@ void CSVWorld::EditWidget::remake(int row) if (mDispatcher) delete mDispatcher; - mDispatcher = new DialogueDelegateDispatcher(0/*this*/, mTable, mCommandDispatcher, mDocument); + mDispatcher = new DialogueDelegateDispatcher(nullptr/*this*/, mTable, mCommandDispatcher, mDocument); if (mNestedTableDispatcher) delete mNestedTableDispatcher; @@ -539,6 +534,9 @@ void CSVWorld::EditWidget::remake(int row) mainLayout->addLayout(tablesLayout, QSizePolicy::Preferred); mainLayout->addStretch(1); + int blockedColumn = mTable->searchColumnIndex(CSMWorld::Columns::ColumnId_Blocked); + bool isBlocked = mTable->data(mTable->index(row, blockedColumn)).toInt(); + int unlocked = 0; int locked = 0; const int columns = mTable->columnCount(); @@ -584,6 +582,8 @@ void CSVWorld::EditWidget::remake(int row) NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); table->resizeColumnsToContents(); + if (isBlocked) + table->setEditTriggers(QAbstractItemView::NoEditTriggers); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); @@ -618,7 +618,9 @@ void CSVWorld::EditWidget::remake(int row) label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - if (! (mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable)) + // HACK: the blocked checkbox needs to keep the same position + // FIXME: unfortunately blocked record displays a little differently to unblocked one + if (!(mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable) || i == blockedColumn) { lockedLayout->addWidget (label, locked, 0); lockedLayout->addWidget (editor, locked, 1); @@ -640,7 +642,7 @@ void CSVWorld::EditWidget::remake(int row) createEditorContextMenu(editor, display, row); } } - else + else // Flag_Dialogue_List { CSMWorld::IdTree *tree = static_cast(mTable); mNestedTableMapper = new QDataWidgetMapper (this); @@ -648,7 +650,7 @@ void CSVWorld::EditWidget::remake(int row) mNestedTableMapper->setModel(tree); // FIXME: lack MIME support? mNestedTableDispatcher = - new DialogueDelegateDispatcher (0/*this*/, mTable, mCommandDispatcher, mDocument, tree); + new DialogueDelegateDispatcher (nullptr/*this*/, mTable, mCommandDispatcher, mDocument, tree); mNestedTableMapper->setRootIndex (tree->index(row, i)); mNestedTableMapper->setItemDelegate(mNestedTableDispatcher); @@ -687,7 +689,10 @@ void CSVWorld::EditWidget::remake(int row) label->setEnabled(false); } - createEditorContextMenu(editor, display, row); + if (!isBlocked) + createEditorContextMenu(editor, display, row); + else + editor->setEnabled(false); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); @@ -732,7 +737,7 @@ bool CSVWorld::SimpleDialogueSubView::isLocked() const CSVWorld::SimpleDialogueSubView::SimpleDialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), - mEditWidget(0), + mEditWidget(nullptr), mMainLayout(nullptr), mTable(dynamic_cast(document.getData().getTableModel(id))), mLocked(false), @@ -834,7 +839,7 @@ void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex &pa if(mEditWidget) { delete mEditWidget; - mEditWidget = 0; + mEditWidget = nullptr; } emit closeRequest(this); } @@ -869,7 +874,7 @@ void CSVWorld::DialogueSubView::addButtonBar() CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) -: SimpleDialogueSubView (id, document), mButtons (0) +: SimpleDialogueSubView (id, document), mButtons (nullptr) { // bottom box mBottom = new TableBottomBox (creatorFactory, document, id, this); @@ -905,7 +910,7 @@ void CSVWorld::DialogueSubView::settingChanged (const CSMPrefs::Setting *setting { getMainLayout().removeWidget (mButtons); delete mButtons; - mButtons = 0; + mButtons = nullptr; } } } diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index eb14efa8e5..2cf05f711e 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -50,7 +50,7 @@ namespace CSVWorld const CSMWorld::IdTable* mTable; public: NotEditableSubDelegate(const CSMWorld::IdTable* table, - QObject * parent = 0); + QObject * parent = nullptr); void setEditorData (QWidget* editor, const QModelIndex& index) const override; @@ -126,7 +126,7 @@ namespace CSVWorld CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, - QAbstractItemModel* model = 0); + QAbstractItemModel* model = nullptr); ~DialogueDelegateDispatcher(); diff --git a/apps/opencs/view/world/dragdroputils.cpp b/apps/opencs/view/world/dragdroputils.cpp index 789d4f33dc..bb4d972737 100644 --- a/apps/opencs/view/world/dragdroputils.cpp +++ b/apps/opencs/view/world/dragdroputils.cpp @@ -15,12 +15,21 @@ bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent &event, CSMWorld::C return data != nullptr && data->holdsType(type); } -CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event, +bool CSVWorld::DragDropUtils::isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type) +{ + const CSMWorld::TableMimeData *data = getTableMimeData(event); + return data != nullptr && ( + data->holdsType(CSMWorld::UniversalId::Type_TopicInfo) || + data->holdsType(CSMWorld::UniversalId::Type_JournalInfo) ); +} + +CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type) { if (canAcceptData(event, type)) { - return getTableMimeData(event)->returnMatching(type); + if (const CSMWorld::TableMimeData *data = getTableMimeData(event)) + return data->returnMatching(type); } return CSMWorld::UniversalId::Type_None; } diff --git a/apps/opencs/view/world/dragdroputils.hpp b/apps/opencs/view/world/dragdroputils.hpp index d1d780708e..2181e7606b 100644 --- a/apps/opencs/view/world/dragdroputils.hpp +++ b/apps/opencs/view/world/dragdroputils.hpp @@ -20,6 +20,9 @@ namespace CSVWorld bool canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); ///< Checks whether the \a event contains a valid CSMWorld::TableMimeData that holds the \a type + bool isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type); + ///< Info types can be dragged to sort the info table + CSMWorld::UniversalId getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); ///< Gets the accepted data from the \a event using the \a type ///< \return Type_None if the \a event data doesn't holds the \a type diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index d795bd5de1..58041af9fc 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -19,14 +19,10 @@ void CSVWorld::DragRecordTable::startDragFromTable (const CSVWorld::DragRecordTa } CSMWorld::TableMimeData* mime = new CSMWorld::TableMimeData (records, mDocument); - - if (mime) - { - QDrag* drag = new QDrag (this); - drag->setMimeData (mime); - drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str())); - drag->exec (Qt::CopyAction); - } + QDrag* drag = new QDrag (this); + drag->setMimeData (mime); + drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str())); + drag->exec (Qt::CopyAction); } CSVWorld::DragRecordTable::DragRecordTable (CSMDoc::Document& document, QWidget* parent) : @@ -50,7 +46,8 @@ void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent *event) void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent *event) { QModelIndex index = indexAt(event->pos()); - if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index))) + if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || + CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) ) { if (index.flags() & Qt::ItemIsEditable) { @@ -79,6 +76,10 @@ void CSVWorld::DragRecordTable::dropEvent(QDropEvent *event) } } } + else if (CSVWorld::DragDropUtils::isInfo(*event, display) && event->source() == this) + { + emit moveRecordsFromSameTable(event); + } } CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(const QModelIndex &index) const diff --git a/apps/opencs/view/world/dragrecordtable.hpp b/apps/opencs/view/world/dragrecordtable.hpp index a6b6756aa0..4b986f759a 100644 --- a/apps/opencs/view/world/dragrecordtable.hpp +++ b/apps/opencs/view/world/dragrecordtable.hpp @@ -2,7 +2,6 @@ #define CSV_WORLD_DRAGRECORDTABLE_H #include -#include #include "../../model/world/columnbase.hpp" @@ -23,6 +22,8 @@ namespace CSVWorld { class DragRecordTable : public QTableView { + Q_OBJECT + protected: CSMDoc::Document& mDocument; bool mEditLock; @@ -45,6 +46,9 @@ namespace CSVWorld private: CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex &index) const; + + signals: + void moveRecordsFromSameTable(QDropEvent *event); }; } diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 3140adc485..6ec585381c 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -5,7 +5,6 @@ #include #include -#include #include "../../model/world/commands.hpp" @@ -71,13 +70,15 @@ QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptio const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { if (!index.data(Qt::EditRole).isValid() && !index.data(Qt::DisplayRole).isValid()) - return 0; + return nullptr; QComboBox *comboBox = new QComboBox (parent); for (std::vector >::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) comboBox->addItem (iter->second); + + comboBox->setMaxVisibleItems(20); return comboBox; } diff --git a/apps/opencs/view/world/enumdelegate.hpp b/apps/opencs/view/world/enumdelegate.hpp index 91326e2c05..c5a851c9f8 100644 --- a/apps/opencs/view/world/enumdelegate.hpp +++ b/apps/opencs/view/world/enumdelegate.hpp @@ -4,7 +4,6 @@ #include #include -#include #include diff --git a/apps/opencs/view/world/extendedcommandconfigurator.cpp b/apps/opencs/view/world/extendedcommandconfigurator.cpp index 8947420242..d7ed39e19e 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.cpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "../../model/doc/document.hpp" diff --git a/apps/opencs/view/world/extendedcommandconfigurator.hpp b/apps/opencs/view/world/extendedcommandconfigurator.hpp index 42573924a8..85862ac49e 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.hpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.hpp @@ -57,7 +57,7 @@ namespace CSVWorld public: ExtendedCommandConfigurator(CSMDoc::Document &document, const CSMWorld::UniversalId &id, - QWidget *parent = 0); + QWidget *parent = nullptr); void configure(Mode mode, const std::vector &selectedIds); void setEditLock(bool locked); diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index 5e2118e9b2..7061c8a8b5 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -148,8 +148,8 @@ void CSVWorld::GenericCreator::addScope (const QString& name, CSMWorld::Scope sc CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules) : mData (data), mUndoStack (undoStack), mListId (id), mLocked (false), - mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (0), - mScopeLabel (0), mCloneMode (false) + mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (nullptr), + mScopeLabel (nullptr), mCloneMode (false) { // If the collection ID has a parent type, use it instead. // It will change IDs with Record/SubRecord class (used for creators in Dialogue subviews) @@ -184,6 +184,11 @@ CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undo connect (&mData, SIGNAL (idListChanged()), this, SLOT (dataIdListChanged())); } +void CSVWorld::GenericCreator::setEditorMaxLength (int length) +{ + mId->setMaxLength (length); +} + void CSVWorld::GenericCreator::setEditLock (bool locked) { mLocked = locked; @@ -233,13 +238,13 @@ void CSVWorld::GenericCreator::create() if (mCloneMode) { - command.reset (new CSMWorld::CloneCommand ( - dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType)); + command = std::make_unique( + dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType); } else { - command.reset (new CSMWorld::CreateCommand ( - dynamic_cast (*mData.getTableModel (mListId)), id)); + command = std::make_unique( + dynamic_cast (*mData.getTableModel (mListId)), id); } @@ -322,10 +327,10 @@ void CSVWorld::GenericCreator::setScope (unsigned int scope) else { delete mScope; - mScope = 0; + mScope = nullptr; delete mScopeLabel; - mScopeLabel = 0; + mScopeLabel = nullptr; } updateNamespace(); diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 3e2a43c918..90c5946ae5 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -84,6 +84,8 @@ namespace CSVWorld std::string getNamespace() const; + void setEditorMaxLength(int length); + private: void updateNamespace(); diff --git a/apps/opencs/view/world/globalcreator.cpp b/apps/opencs/view/world/globalcreator.cpp index c7b140e156..6c5b75fb56 100644 --- a/apps/opencs/view/world/globalcreator.cpp +++ b/apps/opencs/view/world/globalcreator.cpp @@ -1,10 +1,8 @@ #include "globalcreator.hpp" -#include +#include -#include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" namespace CSVWorld diff --git a/apps/opencs/view/world/idcompletiondelegate.cpp b/apps/opencs/view/world/idcompletiondelegate.cpp index 4ff850b9f4..9ef04ec3ab 100644 --- a/apps/opencs/view/world/idcompletiondelegate.cpp +++ b/apps/opencs/view/world/idcompletiondelegate.cpp @@ -74,13 +74,30 @@ QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, { return new CSVWidget::DropLineEdit(display, parent); } - default: return 0; // The rest of them can't be edited anyway + default: return nullptr; // The rest of them can't be edited anyway } } CSMWorld::IdCompletionManager &completionManager = getDocument().getIdCompletionManager(); CSVWidget::DropLineEdit *editor = new CSVWidget::DropLineEdit(display, parent); editor->setCompleter(completionManager.getCompleter(display).get()); + + // The savegame format limits the player faction string to 32 characters. + // The region sound name is limited to 32 characters. (ESM::Region::SoundRef::mSound) + // The script name is limited to 32 characters. (ESM::Script::SCHD::mName) + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + if (display == CSMWorld::ColumnBase::Display_Faction || + display == CSMWorld::ColumnBase::Display_Sound || + display == CSMWorld::ColumnBase::Display_Script || + display == CSMWorld::ColumnBase::Display_Referenceable) + { + editor->setMaxLength (32); + } + else if (display == CSMWorld::ColumnBase::Display_Cell) + { + editor->setMaxLength (64); + } + return editor; } diff --git a/apps/opencs/view/world/idvalidator.cpp b/apps/opencs/view/world/idvalidator.cpp index 1092d72171..442157ac5d 100644 --- a/apps/opencs/view/world/idvalidator.cpp +++ b/apps/opencs/view/world/idvalidator.cpp @@ -42,7 +42,7 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con if (!mNamespace.empty()) { - std::string namespace_ = input.left (mNamespace.size()).toUtf8().constData(); + std::string namespace_ = input.left (static_cast(mNamespace.size())).toUtf8().constData(); if (Misc::StringUtils::lowerCase (namespace_)!=mNamespace) return QValidator::Invalid; // incorrect namespace diff --git a/apps/opencs/view/world/idvalidator.hpp b/apps/opencs/view/world/idvalidator.hpp index 17624a243b..278335a65b 100644 --- a/apps/opencs/view/world/idvalidator.hpp +++ b/apps/opencs/view/world/idvalidator.hpp @@ -19,7 +19,7 @@ namespace CSVWorld public: - IdValidator (bool relaxed = false, QObject *parent = 0); + IdValidator (bool relaxed = false, QObject *parent = nullptr); ///< \param relaxed Relaxed rules for IDs that also functino as user visible text State validate (QString& input, int& pos) const override; diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index 2f1615c876..cf1b48a195 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -34,16 +34,29 @@ void CSVWorld::InfoCreator::configureCreateCommand (CSMWorld::CreateCommand& com { CSMWorld::IdTable& table = dynamic_cast (*getData().getTableModel (getCollectionId())); + CSMWorld::CloneCommand* cloneCommand = dynamic_cast (&command); if (getCollectionId() == CSMWorld::UniversalId::Type_TopicInfos) { - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1); + if (!cloneCommand) + { + command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); + command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1); + command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1); + command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1); + } + else + { + cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); + } } else { - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); + if (!cloneCommand) + { + command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); + } + else + cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); } } diff --git a/apps/opencs/view/world/nestedtable.cpp b/apps/opencs/view/world/nestedtable.cpp index d52f7ca736..c69913f0ac 100644 --- a/apps/opencs/view/world/nestedtable.cpp +++ b/apps/opencs/view/world/nestedtable.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include "../../model/prefs/shortcut.hpp" diff --git a/apps/opencs/view/world/nestedtable.hpp b/apps/opencs/view/world/nestedtable.hpp index f864f5d80c..da4afe6420 100644 --- a/apps/opencs/view/world/nestedtable.hpp +++ b/apps/opencs/view/world/nestedtable.hpp @@ -1,8 +1,6 @@ #ifndef CSV_WORLD_NESTEDTABLE_H #define CSV_WORLD_NESTEDTABLE_H -#include - #include "dragrecordtable.hpp" class QAction; diff --git a/apps/opencs/view/world/previewsubview.cpp b/apps/opencs/view/world/previewsubview.cpp index f3312bb208..cb231b4782 100644 --- a/apps/opencs/view/world/previewsubview.cpp +++ b/apps/opencs/view/world/previewsubview.cpp @@ -25,6 +25,8 @@ CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDo else mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), true, this); + mScene->setExterior(true); + CSVWidget::SceneToolbar *toolbar = new CSVWidget::SceneToolbar (48+6, this); CSVWidget::SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar); diff --git a/apps/opencs/view/world/recordbuttonbar.hpp b/apps/opencs/view/world/recordbuttonbar.hpp index fbee066cea..aca3211f8a 100644 --- a/apps/opencs/view/world/recordbuttonbar.hpp +++ b/apps/opencs/view/world/recordbuttonbar.hpp @@ -58,8 +58,8 @@ namespace CSVWorld public: RecordButtonBar (const CSMWorld::UniversalId& id, - CSMWorld::IdTable& table, TableBottomBox *bottomBox = 0, - CSMWorld::CommandDispatcher *commandDispatcher = 0, QWidget *parent = 0); + CSMWorld::IdTable& table, TableBottomBox *bottomBox = nullptr, + CSMWorld::CommandDispatcher *commandDispatcher = nullptr, QWidget *parent = nullptr); void setEditLock (bool locked); diff --git a/apps/opencs/view/world/recordstatusdelegate.cpp b/apps/opencs/view/world/recordstatusdelegate.cpp index fd98fe6cd9..dd45baff0f 100644 --- a/apps/opencs/view/world/recordstatusdelegate.cpp +++ b/apps/opencs/view/world/recordstatusdelegate.cpp @@ -1,9 +1,5 @@ #include "recordstatusdelegate.hpp" -#include -#include -#include - #include "../../model/world/columns.hpp" CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, diff --git a/apps/opencs/view/world/recordstatusdelegate.hpp b/apps/opencs/view/world/recordstatusdelegate.hpp index 6ec8c37bd8..ece19e26d5 100644 --- a/apps/opencs/view/world/recordstatusdelegate.hpp +++ b/apps/opencs/view/world/recordstatusdelegate.hpp @@ -2,8 +2,6 @@ #define RECORDSTATUSDELEGATE_H #include "util.hpp" -#include -#include #include "datadisplaydelegate.hpp" #include "../../model/world/record.hpp" @@ -19,7 +17,7 @@ namespace CSVWorld RecordStatusDelegate (const ValueList& values, const IconList& icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, - QObject *parent = 0); + QObject *parent = nullptr); }; class RecordStatusDelegateFactory : public DataDisplayDelegateFactory diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 836e8ac7dc..6bc0126b3e 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -22,7 +22,8 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); mType = new QComboBox (this); - + mType->setMaxVisibleItems(20); + for (std::vector::const_iterator iter (types.begin()); iter!=types.end(); ++iter) { @@ -31,8 +32,12 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd mType->addItem (QIcon (id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast (id2.getType())); } - + + mType->model()->sort(0); + insertBeforeButtons (mType, false); + + connect (mType, SIGNAL (currentIndexChanged (int)), this, SLOT (setType (int))); } void CSVWorld::ReferenceableCreator::reset() @@ -41,6 +46,30 @@ void CSVWorld::ReferenceableCreator::reset() GenericCreator::reset(); } +void CSVWorld::ReferenceableCreator::setType (int index) +{ + // container items have name limit of 32 characters + std::string text = mType->currentText().toStdString(); + if (text == "Potion" || + text == "Apparatus" || + text == "Armor" || + text == "Book" || + text == "Clothing" || + text == "Ingredient" || + text == "ItemLevelledList" || + text == "Light" || + text == "Lockpick" || + text == "Miscellaneous" || + text == "Probe" || + text == "Repair" || + text == "Weapon") + { + GenericCreator::setEditorMaxLength (32); + } + else + GenericCreator::setEditorMaxLength (32767); +} + void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) { diff --git a/apps/opencs/view/world/referenceablecreator.hpp b/apps/opencs/view/world/referenceablecreator.hpp index d4657bcf7f..354347cc88 100644 --- a/apps/opencs/view/world/referenceablecreator.hpp +++ b/apps/opencs/view/world/referenceablecreator.hpp @@ -29,6 +29,9 @@ namespace CSVWorld void toggleWidgets(bool active = true) override; + private slots: + + void setType (int index); }; } diff --git a/apps/opencs/view/world/regionmap.hpp b/apps/opencs/view/world/regionmap.hpp index b1f7cdc674..fe88987848 100644 --- a/apps/opencs/view/world/regionmap.hpp +++ b/apps/opencs/view/world/regionmap.hpp @@ -4,9 +4,6 @@ #include #include -#include -#include - #include "./dragrecordtable.hpp" class QAction; @@ -61,7 +58,7 @@ namespace CSVWorld public: RegionMap (const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, - QWidget *parent = 0); + QWidget *parent = nullptr); std::vector getDraggedRecords() const override; diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index 44542c5297..58d159a178 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -15,7 +15,6 @@ #include "../render/pagedworldspacewidget.hpp" #include "../render/unpagedworldspacewidget.hpp" -#include "../render/editmode.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index aabb7ca2a7..53cd54e7ac 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -32,7 +32,6 @@ namespace CSVWidget namespace CSVWorld { - class Table; class TableBottomBox; class CreatorFactoryBase; diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 9083359d27..1217d36f64 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -47,7 +46,7 @@ CSVWorld::ScriptEdit::ScriptEdit( ) : QPlainTextEdit(parent), mChangeLocked(0), mShowLineNum(false), - mLineNumberArea(0), + mLineNumberArea(nullptr), mDefaultFont(font()), mMonoFont(QFont("Monospace")), mTabCharCount(4), @@ -314,7 +313,7 @@ void CSVWorld::ScriptEdit::markOccurrences() // prevent infinite recursion with cursor.select(), // which ends up calling this function again // could be fixed with blockSignals, but mDocument is const - disconnect(this, SIGNAL(cursorPositionChanged()), this, 0); + disconnect(this, SIGNAL(cursorPositionChanged()), this, nullptr); cursor.select(QTextCursor::WordUnderCursor); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(markOccurrences())); diff --git a/apps/opencs/view/world/scripterrortable.cpp b/apps/opencs/view/world/scripterrortable.cpp index 45809b28c6..a331820a2b 100644 --- a/apps/opencs/view/world/scripterrortable.cpp +++ b/apps/opencs/view/world/scripterrortable.cpp @@ -1,5 +1,7 @@ #include "scripterrortable.hpp" +#include + #include #include diff --git a/apps/opencs/view/world/scripterrortable.hpp b/apps/opencs/view/world/scripterrortable.hpp index ad287707dd..7165d0fc6a 100644 --- a/apps/opencs/view/world/scripterrortable.hpp +++ b/apps/opencs/view/world/scripterrortable.hpp @@ -41,7 +41,7 @@ namespace CSVWorld public: - ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent = 0); + ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent = nullptr); void update (const std::string& source); diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index 58ad094519..5036077fc3 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -2,8 +2,6 @@ #include -#include -#include #include #include @@ -12,7 +10,6 @@ #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/data.hpp" -#include "../../model/world/columnbase.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/prefs/state.hpp" @@ -88,7 +85,7 @@ void CSVWorld::ScriptSubView::adjustSplitter() } CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mDocument (document), mColumn (-1), mBottom(0), mButtons (0), +: SubView (id), mDocument (document), mColumn (-1), mBottom(nullptr), mButtons (nullptr), mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())), mErrorHeight (CSMPrefs::get()["Scripts"]["error-height"].toInt()) { @@ -110,7 +107,7 @@ CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc: sizes << 1 << 0; mMain->setSizes (sizes); - QWidget *widget = new QWidget (this);; + QWidget *widget = new QWidget (this); widget->setLayout (&mLayout); setWidget (widget); @@ -177,7 +174,7 @@ void CSVWorld::ScriptSubView::settingChanged (const CSMPrefs::Setting *setting) { mLayout.removeWidget (mButtons); delete mButtons; - mButtons = 0; + mButtons = nullptr; } } else if (*setting=="Scripts/compile-delay") diff --git a/apps/opencs/view/world/scriptsubview.hpp b/apps/opencs/view/world/scriptsubview.hpp index dc352cc5b0..a4171db6ee 100644 --- a/apps/opencs/view/world/scriptsubview.hpp +++ b/apps/opencs/view/world/scriptsubview.hpp @@ -9,7 +9,6 @@ class QModelIndex; class QLabel; -class QVBoxLayout; class QSplitter; class QTime; diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 3e72f9a9e6..169bc2e94e 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -75,10 +75,10 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_TopicInfos, - new CSVDoc::SubViewFactoryWithCreator); + new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_JournalInfos, - new CSVDoc::SubViewFactoryWithCreator); + new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_Pathgrids, new CSVDoc::SubViewFactoryWithCreator); diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index e5f4e36c5b..a3ab3d19d3 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -5,8 +5,9 @@ #include #include #include -#include +#include +#include #include #include @@ -14,18 +15,16 @@ #include "../../model/world/commands.hpp" #include "../../model/world/infotableproxymodel.hpp" -#include "../../model/world/idtableproxymodel.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexturetableproxymodel.hpp" -#include "../../model/world/record.hpp" -#include "../../model/world/columns.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "tableeditidaction.hpp" +#include "tableheadermouseeventhandler.hpp" #include "util.hpp" void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) @@ -238,7 +237,7 @@ void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) CSVWorld::Table::Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) : DragRecordTable(document), mCreateAction (nullptr), mCloneAction(nullptr), mTouchAction(nullptr), - mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) + mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false), mAutoJump (false) { mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); @@ -248,6 +247,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, if (isInfoTable) { mProxyModel = new CSMWorld::InfoTableProxyModel(id.getType(), this); + connect (this, &CSVWorld::DragRecordTable::moveRecordsFromSameTable, this, &CSVWorld::Table::moveRecords); } else if (isLtexTable) { @@ -267,12 +267,6 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); - setSortingEnabled (sorting); - if (sorting) - { - sortByColumn (mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); - } - int columns = mModel->columnCount(); for (int i=0; ifindColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); + } + setSortingEnabled (sorting); + mEditAction = new QAction (tr ("Edit Record"), this); connect (mEditAction, SIGNAL (triggered()), this, SLOT (editRecord())); mEditAction->setIcon(QIcon(":edit-edit")); @@ -403,7 +404,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, /// \note This signal could instead be connected to a slot that filters out changes not affecting /// the records status column (for permanence reasons) connect (mProxyModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (tableSizeUpdate())); + this, SLOT (dataChangedEvent(const QModelIndex&, const QModelIndex&))); connect (selectionModel(), SIGNAL (selectionChanged (const QItemSelection&, const QItemSelection&)), this, SLOT (selectionSizeUpdate ())); @@ -418,6 +419,8 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["ID Tables"].update(); + + new TableHeaderMouseEventHandler(this); } void CSVWorld::Table::setEditLock (bool locked) @@ -563,6 +566,77 @@ void CSVWorld::Table::moveDownRecord() } } +void CSVWorld::Table::moveRecords(QDropEvent *event) +{ + if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) + return; + + QModelIndex targedIndex = indexAt(event->pos()); + + QModelIndexList selectedRows = selectionModel()->selectedRows(); + int targetRowRaw = targedIndex.row(); + int targetRow = mProxyModel->mapToSource (mProxyModel->index (targetRowRaw, 0)).row(); + int baseRowRaw = targedIndex.row() - 1; + int baseRow = mProxyModel->mapToSource (mProxyModel->index (baseRowRaw, 0)).row(); + int highestDifference = 0; + + for (const auto& thisRowData : selectedRows) + { + int thisRow = mProxyModel->mapToSource (mProxyModel->index (thisRowData.row(), 0)).row(); + if (std::abs(targetRow - thisRow) > highestDifference) highestDifference = std::abs(targetRow - thisRow); + if (thisRow - 1 < baseRow) baseRow = thisRow - 1; + } + + std::vector newOrder (highestDifference + 1); + + for (long unsigned int i = 0; i < newOrder.size(); ++i) + { + newOrder[i] = i; + } + + if (selectedRows.size() > 1) + { + Log(Debug::Warning) << "Move operation failed: Moving multiple selections isn't implemented."; + return; + } + + for (const auto& thisRowData : selectedRows) + { + /* + Moving algorithm description + a) Remove the (ORIGIN + 1)th list member. + b) Add (ORIGIN+1)th list member with value TARGET + c) If ORIGIN > TARGET,d_INC; ELSE d_DEC + d_INC) increase all members after (and including) the TARGET by one, stop before hitting ORIGINth address + d_DEC) decrease all members after the ORIGIN by one, stop after hitting address TARGET + */ + + int originRowRaw = thisRowData.row(); + int originRow = mProxyModel->mapToSource (mProxyModel->index (originRowRaw, 0)).row(); + + newOrder.erase(newOrder.begin() + originRow - baseRow - 1); + newOrder.emplace(newOrder.begin() + originRow - baseRow - 1, targetRow - baseRow - 1); + + if (originRow > targetRow) + { + for (int i = targetRow - baseRow - 1; i < originRow - baseRow - 1; ++i) + { + ++newOrder[i]; + } + } + else + { + for (int i = originRow - baseRow; i <= targetRow - baseRow - 1; ++i) + { + --newOrder[i]; + } + } + + } + mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand ( + dynamic_cast (*mModel), baseRow + 1, newOrder)); +} + void CSVWorld::Table::editCell() { emit editRequest(mEditIdAction->getCurrentId(), ""); @@ -728,7 +802,7 @@ void CSVWorld::Table::tableSizeUpdate() case CSMWorld::RecordBase::State_BaseOnly: ++size; break; case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; - case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; + case CSMWorld::RecordBase::State_Deleted: ++deleted; ++modified; break; } } } @@ -801,15 +875,47 @@ std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const return idToDrag; } +// parent, start and end depend on the model sending the signal, in this case mProxyModel +// +// If, for example, mModel was used instead, then scrolTo() should use the index +// mProxyModel->mapFromSource(mModel->index(end, 0)) void CSVWorld::Table::rowAdded(const std::string &id) { tableSizeUpdate(); if(mJumpToAddedRecord) { int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - selectRow(mProxyModel->getModelIndex(id, idColumn).row()); + int end = mProxyModel->getModelIndex(id, idColumn).row(); + selectRow(end); + + // without this delay the scroll works but goes to top for add/clone + QMetaObject::invokeMethod(this, "queuedScrollTo", Qt::QueuedConnection, Q_ARG(int, end)); if(mUnselectAfterJump) clearSelection(); } } + +void CSVWorld::Table::queuedScrollTo(int row) +{ + scrollTo(mProxyModel->index(row, 0), QAbstractItemView::PositionAtCenter); +} + +void CSVWorld::Table::dataChangedEvent(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + tableSizeUpdate(); + + if (mAutoJump) + { + selectRow(bottomRight.row()); + scrollTo(bottomRight, QAbstractItemView::PositionAtCenter); + } +} + +void CSVWorld::Table::jumpAfterModChanged(int state) +{ + if(state == Qt::Checked) + mAutoJump = true; + else + mAutoJump = false; +} diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 61dd57c061..caae3165cf 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -4,8 +4,6 @@ #include #include -#include - #include "../../model/filter/node.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" @@ -74,6 +72,7 @@ namespace CSVWorld std::map mDoubleClickActions; bool mJumpToAddedRecord; bool mUnselectAfterJump; + bool mAutoJump; private: @@ -141,6 +140,8 @@ namespace CSVWorld void moveDownRecord(); + void moveRecords(QDropEvent *event); + void viewRecord(); void previewRecord(); @@ -162,6 +163,12 @@ namespace CSVWorld void recordFilterChanged (std::shared_ptr filter); void rowAdded(const std::string &id); + + void dataChangedEvent(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void jumpAfterModChanged(int state); + + void queuedScrollTo(int state); }; } diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index f6b060a8f3..1b065da49e 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -95,7 +95,7 @@ CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFacto mStatus = new QLabel; - mStatusBar = new QStatusBar; + mStatusBar = new QStatusBar(this); mStatusBar->addWidget (mStatus); diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 50d61150f3..ac5ad2fda6 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -59,9 +59,9 @@ namespace CSVWorld TableBottomBox (const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, const CSMWorld::UniversalId& id, - QWidget *parent = 0); + QWidget *parent = nullptr); - virtual ~TableBottomBox(); + ~TableBottomBox() override; bool eventFilter(QObject *object, QEvent *event) override; diff --git a/apps/opencs/view/world/tableeditidaction.hpp b/apps/opencs/view/world/tableeditidaction.hpp index f2cf0b7bd0..9fe41b0de2 100644 --- a/apps/opencs/view/world/tableeditidaction.hpp +++ b/apps/opencs/view/world/tableeditidaction.hpp @@ -19,7 +19,7 @@ namespace CSVWorld CellData getCellData(int row, int column) const; public: - TableEditIdAction(const QTableView &table, QWidget *parent = 0); + TableEditIdAction(const QTableView &table, QWidget *parent = nullptr); void setCell(int row, int column); diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp new file mode 100644 index 0000000000..9c76cfaffe --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -0,0 +1,65 @@ +#include "tableheadermouseeventhandler.hpp" +#include "dragrecordtable.hpp" + +#include +#include +#include + +namespace CSVWorld +{ + +TableHeaderMouseEventHandler::TableHeaderMouseEventHandler(DragRecordTable * parent) + : QWidget(parent) + , table(*parent) + , header(*table.horizontalHeader()) +{ + header.setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect( + &header, &QHeaderView::customContextMenuRequested, [this](const QPoint & position) { showContextMenu(position); }); + + header.viewport()->installEventFilter(this); +} + +bool TableHeaderMouseEventHandler::eventFilter(QObject * tableWatched, QEvent * event) +{ + if (event->type() == QEvent::Type::MouseButtonPress) + { + auto & clickEvent = static_cast(*event); + if ((clickEvent.button() == Qt::MiddleButton)) + { + const auto & index = table.indexAt(clickEvent.pos()); + table.setColumnHidden(index.column(), true); + clickEvent.accept(); + return true; + } + } + return false; +} + +void TableHeaderMouseEventHandler::showContextMenu(const QPoint & position) +{ + auto & menu{createContextMenu()}; + menu.popup(header.viewport()->mapToGlobal(position)); +} + +QMenu & TableHeaderMouseEventHandler::createContextMenu() +{ + auto * menu = new QMenu(this); + for (int i = 0; i < table.model()->columnCount(); ++i) + { + const auto & name = table.model()->headerData(i, Qt::Horizontal, Qt::DisplayRole); + QAction * action{new QAction(name.toString(), this)}; + action->setCheckable(true); + action->setChecked(!table.isColumnHidden(i)); + menu->addAction(action); + + connect(action, &QAction::triggered, [this, &action, &i]() { + table.setColumnHidden(i, !action->isChecked()); + action->setChecked(!action->isChecked()); + action->toggle(); + }); + } + return *menu; +} + +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.hpp b/apps/opencs/view/world/tableheadermouseeventhandler.hpp new file mode 100644 index 0000000000..381e127624 --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace CSVWorld +{ +class DragRecordTable; + +class TableHeaderMouseEventHandler : public QWidget +{ +public: + explicit TableHeaderMouseEventHandler(DragRecordTable * parent); + + void showContextMenu(const QPoint &); + +private: + DragRecordTable & table; + QHeaderView & header; + + QMenu & createContextMenu(); + bool eventFilter(QObject *, QEvent *) override; + +}; // class TableHeaderMouseEventHandler +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 5413f87a6e..6b4f12738e 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -1,5 +1,8 @@ #include "tablesubview.hpp" +#include +#include +#include #include #include #include @@ -18,7 +21,7 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) -: SubView (id) +: SubView (id), mShowOptions(false), mOptions(0) { QVBoxLayout *layout = new QVBoxLayout; @@ -30,7 +33,37 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D mFilterBox = new CSVFilter::FilterBox (document.getData(), this); - layout->insertWidget (0, mFilterBox); + QHBoxLayout *hLayout = new QHBoxLayout; + hLayout->insertWidget(0,mFilterBox); + + mOptions = new QWidget; + + QHBoxLayout *optHLayout = new QHBoxLayout; + QCheckBox *autoJump = new QCheckBox("Auto Jump"); + autoJump->setToolTip ("Whether to jump to the modified record." + "\nCan be useful in finding the moved or modified" + "\nobject instance while 3D editing."); + autoJump->setCheckState(Qt::Unchecked); + connect(autoJump, SIGNAL (stateChanged(int)), mTable, SLOT (jumpAfterModChanged(int))); + optHLayout->insertWidget(0, autoJump); + optHLayout->setContentsMargins (QMargins (0, 3, 0, 0)); + mOptions->setLayout(optHLayout); + mOptions->resize(mOptions->width(), mFilterBox->height()); + mOptions->hide(); + + QPushButton *opt = new QPushButton (); + opt->setIcon (QIcon (":startup/configure")); + opt->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + opt->setToolTip ("Open additional options for this subview."); + connect (opt, SIGNAL (clicked()), this, SLOT (toggleOptions())); + + QVBoxLayout *buttonLayout = new QVBoxLayout; // work around margin issues + buttonLayout->setContentsMargins (QMargins (0/*left*/, 3/*top*/, 3/*right*/, 0/*bottom*/)); + buttonLayout->insertWidget(0, opt, 0, Qt::AlignVCenter|Qt::AlignRight); + hLayout->insertWidget(1, mOptions); + hLayout->insertLayout(2, buttonLayout); + + layout->insertLayout (0, hLayout); CSVDoc::SizeHintWidget *widget = new CSVDoc::SizeHintWidget; @@ -166,6 +199,20 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) return false; } +void CSVWorld::TableSubView::toggleOptions() +{ + if (mShowOptions) + { + mShowOptions = false; + mOptions->hide(); + } + else + { + mShowOptions = true; + mOptions->show(); + } +} + void CSVWorld::TableSubView::requestFocus (const std::string& id) { mTable->requestFocus(id); diff --git a/apps/opencs/view/world/tablesubview.hpp b/apps/opencs/view/world/tablesubview.hpp index 337d2c7621..97d2bf15f2 100644 --- a/apps/opencs/view/world/tablesubview.hpp +++ b/apps/opencs/view/world/tablesubview.hpp @@ -3,9 +3,8 @@ #include "../doc/subview.hpp" -#include - class QModelIndex; +class QWidget; namespace CSMWorld { @@ -35,6 +34,8 @@ namespace CSVWorld Table *mTable; TableBottomBox *mBottom; CSVFilter::FilterBox *mFilterBox; + bool mShowOptions; + QWidget *mOptions; public: @@ -60,6 +61,7 @@ namespace CSVWorld void cloneRequest (const CSMWorld::UniversalId& toClone); void createFilterRequest(std::vector< CSMWorld::UniversalId >& types, Qt::DropAction action); + void toggleOptions (); public slots: diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index 5a45033622..14033f20fb 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -4,13 +4,10 @@ #include #include -#include #include #include -#include #include #include -#include #include #include "../../model/world/commands.hpp" @@ -57,7 +54,7 @@ QVariant CSVWorld::NastyTableModelHack::getData() const CSVWorld::CommandDelegateFactory::~CommandDelegateFactory() {} -CSVWorld::CommandDelegateFactoryCollection *CSVWorld::CommandDelegateFactoryCollection::sThis = 0; +CSVWorld::CommandDelegateFactoryCollection *CSVWorld::CommandDelegateFactoryCollection::sThis = nullptr; CSVWorld::CommandDelegateFactoryCollection::CommandDelegateFactoryCollection() { @@ -69,7 +66,7 @@ CSVWorld::CommandDelegateFactoryCollection::CommandDelegateFactoryCollection() CSVWorld::CommandDelegateFactoryCollection::~CommandDelegateFactoryCollection() { - sThis = 0; + sThis = nullptr; for (std::map::iterator iter ( mFactories.begin()); @@ -193,7 +190,7 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { - return 0; + return nullptr; } } @@ -261,16 +258,10 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return dsb; } + /// \todo implement size limit. QPlainTextEdit does not support a size limit. case CSMWorld::ColumnBase::Display_LongString: - { - QPlainTextEdit *edit = new QPlainTextEdit(parent); - edit->setUndoRedoEnabled (false); - return edit; - } - case CSMWorld::ColumnBase::Display_LongString256: { - /// \todo implement size limit. QPlainTextEdit does not support a size limit. QPlainTextEdit *edit = new QPlainTextEdit(parent); edit->setUndoRedoEnabled (false); return edit; @@ -297,6 +288,14 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return widget; } + case CSMWorld::ColumnBase::Display_String64: + { + // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used + CSVWidget::DropLineEdit *widget = new CSVWidget::DropLineEdit(display, parent); + widget->setMaxLength (64); + return widget; + } + default: return QStyledItemDelegate::createEditor (parent, option, index); @@ -362,7 +361,7 @@ void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelInde if (!n.isEmpty()) { if (!variant.isValid()) - variant = QVariant(editor->property(n).userType(), (const void *)0); + variant = QVariant(editor->property(n).userType(), (const void *)nullptr); editor->setProperty(n, variant); } diff --git a/apps/opencs/view/world/vartypedelegate.cpp b/apps/opencs/view/world/vartypedelegate.cpp index 48fb4ab874..80c96afce4 100644 --- a/apps/opencs/view/world/vartypedelegate.cpp +++ b/apps/opencs/view/world/vartypedelegate.cpp @@ -1,7 +1,5 @@ #include "vartypedelegate.hpp" -#include - #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commandmacro.hpp" diff --git a/apps/opencs/view/world/vartypedelegate.hpp b/apps/opencs/view/world/vartypedelegate.hpp index 44705e80ec..5b0daec904 100644 --- a/apps/opencs/view/world/vartypedelegate.hpp +++ b/apps/opencs/view/world/vartypedelegate.hpp @@ -1,7 +1,7 @@ #ifndef CSV_WORLD_VARTYPEDELEGATE_H #define CSV_WORLD_VARTYPEDELEGATE_H -#include +#include #include "enumdelegate.hpp" diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index d943c7836b..75d051c63b 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -2,6 +2,7 @@ set(GAME main.cpp engine.cpp + options.cpp ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest @@ -18,15 +19,16 @@ set(GAME_HEADER source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky npcanimation vismask - creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation - bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation - renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation 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 hdr pingpongcanvas transparentpass navmeshmode ) add_openmw_dir (mwinput actions actionmanager bindingsmanager controllermanager controlswitch - inputmanagerimp mousemanager keyboardmanager sdlmappings sensormanager + inputmanagerimp mousemanager keyboardmanager sensormanager gyromanager ) add_openmw_dir (mwgui @@ -42,6 +44,7 @@ add_openmw_dir (mwgui tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview draganddrop timeadvancer jailscreen itemchargeview keyboardnavigation textcolours statswatcher + postprocessorhud ) add_openmw_dir (mwdialogue @@ -55,6 +58,13 @@ add_openmw_dir (mwscript animationextensions transformationextensions consoleextensions userextensions ) +add_openmw_dir (mwlua + luamanagerimp object worldview userdataserializer eventqueue + luabindings localscripts playerscripts objectbindings cellbindings asyncbindings + camerabindings uibindings inputbindings nearbybindings postprocessingbindings stats debugbindings + types/types types/door types/actor types/container types/weapon types/npc types/creature types/activator types/book types/lockpick types/probe types/apparatus types/potion types/misc types/repair + ) + 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 @@ -65,15 +75,15 @@ add_openmw_dir (mwworld containerstore actiontalk actiontake manualref player cellvisitors failedaction cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat - store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor - contentloader esmloader actiontrap cellreflist cellref physicssystem weather projectilemanager - cellpreloader datetimemanager + store esmstore fallback actionrepair actionsoulgem livecellref actiondoor + contentloader esmloader actiontrap cellreflist cellref weather projectilemanager + cellpreloader datetimemanager groundcoverstore magiceffects ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback - contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver - closestnotmeconvexresultcallback raycasting mtphysics + contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile + actorconvexcallback raycasting mtphysics contacttestwrapper projectileconvexcallback ) add_openmw_dir (mwclass @@ -85,9 +95,9 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat creaturestats magiceffects movement actorutil spelllist 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 actor summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects - spellabsorption linkedeffects + disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction summoning + character actors objects aistate trading weaponpriority spellpriority weapontype spellutil + spelleffects ) add_openmw_dir (mwstate @@ -122,15 +132,19 @@ include_directories( ) target_link_libraries(openmw - ${OSG_LIBRARIES} + # 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`. + # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGPARTICLE_LIBRARIES} - ${OSGUTIL_LIBRARIES} - ${OSGDB_LIBRARIES} ${OSGVIEWER_LIBRARIES} ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} + ${OSGDB_LIBRARIES} + ${OSGUTIL_LIBRARIES} + ${OSG_LIBRARIES} + ${Boost_SYSTEM_LIBRARY} - ${Boost_THREAD_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${OPENAL_LIBRARY} @@ -143,29 +157,35 @@ target_link_libraries(openmw components ) -if (ANDROID) - set (OSG_PLUGINS - -Wl,--whole-archive - ) - foreach(PLUGIN_NAME ${USED_OSG_PLUGINS}) - set(OSG_PLUGINS ${OSG_PLUGINS} ${OSG_PLUGINS_DIR}/lib${PLUGIN_NAME}.a) - endforeach() +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw PRIVATE + + - set (OSG_PLUGINS - ${OSG_PLUGINS} -Wl,--no-whole-archive - ) + + + + + + + + + + - target_link_libraries(openmw - EGL - android - log - dl - z - ${OPENSCENEGRAPH_LIBRARIES} - freetype - jpeg - png + + + + + + + + ) +endif() + +if (ANDROID) + target_link_libraries(openmw EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) @@ -184,12 +204,11 @@ endif() if(APPLE) set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) - configure_file("${OpenMW_BINARY_DIR}/settings-default.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) + configure_file("${OpenMW_BINARY_DIR}/defaults.bin" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY) @@ -213,13 +232,6 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw gcov) endif() -if (MSVC) - # Debug version needs increased number of sections beyond 2^16 - if (CMAKE_CL_64) - set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") - endif (CMAKE_CL_64) -endif (MSVC) - if (WIN32) INSTALL(TARGETS openmw RUNTIME DESTINATION ".") endif (WIN32) diff --git a/apps/openmw/android_main.cpp b/apps/openmw/android_main.cpp index cc36388b0b..95365915df 100644 --- a/apps/openmw/android_main.cpp +++ b/apps/openmw/android_main.cpp @@ -1,4 +1,6 @@ +#ifndef stderr int stderr = 0; // Hack: fix linker error +#endif #include "SDL_main.h" #include diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 49a4b40590..c3ace78ac8 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -1,14 +1,15 @@ #include "engine.hpp" #include -#include #include #include +#include #include +#include + #include -#include #include #include @@ -30,18 +31,30 @@ #include +#include +#include + #include #include #include -#include +#include + +#include +#include +#include +#include + +#include #include "mwinput/inputmanagerimp.hpp" #include "mwgui/windowmanagerimp.hpp" +#include "mwlua/luamanagerimp.hpp" + #include "mwscript/scriptmanagerimp.hpp" #include "mwscript/interpretercontext.hpp" @@ -94,8 +107,10 @@ namespace Script, Mechanics, Physics, + PhysicsWorker, World, Gui, + Lua, Number, }; @@ -124,12 +139,18 @@ namespace template <> const UserStats UserStatsValue::sValue {"Phys", "physics"}; + template <> + const UserStats UserStatsValue::sValue {" -Async", "physicsworker"}; + template <> const UserStats UserStatsValue::sValue {"World", "world"}; template <> const UserStats UserStatsValue::sValue {"Gui", "gui"}; + template <> + const UserStats UserStatsValue::sValue {"Lua", "lua"}; + template struct ForEachUserStatsValue { @@ -173,6 +194,8 @@ namespace ~ScopedProfile() { + if (!mStats.collectStats("engine")) + return; const osg::Timer_t end = mTimer.tick(); const UserStats& stats = UserStatsValue::sValue; @@ -203,12 +226,66 @@ namespace profiler.addUserStatsLine(v.mLabel, textColor, barColor, v.mTaken, multiplier, average, averageInInverseSpace, v.mBegin, v.mEnd, maxValue); }); + // 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) + profiler.removeUserStatsLine(" -Async"); } + + struct ScheduleNonDialogMessageBox + { + void operator()(std::string message) const + { + MWBase::Environment::get().getWindowManager()->scheduleMessageBox(std::move(message), MWGui::ShowInDialogueMode_Never); + } + }; + + struct IgnoreString + { + void operator()(std::string) const {} + }; + + class IdentifyOpenGLOperation : public osg::GraphicsOperation + { + public: + IdentifyOpenGLOperation() : GraphicsOperation("IdentifyOpenGLOperation", false) + {} + + void operator()(osg::GraphicsContext* graphicsContext) override + { + Log(Debug::Info) << "OpenGL Vendor: " << glGetString(GL_VENDOR); + Log(Debug::Info) << "OpenGL Renderer: " << glGetString(GL_RENDERER); + Log(Debug::Info) << "OpenGL Version: " << glGetString(GL_VERSION); + glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &mMaxTextureImageUnits); + } + + int getMaxTextureImageUnits() const + { + if (mMaxTextureImageUnits == 0) + throw std::logic_error("mMaxTextureImageUnits is not initialized"); + return mMaxTextureImageUnits; + } + + private: + int mMaxTextureImageUnits = 0; + }; + + class InitializeStereoOperation final : public osg::GraphicsOperation + { + public: + InitializeStereoOperation() : GraphicsOperation("InitializeStereoOperation", false) + {} + + void operator()(osg::GraphicsContext* graphicsContext) override + { + Stereo::Manager::instance().initializeStereo(graphicsContext); + } + }; } void OMW::Engine::executeLocalScripts() { - MWWorld::LocalScripts& localScripts = mEnvironment.getWorld()->getLocalScripts(); + MWWorld::LocalScripts& localScripts = mWorld->getLocalScripts(); localScripts.startIteration(); std::pair script; @@ -216,7 +293,7 @@ void OMW::Engine::executeLocalScripts() { MWScript::InterpreterContext interpreterContext ( &script.second.getRefData().getLocals(), script.second); - mEnvironment.getScriptManager()->run (script.first, interpreterContext); + mScriptManager->run (script.first, interpreterContext); } } @@ -234,7 +311,7 @@ bool OMW::Engine::frame(float frametime) // update input { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getInputManager()->update(frametime, false); + mInputManager->update(frametime, false); } // When the window is minimized, pause the game. Currently this *has* to be here to work around a MyGUI bug. @@ -243,54 +320,58 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (!mEnvironment.getWindowManager()->isWindowVisible()) + if (!mWindowManager->isWindowVisible()) { - mEnvironment.getSoundManager()->pausePlayback(); + mSoundManager->pausePlayback(); return false; } else - mEnvironment.getSoundManager()->resumePlayback(); + mSoundManager->resumePlayback(); // sound if (mUseSound) - mEnvironment.getSoundManager()->update(frametime); + mSoundManager->update(frametime); } // Main menu opened? Then scripts are also paused. - bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu); + bool paused = mWindowManager->containsMode(MWGui::GM_MainMenu); + + // Should be called after input manager update and before any change to the game world. + // It applies to the game world queued changes from the previous frame. + mLuaManager->synchronizedUpdate(); // update game state { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getStateManager()->update (frametime); + mStateManager->update (frametime); } - bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); + bool guiActive = mWindowManager->isGuiMode(); { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { if (!paused) { - if (mEnvironment.getWorld()->getScriptsEnabled()) + if (mWorld->getScriptsEnabled()) { // local scripts executeLocalScripts(); // global scripts - mEnvironment.getScriptManager()->getGlobalScripts().run(); + mScriptManager->getGlobalScripts().run(); } - mEnvironment.getWorld()->markCellAsUnchanged(); + mWorld->markCellAsUnchanged(); } if (!guiActive) { - double hours = (frametime * mEnvironment.getWorld()->getTimeScaleFactor()) / 3600.0; - mEnvironment.getWorld()->advanceTime(hours, true); - mEnvironment.getWorld()->rechargeItems(frametime, true); + double hours = (frametime * mWorld->getTimeScaleFactor()) / 3600.0; + mWorld->advanceTime(hours, true); + mWorld->rechargeItems(frametime, true); } } } @@ -299,16 +380,16 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getMechanicsManager()->update(frametime, guiActive); + mMechanicsManager->update(frametime, guiActive); } - if (mEnvironment.getStateManager()->getState() == MWBase::StateManager::State_Running) + if (mStateManager->getState() == MWBase::StateManager::State_Running) { - MWWorld::Ptr player = mEnvironment.getWorld()->getPlayerPtr(); + MWWorld::Ptr player = mWorld->getPlayerPtr(); if(!guiActive && player.getClass().getCreatureStats(player).isDead()) - mEnvironment.getStateManager()->endGame(); + mStateManager->endGame(); } } @@ -316,9 +397,9 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getWorld()->updatePhysics(frametime, guiActive); + mWorld->updatePhysics(frametime, guiActive, frameStart, frameNumber, *stats); } } @@ -326,16 +407,16 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getWorld()->update(frametime, guiActive); + mWorld->update(frametime, guiActive); } } // update GUI { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getWindowManager()->update(frametime); + mWindowManager->update(frametime); } if (stats->collectStats("resource")) @@ -360,8 +441,10 @@ bool OMW::Engine::frame(float frametime) OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) : mWindow(nullptr) , mEncoding(ToUTF8::WINDOWS_1252) - , mEncoder(nullptr) , mScreenCaptureOperation(nullptr) + , mSelectDepthFormatOperation(new SceneUtil::SelectDepthFormatOperation()) + , mSelectColorFormatOperation(new SceneUtil::Color::SelectColorFormatOperation()) + , mStereoManager(nullptr) , mSkipMenu (false) , mUseSound (true) , mCompileAll (false) @@ -370,16 +453,13 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mScriptConsoleMode (false) , mActivationDistanceOverride(-1) , mGrab(true) - , mExportFonts(false) , mRandomSeed(0) - , mScriptContext (0) , mFSStrict (false) , mScriptBlacklistUse (true) , mNewGame (false) , mCfgMgr(configurationManager) + , mGlMaxTextureImageUnits(0) { - MWClass::registerClasses(); - SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK|SDL_INIT_SENSOR; @@ -395,9 +475,21 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::~Engine() { - mEnvironment.cleanup(); + if (mScreenCaptureOperation != nullptr) + mScreenCaptureOperation->stop(); + + mMechanicsManager = nullptr; + mDialogueManager = nullptr; + mJournal = nullptr; + mScriptManager = nullptr; + mWindowManager = nullptr; + mWorld = nullptr; + mStereoManager = nullptr; + mSoundManager = nullptr; + mInputManager = nullptr; + mStateManager = nullptr; + mLuaManager = nullptr; - delete mScriptContext; mScriptContext = nullptr; mWorkQueue = nullptr; @@ -406,7 +498,6 @@ OMW::Engine::~Engine() mResourceSystem.reset(); - delete mEncoder; mEncoder = nullptr; if (mWindow) @@ -454,62 +545,51 @@ void OMW::Engine::addContentFile(const std::string& file) mContentFiles.push_back(file); } -void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) +void OMW::Engine::addGroundcoverFile(const std::string& file) { - mSkipMenu = skipMenu; - mNewGame = newGame; + mGroundcoverFiles.emplace_back(file); } -std::string OMW::Engine::loadSettings (Settings::Manager & settings) +void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { - // Create the settings manager and load default settings file - const std::string localdefault = (mCfgMgr.getLocalPath() / "settings-default.cfg").string(); - const std::string globaldefault = (mCfgMgr.getGlobalPath() / "settings-default.cfg").string(); - - // prefer local - if (boost::filesystem::exists(localdefault)) - settings.loadDefault(localdefault); - else if (boost::filesystem::exists(globaldefault)) - settings.loadDefault(globaldefault); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"settings-default.cfg\" was properly installed."); - - // load user settings if they exist - const std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - if (boost::filesystem::exists(settingspath)) - settings.loadUser(settingspath); - - return settingspath; + mSkipMenu = skipMenu; + mNewGame = newGame; } -void OMW::Engine::createWindow(Settings::Manager& settings) +void OMW::Engine::createWindow() { - int screen = settings.getInt("screen", "Video"); - int width = settings.getInt("resolution x", "Video"); - int height = settings.getInt("resolution y", "Video"); - bool fullscreen = settings.getBool("fullscreen", "Video"); - bool windowBorder = settings.getBool("window border", "Video"); - bool vsync = settings.getBool("vsync", "Video"); - unsigned int antialiasing = std::max(0, settings.getInt("antialiasing", "Video")); + 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"); + bool vsync = Settings::Manager::getBool("vsync", "Video"); + unsigned int antialiasing = std::max(0, Settings::Manager::getInt("antialiasing", "Video")); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); - if(fullscreen) + if(windowMode == Settings::WindowMode::Fullscreen || windowMode == Settings::WindowMode::WindowedFullscreen) { pos_x = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); pos_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); } Uint32 flags = SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN|SDL_WINDOW_RESIZABLE; - if(fullscreen) + if(windowMode == Settings::WindowMode::Fullscreen) flags |= SDL_WINDOW_FULLSCREEN; + else if (windowMode == Settings::WindowMode::WindowedFullscreen) + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + + // Allows for Windows snapping features to properly work in borderless window + SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1"); + SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1"); if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, - settings.getBool("minimize on focus loss", "Video") ? "1" : "0"); + Settings::Manager::getBool("minimize on focus loss", "Video") ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); @@ -583,8 +663,8 @@ void OMW::Engine::createWindow(Settings::Manager& settings) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->green << " bit green channel."; if (traits->blue < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->blue << " bit blue channel."; - if (traits->depth < 8) - Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->red << " bits of depth precision."; + if (traits->depth < 24) + Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->depth << " bits of depth precision."; traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel } @@ -593,18 +673,33 @@ void OMW::Engine::createWindow(Settings::Manager& settings) camera->setGraphicsContext(graphicsWindow); camera->setViewport(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); + osg::ref_ptr realizeOperations = new SceneUtil::OperationSequence(false); + mViewer->setRealizeOperation(realizeOperations); + osg::ref_ptr identifyOp = new IdentifyOpenGLOperation(); + realizeOperations->add(identifyOp); + if (Debug::shouldDebugOpenGL()) - mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation()); + realizeOperations->add(new Debug::EnableGLDebugOperation()); + + realizeOperations->add(mSelectDepthFormatOperation); + realizeOperations->add(mSelectColorFormatOperation); + + if (Stereo::getStereo()) + { + realizeOperations->add(new InitializeStereoOperation()); + Stereo::setVertexBufferHint(); + } mViewer->realize(); + mGlMaxTextureImageUnits = identifyOp->getMaxTextureImageUnits(); mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); } void OMW::Engine::setWindowIcon() { - boost::filesystem::ifstream windowIconStream; - std::string windowIcon = (mResDir / "mygui" / "openmw.png").string(); + std::ifstream windowIconStream; + std::string windowIcon = (mResDir / "openmw.png").string(); windowIconStream.open(windowIcon, std::ios_base::in | std::ios_base::binary); if (windowIconStream.fail()) Log(Debug::Error) << "Error: Failed to open " << windowIcon; @@ -625,21 +720,24 @@ void OMW::Engine::setWindowIcon() } } -void OMW::Engine::prepareEngine (Settings::Manager & settings) +void OMW::Engine::prepareEngine() { - mEnvironment.setStateManager ( - new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); + mEnvironment.setStateManager(*mStateManager); - createWindow(settings); + mStereoManager = std::make_unique(mViewer); - osg::ref_ptr rootNode (new osg::Group); + osg::ref_ptr rootNode(new osg::Group); mViewer->setSceneData(rootNode); - mVFS.reset(new VFS::Manager(mFSStrict)); + createWindow(); + + mVFS = std::make_unique(mFSStrict); VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); - mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); + mResourceSystem = std::make_unique(mVFS.get()); + mResourceSystem->getSceneManager()->getShaderManager().setMaxTextureUnits(mGlMaxTextureImageUnits); mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply(false); // keep to Off for now to allow better state sharing mResourceSystem->getSceneManager()->setFilterSettings( Settings::Manager::getString("texture mag filter", "General"), @@ -647,23 +745,42 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) Settings::Manager::getString("texture mipmap", "General"), Settings::Manager::getInt("anisotropy", "General") ); + mEnvironment.setResourceSystem(*mResourceSystem); int numThreads = Settings::Manager::getInt("preload num threads", "Cells"); if (numThreads <= 0) throw std::runtime_error("Invalid setting: 'preload num threads' must be >0"); mWorkQueue = new SceneUtil::WorkQueue(numThreads); + mScreenCaptureOperation = new SceneUtil::AsyncScreenCaptureOperation( + mWorkQueue, + new SceneUtil::WriteScreenshotToFileOperation( + mCfgMgr.getScreenshotPath().string(), + Settings::Manager::getString("screenshot format", "General"), + Settings::Manager::getBool("notify on saved screenshot", "General") + ? std::function(ScheduleNonDialogMessageBox {}) + : std::function(IgnoreString {}) + ) + ); + + mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); + + mViewer->addEventHandler(mScreenCaptureHandler); + + mLuaManager = std::make_unique(mVFS.get(), (mResDir / "lua_libs").string()); + mEnvironment.setLuaManager(*mLuaManager); + // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v3.xml").string(); - bool keybinderUserExists = boost::filesystem::exists(keybinderUser); + bool keybinderUserExists = std::filesystem::exists(keybinderUser); if(!keybinderUserExists) { std::string input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); - if(boost::filesystem::exists(input2)) { - boost::filesystem::copy_file(input2, keybinderUser); - keybinderUserExists = boost::filesystem::exists(keybinderUser); + if(std::filesystem::exists(input2)) { + std::filesystem::copy_file(input2, keybinderUser); + keybinderUserExists = std::filesystem::exists(keybinderUser); Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; } } @@ -675,79 +792,97 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.txt"; std::string userGameControllerdb; - if (boost::filesystem::exists(userdefault)){ + if (std::filesystem::exists(userdefault)) userGameControllerdb = userdefault; - } - else - userGameControllerdb = ""; std::string gameControllerdb; - if (boost::filesystem::exists(localdefault)) + if (std::filesystem::exists(localdefault)) gameControllerdb = localdefault; - else if (boost::filesystem::exists(globaldefault)) + else if (std::filesystem::exists(globaldefault)) gameControllerdb = globaldefault; - else - gameControllerdb = ""; //if it doesn't exist, pass in an empty string + //else if it doesn't exist, pass in an empty string + + // gui needs our shaders path before everything else + mResourceSystem->getSceneManager()->setShaderPath((mResDir / "shaders").string()); + + osg::ref_ptr exts = osg::GLExtensions::Get(0, false); + bool shadersSupported = exts && (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; +#endif - std::string myguiResources = (mResDir / "mygui").string(); osg::ref_ptr guiRoot = new osg::Group; guiRoot->setName("GUI Root"); guiRoot->setNodeMask(MWRender::Mask_GUI); + mStereoManager->disableStereoForNode(guiRoot); rootNode->addChild(guiRoot); - MWGui::WindowManager* window = new MWGui::WindowManager(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), - mCfgMgr.getLogPath().string() + std::string("/"), myguiResources, - mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, - Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); - mEnvironment.setWindowManager (window); - MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); - mEnvironment.setInputManager (input); + mWindowManager = std::make_unique(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), + mCfgMgr.getLogPath().string() + std::string("/"), + mScriptConsoleMode, mTranslationDataStorage, mEncoding, + Version::getOpenmwVersionDescription(mResDir.string()), shadersSupported); + mEnvironment.setWindowManager(*mWindowManager); + + mInputManager = std::make_unique(mWindow, mViewer, mScreenCaptureHandler, + mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); + mEnvironment.setInputManager(*mInputManager); // Create sound system - mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound)); + mSoundManager = std::make_unique(mVFS.get(), mUseSound); + mEnvironment.setSoundManager(*mSoundManager); if (!mSkipMenu) { const std::string& logo = Fallback::Map::getString("Movies_Company_Logo"); if (!logo.empty()) - window->playVideo(logo, true); + mWindowManager->playVideo(logo, true); } // Create the world - mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), - mFileCollections, mContentFiles, mEncoder, mActivationDistanceOverride, mCellName, - mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string())); - mEnvironment.getWorld()->setupPlayer(); + mWorld = std::make_unique(mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), + mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder.get(), mActivationDistanceOverride, mCellName, + mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string()); + mWorld->setupPlayer(); + mWorld->setRandomSeed(mRandomSeed); + mEnvironment.setWorld(*mWorld); - window->setStore(mEnvironment.getWorld()->getStore()); - window->initUI(); + mWindowManager->setStore(mWorld->getStore()); + mLuaManager->initL10n(); + mWindowManager->initUI(); //Load translation data - mTranslationDataStorage.setEncoder(mEncoder); + mTranslationDataStorage.setEncoder(mEncoder.get()); for (size_t i = 0; i < mContentFiles.size(); i++) mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFiles[i]); Compiler::registerExtensions (mExtensions); // Create script system - mScriptContext = new MWScript::CompilerContext (MWScript::CompilerContext::Type_Full); + mScriptContext = std::make_unique(MWScript::CompilerContext::Type_Full); mScriptContext->setExtensions (&mExtensions); - mEnvironment.setScriptManager (new MWScript::ScriptManager (mEnvironment.getWorld()->getStore(), *mScriptContext, mWarningsMode, - mScriptBlacklistUse ? mScriptBlacklist : std::vector())); + mScriptManager = std::make_unique(mWorld->getStore(), *mScriptContext, mWarningsMode, + mScriptBlacklistUse ? mScriptBlacklist : std::vector()); + mEnvironment.setScriptManager(*mScriptManager); // Create game mechanics system - MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; - mEnvironment.setMechanicsManager (mechanics); + mMechanicsManager = std::make_unique(); + mEnvironment.setMechanicsManager(*mMechanicsManager); // Create dialog system - mEnvironment.setJournal (new MWDialogue::Journal); - mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage)); + mJournal = std::make_unique(); + mEnvironment.setJournal(*mJournal); + + mDialogueManager = std::make_unique(mExtensions, mTranslationDataStorage); + mEnvironment.setDialogueManager(*mDialogueManager); // scripts if (mCompileAll) { - std::pair result = mEnvironment.getScriptManager()->compileAll(); + std::pair result = mScriptManager->compileAll(); if (result.first) Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " scripts (" @@ -763,54 +898,98 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) << 100*static_cast (result.second)/result.first << "%)"; } + + mLuaManager->init(); + mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath().string()); } -class WriteScreenshotToFileOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation +class OMW::Engine::LuaWorker { public: - WriteScreenshotToFileOperation(const std::string& screenshotPath, const std::string& screenshotFormat) - : mScreenshotPath(screenshotPath) - , mScreenshotFormat(screenshotFormat) + explicit LuaWorker(Engine* engine) : mEngine(engine) + { + if (Settings::Manager::getInt("lua num threads", "Lua") > 0) + mThread = std::thread([this]{ threadBody(); }); + }; + + ~LuaWorker() { + if (mThread && mThread->joinable()) + { + Log(Debug::Error) << "Unexpected destruction of LuaWorker; likely there is an unhandled exception in the main thread."; + join(); + } } - void operator()(const osg::Image& image, const unsigned int context_id) override + void allowUpdate() { - // Count screenshots. - int shotCount = 0; + if (!mThread) + return; + { + std::lock_guard lk(mMutex); + mUpdateRequest = true; + } + mCV.notify_one(); + } - // Find the first unused filename with a do-while - std::ostringstream stream; - do + void finishUpdate() + { + if (mThread) { - // Reset the stream - stream.str(""); - stream.clear(); + std::unique_lock lk(mMutex); + mCV.wait(lk, [&]{ return !mUpdateRequest; }); + } + else + update(); + }; - stream << mScreenshotPath << "/screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << mScreenshotFormat; + void join() + { + if (mThread) + { + { + std::lock_guard lk(mMutex); + mJoinRequest = true; + } + mCV.notify_one(); + mThread->join(); + } + } - } while (boost::filesystem::exists(stream.str())); +private: + void update() + { + const auto& viewer = mEngine->mViewer; + const osg::Timer_t frameStart = viewer->getStartTick(); + const unsigned int frameNumber = viewer->getFrameStamp()->getFrameNumber(); + ScopedProfile profile(frameStart, frameNumber, *osg::Timer::instance(), *viewer->getViewerStats()); - boost::filesystem::ofstream outStream; - outStream.open(boost::filesystem::path(stream.str()), std::ios::binary); + mEngine->mLuaManager->update(); + } - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension(mScreenshotFormat); - if (!readerwriter) + void threadBody() + { + while (true) { - Log(Debug::Error) << "Error: Can't write screenshot, no '" << mScreenshotFormat << "' readerwriter found"; - return; - } + std::unique_lock lk(mMutex); + mCV.wait(lk, [&]{ return mUpdateRequest || mJoinRequest; }); + if (mJoinRequest) + break; - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(image, outStream); - if (!result.success()) - { - Log(Debug::Error) << "Error: Can't write screenshot: " << result.message() << " code " << result.status(); + update(); + + mUpdateRequest = false; + lk.unlock(); + mCV.notify_one(); } } -private: - std::string mScreenshotPath; - std::string mScreenshotFormat; + Engine* mEngine; + std::mutex mMutex; + std::condition_variable mCV; + bool mUpdateRequest = false; + bool mJoinRequest = false; + std::optional mThread; }; // Initialise and enter main loop. @@ -825,13 +1004,12 @@ void OMW::Engine::go() Misc::Rng::init(mRandomSeed); - // Load settings - Settings::Manager settings; - std::string settingspath; - settingspath = loadSettings (settings); + Settings::ShaderManager::get().load((mCfgMgr.getUserConfigPath() / "shaders.yaml").string()); + + MWClass::registerClasses(); // Create encoder - mEncoder = new ToUTF8::Utf8Encoder(mEncoding); + mEncoder = std::make_unique(mEncoding); // Setup viewer mViewer = new osgViewer::Viewer; @@ -842,68 +1020,69 @@ void OMW::Engine::go() mViewer->setUseConfigureAffinity(false); #endif - mScreenCaptureOperation = new WriteScreenshotToFileOperation( - mCfgMgr.getScreenshotPath().string(), - Settings::Manager::getString("screenshot format", "General")); - - mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); - - mViewer->addEventHandler(mScreenCaptureHandler); - mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); - prepareEngine (settings); + prepareEngine(); + + std::ofstream stats; + if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE")) + { + stats.open(path, std::ios_base::out); + if (stats.is_open()) + Log(Debug::Info) << "Stats will be written to: " << path; + else + Log(Debug::Warning) << "Failed to open file for stats: " << path; + } // Setup profiler - osg::ref_ptr statshandler = new Resource::Profiler; + osg::ref_ptr statshandler = new Resource::Profiler(stats.is_open(), mVFS.get()); initStatsHandler(*statshandler); mViewer->addEventHandler(statshandler); - osg::ref_ptr resourceshandler = new Resource::StatsHandler; + osg::ref_ptr resourceshandler = new Resource::StatsHandler(stats.is_open(), mVFS.get()); mViewer->addEventHandler(resourceshandler); + if (stats.is_open()) + Resource::CollectStatistics(mViewer); + // Start the game if (!mSaveGameFile.empty()) { - mEnvironment.getStateManager()->loadGame(mSaveGameFile); + mStateManager->loadGame(mSaveGameFile); } else if (!mSkipMenu) { // start in main menu - mEnvironment.getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - mEnvironment.getSoundManager()->playTitleMusic(); + mWindowManager->pushGuiMode (MWGui::GM_MainMenu); + mSoundManager->playTitleMusic(); const std::string& logo = Fallback::Map::getString("Movies_Morrowind_Logo"); if (!logo.empty()) - mEnvironment.getWindowManager()->playVideo(logo, true); + mWindowManager->playVideo(logo, true); } else { - mEnvironment.getStateManager()->newGame (!mNewGame); + mStateManager->newGame (!mNewGame); } - if (!mStartupScript.empty() && mEnvironment.getStateManager()->getState() == MWState::StateManager::State_Running) + if (!mStartupScript.empty() && mStateManager->getState() == MWState::StateManager::State_Running) { - mEnvironment.getWindowManager()->executeInConsole(mStartupScript); + mWindowManager->executeInConsole(mStartupScript); } - std::ofstream stats; - if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE")) - { - stats.open(path, std::ios_base::out); - if (!stats) - Log(Debug::Warning) << "Failed to open file for stats: " << path; - } + LuaWorker luaWorker(this); // starts a separate lua thread if "lua num threads" > 0 // Start the main rendering loop - osg::Timer frameTimer; double simulationTime = 0.0; - while (!mViewer->done() && !mEnvironment.getStateManager()->hasQuitRequest()) + Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); + const std::chrono::steady_clock::duration maxSimulationInterval(std::chrono::milliseconds(200)); + while (!mViewer->done() && !mStateManager->hasQuitRequest()) { - double dt = frameTimer.time_s(); - frameTimer.setStartTick(); - dt = std::min(dt, 0.2); + const double dt = std::chrono::duration_cast>(std::min( + frameRateLimiter.getLastFrameDuration(), + maxSimulationInterval + )).count() * mEnvironment.getWorld()->getSimulationTimeScale(); mViewer->advance(simulationTime); @@ -917,11 +1096,15 @@ void OMW::Engine::go() mViewer->eventTraversal(); mViewer->updateTraversal(); - mEnvironment.getWorld()->updateWindowManager(); + mWorld->updateWindowManager(); + + luaWorker.allowUpdate(); // if there is a separate Lua thread, it starts the update now mViewer->renderingTraversals(); - bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); + luaWorker.finishUpdate(); + + bool guiActive = mWindowManager->isGuiMode(); if (!guiActive) simulationTime += dt; } @@ -939,11 +1122,15 @@ void OMW::Engine::go() } } - mEnvironment.limitFrameRate(frameTimer.time_s()); + frameRateLimiter.limit(); } + luaWorker.join(); + // Save user settings - settings.saveUser(settingspath); + Settings::Manager::saveUser((mCfgMgr.getUserConfigPath() / "settings.cfg").string()); + Settings::ShaderManager::get().save(); + mLuaManager->savePermanentStorage(mCfgMgr.getUserConfigPath().string()); Log(Debug::Info) << "Quitting peacefully."; } @@ -998,11 +1185,6 @@ void OMW::Engine::setScriptBlacklistUse (bool use) mScriptBlacklistUse = use; } -void OMW::Engine::enableFontExport(bool exportFonts) -{ - mExportFonts = exportFonts; -} - void OMW::Engine::setSaveGameFile(const std::string &savegame) { mSaveGameFile = savegame; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 3dd1a69b27..329993a93e 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -21,6 +21,7 @@ namespace Resource namespace SceneUtil { class WorkQueue; + class AsyncScreenCaptureOperation; } namespace VFS @@ -33,9 +34,49 @@ namespace Compiler class Context; } -namespace MWScript +namespace MWLua { - class ScriptManager; + class LuaManager; +} + +namespace Stereo +{ + class Manager; +} + +namespace Files +{ + struct ConfigurationManager; +} + +namespace osgViewer +{ + class ScreenCaptureHandler; +} + +namespace SceneUtil +{ + class SelectDepthFormatOperation; + + namespace Color + { + class SelectColorFormatOperation; + } +} + +namespace MWState +{ + class StateManager; +} + +namespace MWGui +{ + class WindowManager; +} + +namespace MWInput +{ + class InputManager; } namespace MWSound @@ -48,19 +89,24 @@ namespace MWWorld class World; } -namespace MWGui +namespace MWScript { - class WindowManager; + class ScriptManager; } -namespace Files +namespace MWMechanics { - struct ConfigurationManager; + class MechanicsManager; } -namespace osgViewer +namespace MWDialogue { - class ScreenCaptureHandler; + class DialogueManager; +} + +namespace MWDialogue +{ + class Journal; } struct SDL_Window; @@ -74,17 +120,33 @@ namespace OMW std::unique_ptr mVFS; std::unique_ptr mResourceSystem; osg::ref_ptr mWorkQueue; + std::unique_ptr mWorld; + std::unique_ptr mSoundManager; + std::unique_ptr mScriptManager; + std::unique_ptr mWindowManager; + std::unique_ptr mMechanicsManager; + std::unique_ptr mDialogueManager; + std::unique_ptr mJournal; + std::unique_ptr mInputManager; + std::unique_ptr mStateManager; + std::unique_ptr mLuaManager; MWBase::Environment mEnvironment; ToUTF8::FromType mEncoding; - ToUTF8::Utf8Encoder* mEncoder; + std::unique_ptr mEncoder; Files::PathContainer mDataDirs; std::vector mArchives; boost::filesystem::path mResDir; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; - osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; + osg::ref_ptr mScreenCaptureOperation; + osg::ref_ptr mSelectDepthFormatOperation; + osg::ref_ptr mSelectColorFormatOperation; std::string mCellName; std::vector mContentFiles; + std::vector mGroundcoverFiles; + + std::unique_ptr mStereoManager; + bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -98,11 +160,10 @@ namespace OMW // Grab mouse? bool mGrab; - bool mExportFonts; unsigned int mRandomSeed; Compiler::Extensions mExtensions; - Compiler::Context *mScriptContext; + std::unique_ptr mScriptContext; Files::Collections mFileCollections; bool mFSStrict; @@ -119,13 +180,10 @@ namespace OMW bool frame (float dt); - /// Load settings from various files, returns the path to the user settings file - std::string loadSettings (Settings::Manager & settings); - /// Prepare engine for game play - void prepareEngine (Settings::Manager & settings); + void prepareEngine(); - void createWindow(Settings::Manager& settings); + void createWindow(); void setWindowIcon(); public: @@ -155,6 +213,7 @@ namespace OMW * @param file - filename (extension is required) */ void addContentFile(const std::string& file); + void addGroundcoverFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); @@ -194,8 +253,6 @@ namespace OMW void setScriptBlacklistUse (bool use); - void enableFontExport(bool exportFonts); - /// Set the save game file to load after initialising the engine. void setSaveGameFile(const std::string& savegame); @@ -203,6 +260,8 @@ namespace OMW private: Files::ConfigurationManager& mCfgMgr; + class LuaWorker; + int mGlMaxTextureImageUnits; }; } diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d3d9849010..f13d1b39b5 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -1,22 +1,28 @@ #include #include -#include #include #include #include #include +#include + +#include "mwgui/debugwindow.hpp" #include "engine.hpp" +#include "options.hpp" + +#include #if defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include +#include // makes __argc and __argv available on windows #include + +extern "C" __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x00000001; #endif +#include + #if (defined(__APPLE__) || defined(__linux) || defined(__unix) || defined(__posix)) #include #endif @@ -39,103 +45,15 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat namespace bpo = boost::program_options; typedef std::vector StringsVector; - bpo::options_description desc("Syntax: openmw \nAllowed options"); - - desc.add_options() - ("help", "print help message") - ("version", "print version information and quit") - ("data", bpo::value()->default_value(Files::EscapePathContainer(), "data") - ->multitoken()->composing(), "set data directories (later directories have higher priority)") - - ("data-local", bpo::value()->default_value(Files::EscapePath(), ""), - "set local data directory (highest priority)") - - ("fallback-archive", bpo::value()->default_value(Files::EscapeStringVector(), "fallback-archive") - ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") - - ("resources", bpo::value()->default_value(Files::EscapePath(), "resources"), - "set resources directory") - - ("start", bpo::value()->default_value(""), - "set initial cell") - - ("content", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") - - ("no-sound", bpo::value()->implicit_value(true) - ->default_value(false), "disable all sounds") - - ("script-all", bpo::value()->implicit_value(true) - ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") - - ("script-all-dialogue", bpo::value()->implicit_value(true) - ->default_value(false), "compile all dialogue scripts at startup") - - ("script-console", bpo::value()->implicit_value(true) - ->default_value(false), "enable console-only script functionality") - - ("script-run", bpo::value()->default_value(""), - "select a file containing a list of console commands that is executed on startup") - - ("script-warn", bpo::value()->implicit_value (1) - ->default_value (1), - "handling of warnings when compiling scripts\n" - "\t0 - ignore warning\n" - "\t1 - show warning but consider script as correctly compiled anyway\n" - "\t2 - treat warnings as errors") - - ("script-blacklist", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") - - ("script-blacklist-use", bpo::value()->implicit_value(true) - ->default_value(true), "enable script blacklisting") - - ("load-savegame", bpo::value()->default_value(Files::EscapePath(), ""), - "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") - - ("skip-menu", bpo::value()->implicit_value(true) - ->default_value(false), "skip main menu on game startup") - - ("new-game", bpo::value()->implicit_value(true) - ->default_value(false), "run new game sequence (ignored if skip-menu=0)") - - ("fs-strict", bpo::value()->implicit_value(true) - ->default_value(false), "strict file system handling (no case folding)") - - ("encoding", bpo::value()-> - default_value("win1252"), - "Character encoding used in OpenMW game messages:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - - ("fallback", bpo::value()->default_value(FallbackMap(), "") - ->multitoken()->composing(), "fallback values") - - ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") - - ("export-fonts", bpo::value()->implicit_value(true) - ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") - - ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") - - ("random-seed", bpo::value () - ->default_value(Misc::Rng::generateDefaultSeed()), - "seed value for random number generator") - ; - - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) - .options(desc).allow_unregistered().run(); - + bpo::options_description desc = OpenMW::makeOptionsDescription(); bpo::variables_map variables; - // Runtime options override settings from all configs - bpo::store(valid_opts, variables); + Files::parseArgs(argc, argv, variables, desc); bpo::notify(variables); if (variables.count ("help")) { - std::cout << desc << std::endl; + getRawStdout() << desc << std::endl; return false; } @@ -143,62 +61,83 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat { cfgMgr.readConfiguration(variables, desc, true); - Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); - std::cout << v.describe() << std::endl; + Version::Version v = Version::getOpenmwVersion(variables["resources"].as().string()); + getRawStdout() << v.describe() << std::endl; return false; } - bpo::variables_map composingVariables = cfgMgr.separateComposingVariables(variables, desc); cfgMgr.readConfiguration(variables, desc); - cfgMgr.mergeComposingVariables(variables, composingVariables, desc); + Settings::Manager::load(cfgMgr); + + setupLogging(cfgMgr.getLogPath().string(), "OpenMW"); + MWGui::DebugWindow::startLogRecording(); - Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); - std::cout << v.describe() << std::endl; + Version::Version v = Version::getOpenmwVersion(variables["resources"].as().string()); + Log(Debug::Info) << v.describe(); engine.setGrabMouse(!variables["no-grab"].as()); // Font encoding settings - std::string encoding(variables["encoding"].as().toStdString()); - std::cout << ToUTF8::encodingUsingMessage(encoding) << std::endl; + std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); engine.setEncoding(ToUTF8::calculateEncoding(encoding)); // directory settings engine.enableFSStrict(variables["fs-strict"].as()); - Files::PathContainer dataDirs(Files::EscapePath::toPathContainer(variables["data"].as())); + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); - Files::PathContainer::value_type local(variables["data-local"].as().mPath); + Files::PathContainer::value_type local(variables["data-local"].as()); if (!local.empty()) dataDirs.push_back(local); - cfgMgr.processPaths(dataDirs); + cfgMgr.filterOutNonExistingPaths(dataDirs); - engine.setResourceDir(variables["resources"].as().mPath); + engine.setResourceDir(variables["resources"].as()); engine.setDataDirs(dataDirs); // fallback archives - StringsVector archives = variables["fallback-archive"].as().toStdStringVector(); + StringsVector archives = variables["fallback-archive"].as(); for (StringsVector::const_iterator it = archives.begin(); it != archives.end(); ++it) { engine.addArchive(*it); } - StringsVector content = variables["content"].as().toStdStringVector(); + StringsVector content = variables["content"].as(); if (content.empty()) { Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; return false; } + std::set contentDedupe; + for (const auto& contentFile : content) + { + if (!contentDedupe.insert(contentFile).second) + { + Log(Debug::Error) << "Content file specified more than once: " << contentFile << ". Aborting..."; + return false; + } + } - StringsVector::const_iterator it(content.begin()); - StringsVector::const_iterator end(content.end()); - for (; it != end; ++it) + for (auto& file : content) { - engine.addContentFile(*it); + engine.addContentFile(file); + } + + StringsVector groundcover = variables["groundcover"].as(); + for (auto& file : groundcover) + { + engine.addGroundcoverFile(file); + } + + if (variables.count("lua-scripts")) + { + Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. " + "Please update them to a version which uses the new omwscripts format."; } // startup-settings - engine.setCell(variables["start"].as().toStdString()); + engine.setCell(variables["start"].as()); engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); if (!variables["skip-menu"].as() && variables["new-game"].as()) Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it"; @@ -207,33 +146,82 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat engine.setCompileAll(variables["script-all"].as()); engine.setCompileAllDialogue(variables["script-all-dialogue"].as()); engine.setScriptConsoleMode (variables["script-console"].as()); - engine.setStartupScript (variables["script-run"].as().toStdString()); + engine.setStartupScript (variables["script-run"].as()); engine.setWarningsMode (variables["script-warn"].as()); - engine.setScriptBlacklist (variables["script-blacklist"].as().toStdStringVector()); + engine.setScriptBlacklist (variables["script-blacklist"].as()); engine.setScriptBlacklistUse (variables["script-blacklist-use"].as()); - engine.setSaveGameFile (variables["load-savegame"].as().mPath.string()); + engine.setSaveGameFile (variables["load-savegame"].as().string()); // other settings Fallback::Map::init(variables["fallback"].as().mMap); engine.setSoundUsage(!variables["no-sound"].as()); engine.setActivationDistanceOverride (variables["activate-dist"].as()); - engine.enableFontExport(variables["export-fonts"].as()); engine.setRandomSeed(variables["random-seed"].as()); return true; } +namespace +{ + class OSGLogHandler : public osg::NotifyHandler + { + void notify(osg::NotifySeverity severity, const char* msg) override + { + // Copy, because osg logging is not thread safe. + std::string msgCopy(msg); + if (msgCopy.empty()) + return; + + Debug::Level level; + switch (severity) + { + case osg::ALWAYS: + case osg::FATAL: + level = Debug::Error; + break; + case osg::WARN: + case osg::NOTICE: + level = Debug::Warning; + break; + case osg::INFO: + level = Debug::Info; + break; + case osg::DEBUG_INFO: + case osg::DEBUG_FP: + default: + level = Debug::Debug; + } + std::string_view s(msgCopy); + if (s.size() < 1024) + Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s); + else + { + while (!s.empty()) + { + size_t lineSize = 1; + while (lineSize < s.size() && s[lineSize - 1] != '\n') + lineSize++; + Log(level) << s.substr(0, s[lineSize - 1] == '\n' ? lineSize - 1 : lineSize); + s = s.substr(lineSize); + } + } + } + }; +} + int runApplication(int argc, char *argv[]) { + Platform::init(); + #ifdef __APPLE__ - boost::filesystem::path binary_path = boost::filesystem::system_complete(boost::filesystem::path(argv[0])); - boost::filesystem::current_path(binary_path.parent_path()); + 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 + osg::setNotifyHandler(new OSGLogHandler()); Files::ConfigurationManager cfgMgr; - std::unique_ptr engine; - engine.reset(new OMW::Engine(cfgMgr)); + std::unique_ptr engine = std::make_unique(cfgMgr); if (parseOptions(argc, argv, *engine, cfgMgr)) { @@ -249,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 wrapApplication(&runApplication, argc, argv, "OpenMW", false); } // 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 6103921e02..94543ed955 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -2,10 +2,11 @@ #define GAME_MWBASE_DIALOGUEMANAGER_H #include +#include #include #include -#include +#include namespace Loading { @@ -55,9 +56,9 @@ namespace MWBase virtual bool inJournal (const std::string& topicId, const std::string& infoId) = 0; - virtual void addTopic (const std::string& topic) = 0; + virtual void addTopic(std::string_view topic) = 0; - virtual void addChoice (const std::string& text,int choice) = 0; + virtual void addChoice(std::string_view text,int choice) = 0; virtual const std::vector >& getChoices() = 0; virtual bool isGoodbye() = 0; @@ -94,7 +95,6 @@ namespace MWBase virtual bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; virtual void persuade (int type, ResponseCallback* callback) = 0; - virtual int getTemporaryDispositionChange () const = 0; /// @note Controlled by an option, gets discarded when dialogue ends by default virtual void applyBarterDispositionChange (int delta) = 0; @@ -106,12 +106,12 @@ namespace MWBase virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; /// Changes faction1's opinion of faction2 by \a diff. - virtual void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) = 0; + virtual void modFactionReaction (std::string_view faction1, std::string_view faction2, int diff) = 0; - virtual void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) = 0; + virtual void setFactionReaction (std::string_view faction1, std::string_view faction2, int absolute) = 0; /// @return faction1's opinion of faction2 - virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const = 0; + virtual int getFactionReaction (std::string_view faction1, std::string_view faction2) 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/environment.cpp b/apps/openmw/mwbase/environment.cpp index 764a07ec96..927db90288 100644 --- a/apps/openmw/mwbase/environment.cpp +++ b/apps/openmw/mwbase/environment.cpp @@ -1,8 +1,8 @@ #include "environment.hpp" #include -#include -#include + +#include #include "world.hpp" #include "scriptmanager.hpp" @@ -13,190 +13,19 @@ #include "inputmanager.hpp" #include "windowmanager.hpp" #include "statemanager.hpp" +#include "luamanager.hpp" -MWBase::Environment *MWBase::Environment::sThis = 0; +MWBase::Environment *MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() -: mWorld (0), mSoundManager (0), mScriptManager (0), mWindowManager (0), - mMechanicsManager (0), mDialogueManager (0), mJournal (0), mInputManager (0), mStateManager (0), - mFrameDuration (0), mFrameRateLimit(0.f) { - assert (!sThis); + assert(sThis == nullptr); sThis = this; } MWBase::Environment::~Environment() { - cleanup(); - sThis = 0; -} - -void MWBase::Environment::setWorld (World *world) -{ - mWorld = world; -} - -void MWBase::Environment::setSoundManager (SoundManager *soundManager) -{ - mSoundManager = soundManager; -} - -void MWBase::Environment::setScriptManager (ScriptManager *scriptManager) -{ - mScriptManager = scriptManager; -} - -void MWBase::Environment::setWindowManager (WindowManager *windowManager) -{ - mWindowManager = windowManager; -} - -void MWBase::Environment::setMechanicsManager (MechanicsManager *mechanicsManager) -{ - mMechanicsManager = mechanicsManager; -} - -void MWBase::Environment::setDialogueManager (DialogueManager *dialogueManager) -{ - mDialogueManager = dialogueManager; -} - -void MWBase::Environment::setJournal (Journal *journal) -{ - mJournal = journal; -} - -void MWBase::Environment::setInputManager (InputManager *inputManager) -{ - mInputManager = inputManager; -} - -void MWBase::Environment::setStateManager (StateManager *stateManager) -{ - mStateManager = stateManager; -} - -void MWBase::Environment::setFrameDuration (float duration) -{ - mFrameDuration = duration; -} - -void MWBase::Environment::setFrameRateLimit(float limit) -{ - mFrameRateLimit = limit; -} - -float MWBase::Environment::getFrameRateLimit() const -{ - return mFrameRateLimit; -} - -void MWBase::Environment::limitFrameRate(double dt) const -{ - if (mFrameRateLimit > 0.f) - { - double thisFrameTime = dt; - double minFrameTime = 1.0 / static_cast(mFrameRateLimit); - if (thisFrameTime < minFrameTime) - { - std::this_thread::sleep_for(std::chrono::duration(minFrameTime - thisFrameTime)); - } - } -} - -MWBase::World *MWBase::Environment::getWorld() const -{ - assert (mWorld); - return mWorld; -} - -MWBase::SoundManager *MWBase::Environment::getSoundManager() const -{ - assert (mSoundManager); - return mSoundManager; -} - -MWBase::ScriptManager *MWBase::Environment::getScriptManager() const -{ - assert (mScriptManager); - return mScriptManager; -} - -MWBase::WindowManager *MWBase::Environment::getWindowManager() const -{ - assert (mWindowManager); - return mWindowManager; -} - -MWBase::MechanicsManager *MWBase::Environment::getMechanicsManager() const -{ - assert (mMechanicsManager); - return mMechanicsManager; -} - -MWBase::DialogueManager *MWBase::Environment::getDialogueManager() const -{ - assert (mDialogueManager); - return mDialogueManager; -} - -MWBase::Journal *MWBase::Environment::getJournal() const -{ - assert (mJournal); - return mJournal; -} - -MWBase::InputManager *MWBase::Environment::getInputManager() const -{ - assert (mInputManager); - return mInputManager; -} - -MWBase::StateManager *MWBase::Environment::getStateManager() const -{ - assert (mStateManager); - return mStateManager; -} - -float MWBase::Environment::getFrameDuration() const -{ - return mFrameDuration; -} - -void MWBase::Environment::cleanup() -{ - delete mMechanicsManager; - mMechanicsManager = 0; - - delete mDialogueManager; - mDialogueManager = 0; - - delete mJournal; - mJournal = 0; - - delete mScriptManager; - mScriptManager = 0; - - delete mWindowManager; - mWindowManager = 0; - - delete mWorld; - mWorld = 0; - - delete mSoundManager; - mSoundManager = 0; - - delete mInputManager; - mInputManager = 0; - - delete mStateManager; - mStateManager = 0; -} - -const MWBase::Environment& MWBase::Environment::get() -{ - assert (sThis); - return *sThis; + sThis = nullptr; } void MWBase::Environment::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index 80e6a6243c..0c5d39425b 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -1,11 +1,20 @@ #ifndef GAME_BASE_ENVIRONMENT_H #define GAME_BASE_ENVIRONMENT_H +#include + +#include + namespace osg { class Stats; } +namespace Resource +{ + class ResourceSystem; +} + namespace MWBase { class World; @@ -17,34 +26,29 @@ namespace MWBase class InputManager; class WindowManager; class StateManager; + class LuaManager; /// \brief Central hub for mw-subsystems /// /// This class allows each mw-subsystem to access any others subsystem's top-level manager class. /// - /// \attention Environment takes ownership of the manager class instances it is handed over in - /// the set* functions. class Environment { static Environment *sThis; - World *mWorld; - SoundManager *mSoundManager; - ScriptManager *mScriptManager; - WindowManager *mWindowManager; - MechanicsManager *mMechanicsManager; - DialogueManager *mDialogueManager; - Journal *mJournal; - InputManager *mInputManager; - StateManager *mStateManager; - float mFrameDuration; - float mFrameRateLimit; - - Environment (const Environment&); - ///< not implemented - - Environment& operator= (const Environment&); - ///< not implemented + World* mWorld = nullptr; + SoundManager* mSoundManager = nullptr; + ScriptManager* mScriptManager = nullptr; + WindowManager* mWindowManager = nullptr; + MechanicsManager* mMechanicsManager = nullptr; + DialogueManager* mDialogueManager = nullptr; + Journal* mJournal = nullptr; + InputManager* mInputManager = nullptr; + StateManager* mStateManager = nullptr; + LuaManager* mLuaManager = nullptr; + Resource::ResourceSystem* mResourceSystem = nullptr; + float mFrameRateLimit = 0; + float mFrameDuration = 0; public: @@ -52,56 +56,68 @@ namespace MWBase ~Environment(); - void setWorld (World *world); + Environment(const Environment&) = delete; + + Environment& operator=(const Environment&) = delete; + + void setWorld(World& value) { mWorld = &value; } + + void setSoundManager(SoundManager& value) { mSoundManager = &value; } + + void setScriptManager(ScriptManager& value) { mScriptManager = &value; } + + void setWindowManager(WindowManager& value) { mWindowManager = &value; } + + void setMechanicsManager(MechanicsManager& value) { mMechanicsManager = &value; } - void setSoundManager (SoundManager *soundManager); + void setDialogueManager(DialogueManager& value) { mDialogueManager = &value; } - void setScriptManager (MWBase::ScriptManager *scriptManager); + void setJournal(Journal& value) { mJournal = &value; } - void setWindowManager (WindowManager *windowManager); + void setInputManager(InputManager& value) { mInputManager = &value; } - void setMechanicsManager (MechanicsManager *mechanicsManager); + void setStateManager(StateManager& value) { mStateManager = &value; } - void setDialogueManager (DialogueManager *dialogueManager); + void setLuaManager(LuaManager& value) { mLuaManager = &value; } - void setJournal (Journal *journal); + void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; } - void setInputManager (InputManager *inputManager); + Misc::NotNullPtr getWorld() const { return mWorld; } - void setStateManager (StateManager *stateManager); + Misc::NotNullPtr getSoundManager() const { return mSoundManager; } - void setFrameDuration (float duration); - ///< Set length of current frame in seconds. + Misc::NotNullPtr getScriptManager() const { return mScriptManager; } - void setFrameRateLimit(float frameRateLimit); - float getFrameRateLimit() const; - void limitFrameRate(double dt) const; + Misc::NotNullPtr getWindowManager() const { return mWindowManager; } - World *getWorld() const; + Misc::NotNullPtr getMechanicsManager() const { return mMechanicsManager; } - SoundManager *getSoundManager() const; + Misc::NotNullPtr getDialogueManager() const { return mDialogueManager; } - ScriptManager *getScriptManager() const; + Misc::NotNullPtr getJournal() const { return mJournal; } - WindowManager *getWindowManager() const; + Misc::NotNullPtr getInputManager() const { return mInputManager; } - MechanicsManager *getMechanicsManager() const; + Misc::NotNullPtr getStateManager() const { return mStateManager; } - DialogueManager *getDialogueManager() const; + Misc::NotNullPtr getLuaManager() const { return mLuaManager; } - Journal *getJournal() const; + Misc::NotNullPtr getResourceSystem() const { return mResourceSystem; } - InputManager *getInputManager() const; + float getFrameRateLimit() const { return mFrameRateLimit; } - StateManager *getStateManager() const; + void setFrameRateLimit(float value) { mFrameRateLimit = value; } - float getFrameDuration() const; + float getFrameDuration() const { return mFrameDuration; } - void cleanup(); - ///< Delete all mw*-subsystems. + void setFrameDuration(float value) { mFrameDuration = value; } - static const Environment& get(); - ///< Return instance of this class. + /// Return instance of this class. + static const Environment& get() + { + assert(sThis != nullptr); + return *sThis; + } void reportStats(unsigned int frameNumber, osg::Stats& stats) const; }; diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 951b5053a2..e22d7f00bc 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -5,7 +5,8 @@ #include #include -#include +#include +#include namespace Loading { @@ -48,12 +49,20 @@ namespace MWBase virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; virtual void setAttemptJump(bool jumping) = 0; - virtual void toggleControlSwitch (const std::string& sw, bool value) = 0; - virtual bool getControlSwitch (const std::string& sw) = 0; + virtual void toggleControlSwitch(std::string_view sw, bool value) = 0; + virtual bool getControlSwitch(std::string_view sw) = 0; + + virtual std::string getActionDescription (int action) const = 0; + virtual std::string getActionKeyBindingName (int action) const = 0; + virtual std::string getActionControllerBindingName (int action) const = 0; + virtual bool actionIsActive(int action) const = 0; + + virtual float getActionValue(int action) const = 0; // returns value in range [0, 1] + virtual bool isControllerButtonPressed(SDL_GameControllerButton button) const = 0; + virtual float getControllerAxisValue(SDL_GameControllerAxis axis) const = 0; // returns value in range [-1, 1] + virtual int getMouseMoveX() const = 0; + virtual int getMouseMoveY() const = 0; - virtual std::string getActionDescription (int action) = 0; - virtual std::string getActionKeyBindingName (int action) = 0; - virtual std::string getActionControllerBindingName (int action) = 0; ///Actions available for binding to keyboard buttons virtual std::vector getActionKeySorting() = 0; ///Actions available for binding to controller buttons @@ -74,6 +83,7 @@ namespace MWBase virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; virtual void resetIdleTime() = 0; + virtual bool isIdle() const = 0; virtual void executeAction(int action) = 0; diff --git a/apps/openmw/mwbase/journal.hpp b/apps/openmw/mwbase/journal.hpp index cd87928903..ed07392992 100644 --- a/apps/openmw/mwbase/journal.hpp +++ b/apps/openmw/mwbase/journal.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "../mwdialogue/journalentry.hpp" #include "../mwdialogue/topic.hpp" diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp new file mode 100644 index 0000000000..64be66812b --- /dev/null +++ b/apps/openmw/mwbase/luamanager.hpp @@ -0,0 +1,102 @@ +#ifndef GAME_MWBASE_LUAMANAGER_H +#define GAME_MWBASE_LUAMANAGER_H + +#include +#include + +#include + +namespace MWWorld +{ + class Ptr; +} + +namespace Loading +{ + class Listener; +} + +namespace ESM +{ + class ESMReader; + class ESMWriter; + struct LuaScripts; +} + +namespace MWBase +{ + + class LuaManager + { + public: + virtual ~LuaManager() = default; + + virtual std::string translate(const std::string& contextName, const std::string& key) = 0; + virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; + virtual void registerObject(const MWWorld::Ptr& ptr) = 0; + virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; + virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; + virtual void objectRemovedFromScene(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; + // 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; + + struct InputEvent + { + enum { + KeyPressed, + KeyReleased, + ControllerPressed, + ControllerReleased, + Action, + TouchPressed, + TouchReleased, + TouchMoved, + } mType; + std::variant mValue; + }; + virtual void inputEvent(const InputEvent& event) = 0; + + struct ActorControls + { + bool mDisableAI = false; + bool mChanged = false; + + bool mJump = false; + bool mRun = false; + float mMovement = 0; + float mSideMovement = 0; + float mPitchChange = 0; + float mYawChange = 0; + int mUse = 0; + }; + + virtual ActorControls* getActorControls(const MWWorld::Ptr&) const = 0; + + virtual void clear() = 0; + virtual void setupPlayer(const MWWorld::Ptr&) = 0; + + // Saving + int countSavedGameRecords() const { return 1; }; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; + virtual void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) = 0; + + // Loading from a save + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; + virtual void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) = 0; + + // Should be called before loading. The map is used to fix refnums if the order of content files was changed. + virtual void setContentFileMapping(const std::map&) = 0; + + // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. + virtual void reloadAllScripts() = 0; + + virtual void handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) = 0; + }; + +} + +#endif // GAME_MWBASE_LUAMANAGER_H diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b6ce4d0619..e542883012 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -3,12 +3,11 @@ #include #include -#include #include -#include +#include +#include -#include "../mwmechanics/actorutil.hpp" -// For MWMechanics::GreetingState +#include "../mwmechanics/greetingstate.hpp" #include "../mwworld/ptr.hpp" @@ -58,7 +57,7 @@ namespace MWBase virtual void add (const MWWorld::Ptr& ptr) = 0; ///< Register an object for management - virtual void remove (const MWWorld::Ptr& ptr) = 0; + virtual void remove (const MWWorld::Ptr& ptr, bool keepActive) = 0; ///< Deregister an object for management virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; @@ -67,12 +66,6 @@ namespace MWBase virtual void drop (const MWWorld::CellStore *cellStore) = 0; ///< Deregister all objects in the given cell. - virtual void update (float duration, bool paused) = 0; - ///< Update objects - /// - /// \param paused In game type does not currently advance (this usually means some GUI - /// component is up). - virtual void setPlayerName (const std::string& name) = 0; ///< Set player name. @@ -88,7 +81,7 @@ namespace MWBase virtual void setPlayerClass (const ESM::Class& class_) = 0; ///< Set player class to custom class. - virtual void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) = 0; + virtual void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) = 0; virtual void rest(double hours, bool sleep) = 0; ///< If the player is sleeping or waiting, this should be called every hour. @@ -100,7 +93,7 @@ namespace MWBase virtual int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) = 0; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. - virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) = 0; + virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) = 0; ///< Calculate the diposition of an NPC toward the player. virtual int countDeaths (const std::string& id) const = 0; @@ -112,6 +105,9 @@ namespace MWBase /// Makes \a ptr fight \a target. Also shouts a combat taunt. virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; + /// Removes an actor and its allies from combat with the actor's targets. + virtual void stopCombat(const MWWorld::Ptr& ptr) = 0; + enum OffenseType { OT_Theft, // Taking items owned by an NPC or a faction you are not a member of @@ -156,7 +152,7 @@ namespace MWBase PT_Bribe100, PT_Bribe1000 }; - virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) = 0; + virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) = 0; ///< Perform a persuasion action on NPC virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0; @@ -196,15 +192,16 @@ namespace MWBase ///Returns the list of actors which are siding with the given actor in fights /**ie AiFollow or AiEscort is active and the target is the actor **/ - virtual std::list getActorsSidingWith(const MWWorld::Ptr& actor) = 0; - virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; - virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsSidingWith(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsFollowing(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; + virtual std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) = 0; ///Returns a list of actors who are fighting the given actor within the fAlarmDistance /** ie AiCombat is active and the target is the actor **/ - virtual std::list getActorsFighting(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsFighting(const MWWorld::Ptr& actor) = 0; - virtual std::list getEnemiesNearby(const MWWorld::Ptr& actor) = 0; + virtual std::vector getEnemiesNearby(const MWWorld::Ptr& actor) = 0; /// Recursive versions of above methods virtual void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) = 0; @@ -229,7 +226,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 std::string spellId, bool manualSpell) = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const std::string& spellId, bool manualSpell) = 0; virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; @@ -276,8 +273,6 @@ namespace MWBase virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; - - virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; }; } diff --git a/apps/openmw/mwbase/scriptmanager.hpp b/apps/openmw/mwbase/scriptmanager.hpp index ac8333ed10..c7717cfb05 100644 --- a/apps/openmw/mwbase/scriptmanager.hpp +++ b/apps/openmw/mwbase/scriptmanager.hpp @@ -10,6 +10,7 @@ namespace Interpreter namespace Compiler { + class Extensions; class Locals; } @@ -52,6 +53,8 @@ namespace MWBase ///< Return locals for script \a name. virtual MWScript::GlobalScripts& getGlobalScripts() = 0; + + virtual const Compiler::Extensions& getExtensions() const = 0; }; } diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 2bac561fdd..a60ed78154 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "../mwworld/ptr.hpp" @@ -42,6 +43,8 @@ namespace MWSound * much. */ NoPlayerLocal = 1<<3, /* (3D only) Don't play the sound local to the listener even if the * player is making it. */ + NoScaling = 1<<4, /* Don't scale audio with simulation time */ + NoEnvNoScaling = NoEnv | NoScaling, LoopNoEnv = Loop | NoEnv, LoopRemoveAtDistance = Loop | RemoveAtDistance }; @@ -71,6 +74,8 @@ namespace MWBase using PlayMode = MWSound::PlayMode; using Type = MWSound::Type; + float mSimulationTimeScale = 1.0; + public: SoundManager() {} virtual ~SoundManager() {} @@ -128,19 +133,19 @@ namespace MWBase /// returned by \ref playTrack). Only intended to be called by the track /// decoder's read method. - virtual Sound *playSound(const std::string& soundId, float volume, float pitch, + virtual Sound *playSound(std::string_view soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. - virtual Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, + virtual Sound *playSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. - virtual Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, + virtual Sound *playSound3D(const osg::Vec3f& initialPos, std::string_view soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. @@ -148,7 +153,7 @@ namespace MWBase virtual void stopSound(Sound *sound) = 0; ///< Stop the given sound from playing - virtual void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) = 0; + virtual void stopSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId) = 0; ///< Stop the given object from playing the given sound, virtual void stopSound3D(const MWWorld::ConstPtr &reference) = 0; @@ -157,13 +162,13 @@ namespace MWBase virtual void stopSound(const MWWorld::CellStore *cell) = 0; ///< Stop all sounds for the given cell. - virtual void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) = 0; + virtual void fadeOutSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId, float duration) = 0; ///< Fade out given sound (that is already playing) of given object ///< @param reference Reference to object, whose sound is faded out ///< @param soundId ID of the sound to fade out. ///< @param duration Time until volume reaches 0. - virtual bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const = 0; + virtual bool getSoundPlaying(const MWWorld::ConstPtr &reference, std::string_view soundId) const = 0; ///< Is the given sound currently playing on the given object? /// If you want to check if sound played with playSound is playing, use empty Ptr @@ -176,12 +181,13 @@ namespace MWBase virtual void pausePlayback() = 0; virtual void resumePlayback() = 0; - virtual void update(float duration) = 0; - virtual void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) = 0; virtual void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) = 0; + void setSimulationTimeScale(float scale) { mSimulationTimeScale = scale; } + float getSimulationTimeScale() const { return mSimulationTimeScale; } + virtual void clear() = 0; }; } diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 643695c37c..c18db4190d 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -53,13 +53,11 @@ namespace MWBase /// /// \param bypass Skip new game mechanics. - virtual void endGame() = 0; - virtual void resumeGame() = 0; virtual void deleteGame (const MWState::Character *character, const MWState::Slot *slot) = 0; - virtual void saveGame (const std::string& description, const MWState::Slot *slot = 0) = 0; + virtual void saveGame (const std::string& description, const MWState::Slot *slot = nullptr) = 0; ///< Write a saved game to \a slot or create a new slot if \a slot == 0. /// /// \note Slot must belong to the current character. @@ -88,8 +86,6 @@ namespace MWBase /// iterator. virtual CharacterIterator characterEnd() = 0; - - virtual void update (float duration) = 0; }; } diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 29d404777c..a65ccd6932 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWBASE_WINDOWMANAGER_H #define GAME_MWBASE_WINDOWMANAGER_H -#include +#include #include #include #include @@ -32,7 +32,6 @@ namespace MyGUI namespace ESM { - struct Class; class ESMReader; class ESMWriter; struct CellId; @@ -70,6 +69,8 @@ namespace MWGui class DialogueWindow; class WindowModal; class JailScreen; + class MessageBox; + class PostProcessorHud; enum ShowInDialogueMode { ShowInDialogueMode_IfPossible, @@ -146,6 +147,8 @@ namespace MWBase virtual MWGui::CountDialog* getCountDialog() = 0; virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; virtual MWGui::TradeWindow* getTradeWindow() = 0; + virtual const std::vector getActiveMessageBoxes() = 0; + virtual MWGui::PostProcessorHud* getPostProcessorHud() = 0; /// Make the player use an item, while updating GUI state accordingly virtual void useItem(const MWWorld::Ptr& item, bool force=false) = 0; @@ -153,6 +156,13 @@ namespace MWBase virtual void updateSpellWindow() = 0; virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; + virtual void setConsoleMode(const std::string& mode) = 0; + + static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; + static constexpr std::string_view sConsoleColor_Error = "#FF2222"; + static constexpr std::string_view sConsoleColor_Success = "#FF00FF"; + static constexpr std::string_view sConsoleColor_Info = "#AAAAAA"; + virtual void printToConsole(const std::string& msg, std::string_view color) = 0; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning @@ -172,6 +182,8 @@ namespace MWBase virtual void setDragDrop(bool dragDrop) = 0; virtual bool getWorldMouseOver() = 0; + virtual float getScalingFactor() const = 0; + virtual bool toggleFogOfWar() = 0; virtual bool toggleFullHelp() = 0; @@ -227,6 +239,8 @@ namespace MWBase virtual void exitCurrentGuiMode() = 0; virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; + /// Puts message into a queue to show on the next update. Thread safe alternative for messageBox. + virtual void scheduleMessageBox(std::string message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; virtual void staticMessageBox(const std::string& message) = 0; virtual void removeStaticMessageBox() = 0; virtual void interactiveMessageBox (const std::string& message, @@ -235,8 +249,6 @@ namespace MWBase /// returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual int readPressedButton() = 0; - virtual void update (float duration) = 0; - virtual void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) = 0; /** @@ -272,8 +284,6 @@ namespace MWBase /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. virtual void setKeyFocusWidget (MyGUI::Widget* widget) = 0; - virtual void loadUserFonts() = 0; - virtual Loading::Listener* getLoadingScreen() = 0; /// Should the cursor be visible? @@ -318,6 +328,7 @@ namespace MWBase virtual void toggleConsole() = 0; virtual void toggleDebugWindow() = 0; + virtual void togglePostProcessorHud() = 0; /// Cycle to next or previous spell virtual void cycleSpell(bool next) = 0; @@ -326,12 +337,6 @@ namespace MWBase virtual void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) = 0; - // In WindowManager for now since there isn't a VFS singleton - virtual std::string correctIconPath(const std::string& path) = 0; - virtual std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) = 0; - virtual std::string correctTexturePath(const std::string& path) = 0; - virtual bool textureExists(const std::string& path) = 0; - virtual void addCell(MWWorld::CellStore* cell) = 0; virtual void removeCell(MWWorld::CellStore* cell) = 0; virtual void writeFog(MWWorld::CellStore* cell) = 0; @@ -348,6 +353,19 @@ namespace MWBase virtual void watchActor(const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr getWatchedActor() const = 0; + + virtual const std::string& getVersionDescription() const = 0; + + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0; + virtual void forceLootMode(const MWWorld::Ptr& ptr) = 0; + + virtual void asyncPrepareSaveMap() = 0; + + /// Sets the cull masks for all applicable views + virtual void setCullMask(uint32_t mask) = 0; + + /// Same as viewer->getCamera()->getCullMask(), provided for consistency. + virtual uint32_t getCullMask() = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f9cbc89723..cffdb6dbed 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -6,9 +6,14 @@ #include #include #include +#include #include -#include +#include +#include +#include + +#include #include "../mwworld/ptr.hpp" #include "../mwworld/doorstate.hpp" @@ -54,12 +59,16 @@ namespace ESM namespace MWPhysics { + class RayCastingResult; class RayCastingInterface; } namespace MWRender { class Animation; + class Camera; + class RenderingManager; + class PostProcessor; } namespace MWMechanics @@ -70,6 +79,7 @@ namespace MWMechanics namespace DetourNavigator { struct Navigator; + struct AgentBounds; } namespace MWWorld @@ -108,6 +118,9 @@ namespace MWBase virtual ~World() {} + virtual void setRandomSeed(uint32_t seed) = 0; + ///< \param seed The seed used when starting a new game. + virtual void startNewGame (bool bypass) = 0; ///< \param bypass Bypass regular game start. @@ -127,6 +140,8 @@ namespace MWBase virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; + virtual bool isCellActive(MWWorld::CellStore* cell) const = 0; + virtual void testExteriorCells() = 0; virtual void testInteriorCells() = 0; @@ -138,16 +153,12 @@ namespace MWBase virtual bool toggleWorld() = 0; virtual bool toggleBorders() = 0; - virtual void adjustSky() = 0; - virtual MWWorld::Player& getPlayer() = 0; virtual MWWorld::Ptr getPlayerPtr() = 0; virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; virtual const MWWorld::ESMStore& getStore() const = 0; - virtual std::vector& getEsmReader() = 0; - virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual bool hasCellChanged() const = 0; @@ -163,35 +174,36 @@ namespace MWBase virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) = 0; ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void setGlobalInt (const std::string& name, int value) = 0; + virtual void setGlobalInt(std::string_view name, int value) = 0; ///< Set value independently from real type. - virtual void setGlobalFloat (const std::string& name, float value) = 0; + virtual void setGlobalFloat(std::string_view name, float value) = 0; ///< Set value independently from real type. - virtual int getGlobalInt (const std::string& name) const = 0; + virtual int getGlobalInt(std::string_view name) const = 0; ///< Get value independently from real type. - virtual float getGlobalFloat (const std::string& name) const = 0; + virtual float getGlobalFloat(std::string_view name) const = 0; ///< Get value independently from real type. - virtual char getGlobalVariableType (const std::string& name) const = 0; + virtual char getGlobalVariableType(std::string_view name) const = 0; ///< Return ' ', if there is no global variable with this name. - virtual std::string getCellName (const MWWorld::CellStore *cell = 0) const = 0; + virtual std::string getCellName (const MWWorld::CellStore *cell = nullptr) const = 0; ///< Return name of the cell. /// /// \note If cell==0, the cell the player is currently in will be used instead to /// generate a name. + virtual std::string getCellName(const ESM::Cell* cell) const = 0; virtual void removeRefScript (MWWorld::RefData *ref) = 0; //< Remove the script attached to ref from mLocalScripts - virtual MWWorld::Ptr getPtr (const std::string& name, bool activeOnly) = 0; + virtual MWWorld::Ptr getPtr (std::string_view name, bool activeOnly) = 0; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. - virtual MWWorld::Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = true) = 0; + virtual MWWorld::Ptr searchPtr (std::string_view name, bool activeOnly, bool searchInContainers = true) = 0; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. @@ -227,6 +239,10 @@ namespace MWBase virtual int getCurrentWeather() const = 0; + virtual int getNextWeather() const = 0; + + virtual float getWeatherTransition() const = 0; + virtual unsigned int getNightDayMode() const = 0; virtual int getMasserPhase() const = 0; @@ -239,6 +255,10 @@ namespace MWBase virtual float getTimeScaleFactor() const = 0; + virtual float getSimulationTimeScale() const = 0; + + virtual void setSimulationTimeScale(float scale) = 0; + virtual void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; ///< Move to interior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes @@ -260,7 +280,7 @@ namespace MWBase virtual float getDistanceToFacedObject() = 0; - virtual float getMaxActivationDistance() = 0; + 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 @@ -278,18 +298,20 @@ namespace MWBase virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; - virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z, bool moveToActive=false) = 0; + virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes - virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z, bool movePhysics=true) = 0; + virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; + ///< @return an updated Ptr + + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec) = 0; ///< @return an updated Ptr - virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; + virtual void scaleObject (const MWWorld::Ptr& ptr, float scale, bool force = false) = 0; - virtual void rotateObject(const MWWorld::Ptr& ptr, float x, float y, float z, - RotationFlags flags = RotationFlag_inverseOrder) = 0; + virtual void rotateObject(const MWWorld::Ptr& ptr, const osg::Vec3f& rot, RotationFlags flags = RotationFlag_inverseOrder) = 0; - virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) = 0; + virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) = 0; ///< Place an object. Makes a copy of the Ptr. virtual MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) = 0; @@ -300,9 +322,6 @@ namespace MWBase const = 0; ///< Convert cell numbers to position. - virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const = 0; - ///< Convert position to cell numbers - virtual void queueMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) = 0; ///< Queues movement for \a ptr (in local space), to be applied in the next call to /// doPhysics. @@ -318,6 +337,9 @@ namespace MWBase virtual bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) = 0; + virtual bool castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, + bool ignorePlayer, bool ignoreActors) = 0; + virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0; @@ -390,11 +412,6 @@ namespace MWBase ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record - virtual void update (float duration, bool paused) = 0; - virtual void updatePhysics (float duration, bool paused) = 0; - - virtual void updateWindowManager () = 0; - virtual MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) = 0; ///< copy and place an object into the gameworld at the specified cursor position /// @param object @@ -426,17 +443,17 @@ namespace MWBase virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0; + virtual MWRender::Camera* getCamera() = 0; virtual void togglePOV(bool force = false) = 0; virtual bool isFirstPerson() const = 0; virtual bool isPreviewModeEnabled() const = 0; - virtual void togglePreviewMode(bool enable) = 0; virtual bool toggleVanityMode(bool enable) = 0; - virtual void allowVanityMode(bool allow) = 0; virtual bool vanityRotateCamera(float * rot) = 0; - virtual void adjustCameraDistance(float dist) = 0; virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0; virtual void disableDeferredPreviewRotation() = 0; + virtual void saveLoaded() = 0; + virtual void setupPlayer() = 0; virtual void renderPlayer() = 0; @@ -492,7 +509,7 @@ 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, std::string settingStr) = 0; + virtual bool screenshot360 (osg::Image* image) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise @@ -514,7 +531,7 @@ namespace MWBase /// Returns true if levitation spell effect is allowed. virtual bool isLevitationEnabled() const = 0; - virtual bool getGodModeState() = 0; + virtual bool getGodModeState() const = 0; virtual bool toggleGodMode() = 0; @@ -530,11 +547,12 @@ namespace MWBase virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; - virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) = 0; virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; + virtual void updateProjectilesCasters() = 0; - virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) = 0; + virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) const = 0; virtual const std::vector& getContentFiles() const = 0; @@ -581,7 +599,7 @@ namespace MWBase virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, - const std::string& sourceName, const bool fromProjectile=false) = 0; + const std::string& sourceName, const bool fromProjectile=false, int slot = 0) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; @@ -598,7 +616,7 @@ namespace MWBase /// Return a vector aiming the actor's weapon towards a target. /// @note The length of the vector is the distance between actor and target. - virtual osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; + virtual osg::Vec3f aimToTarget(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; @@ -609,12 +627,11 @@ namespace MWBase virtual bool isPlayerInJail() const = 0; virtual void rest(double hours) = 0; - virtual void rechargeItems(double duration, bool activeOnly) = 0; virtual void setPlayerTraveling(bool traveling) = 0; virtual bool isPlayerTraveling() const = 0; - virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; + virtual void rotateWorldObject (const MWWorld::Ptr& ptr, const osg::Quat& rotate) = 0; /// Return terrain height at \a worldPos position. virtual float getTerrainHeightAt(const osg::Vec3f& worldPos) const = 0; @@ -632,22 +649,30 @@ namespace MWBase virtual DetourNavigator::Navigator* getNavigator() const = 0; virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const = 0; + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const = 0; virtual void removeActorPath(const MWWorld::ConstPtr& actor) const = 0; virtual void setNavMeshNumberToRender(const std::size_t value) = 0; - /// Return physical half extents of the given actor to be used in pathfinding - virtual osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const = 0; + virtual DetourNavigator::AgentBounds getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const = 0; virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; + virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const Misc::Span& ignore, std::vector* occupyingActors = nullptr) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual std::vector getAll(const std::string& id) = 0; + + virtual Misc::Rng::Generator& getPrng() = 0; + + virtual MWRender::RenderingManager* getRenderingManager() = 0; + + virtual MWRender::PostProcessor* getPostProcessor() = 0; + + virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0; }; } diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index c54b1c3691..4937ca3b03 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -1,6 +1,8 @@ #include "activator.hpp" -#include +#include + +#include #include #include @@ -25,9 +27,14 @@ #include "../mwmechanics/npcstats.hpp" +#include "classmodel.hpp" namespace MWClass { + Activator::Activator() + : MWWorld::RegisteredClass(ESM::Activator::sRecordId) + { + } void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -38,21 +45,19 @@ namespace MWClass } } - void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const + void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const + { + return getClassModel(ptr); } bool Activator::isActivator() const @@ -80,22 +85,11 @@ namespace MWClass return ref->mBase->mScript; } - void Activator::registerSelf() - { - std::shared_ptr instance (new Activator); - - registerClass (typeid (ESM::Activator).name(), instance); - } - bool Activator::hasToolTip (const MWWorld::ConstPtr& ptr) const { return !getName(ptr).empty(); } - bool Activator::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { - return false; - } - MWGui::ToolTipInfo Activator::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); @@ -114,19 +108,20 @@ namespace MWClass return info; } - std::shared_ptr Activator::activate(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const + std::unique_ptr Activator::activate(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfActivator"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfActivator", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; } - return std::shared_ptr(new MWWorld::NullAction); + return std::make_unique(); } @@ -142,10 +137,12 @@ namespace MWClass const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); std::string creatureId; + 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, "meshes\\" + iter.mModel)) + if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, + Misc::ResourceHelpers::correctMeshPath(iter.mModel, vfs))) { creatureId = !iter.mOriginal.empty() ? iter.mOriginal : iter.mId; break; @@ -155,6 +152,7 @@ namespace MWClass int type = getSndGenTypeFromName(name); std::vector fallbacksounds; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (!creatureId.empty()) { std::vector sounds; @@ -167,9 +165,9 @@ namespace MWClass } if (!sounds.empty()) - return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; + return sounds[Misc::Rng::rollDice(sounds.size(), prng)]->mSound; if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; } else { @@ -179,7 +177,7 @@ namespace MWClass fallbacksounds.push_back(&*sound); if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; } return std::string(); diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 10ace6f74d..e34786a1f0 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -1,23 +1,27 @@ #ifndef GAME_MWCLASS_ACTIVATOR_H #define GAME_MWCLASS_ACTIVATOR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Activator : public MWWorld::Class + class Activator final : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Activator(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; static int getSndGenTypeFromName(const std::string &name); public: - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. @@ -25,20 +29,15 @@ namespace MWClass bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; - ///< Return whether this class of object can be activated with telekinesis - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr - std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 33aeb26bb0..8b291c5882 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -1,6 +1,6 @@ #include "actor.hpp" -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -17,23 +17,16 @@ namespace MWClass { - Actor::Actor() {} - - Actor::~Actor() {} - void Actor::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - if (!model.empty()) - { - physics.addActor(ptr, model); - if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) - MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); - } + physics.addActor(ptr, model); + if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) + MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } bool Actor::useAnim() const diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 3d509b2768..fca610bc41 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -3,6 +3,11 @@ #include "../mwworld/class.hpp" +#include "../mwmechanics/magiceffects.hpp" + +#include +#include + namespace ESM { struct GameSetting; @@ -15,16 +20,25 @@ namespace MWClass { protected: - Actor(); + explicit Actor(unsigned type) : Class(type) {} + + template + float getSwimSpeedImpl(const MWWorld::Ptr& ptr, const GMST& gmst, const MWMechanics::MagicEffects& mageffects, float baseSpeed) const + { + return baseSpeed + * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) + * (gmst.fSwimRunBase->mValue.getFloat() + + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + } public: - virtual ~Actor(); + ~Actor() override = default; void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; @@ -46,8 +60,8 @@ namespace MWClass float getCurrentSpeed(const MWWorld::Ptr& ptr) const override; // not implemented - Actor(const Actor&); - Actor& operator= (const Actor&); + Actor(const Actor&) = delete; + Actor& operator= (const Actor&) = delete; }; } diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 518695fabf..39dd864f99 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -1,6 +1,8 @@ #include "apparatus.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -16,8 +18,14 @@ #include "../mwgui/tooltips.hpp" +#include "classmodel.hpp" + namespace MWClass { + Apparatus::Apparatus() + : MWWorld::RegisteredClass(ESM::Apparatus::sRecordId) + { + } void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -26,20 +34,9 @@ namespace MWClass } } - void Apparatus::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Apparatus::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Apparatus::getName (const MWWorld::ConstPtr& ptr) const @@ -50,7 +47,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Apparatus::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Apparatus::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -70,13 +67,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Apparatus::registerSelf() - { - std::shared_ptr instance (new Apparatus); - - registerClass (typeid (ESM::Apparatus).name(), instance); - } - std::string Apparatus::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Apparatus Up"); @@ -116,9 +106,9 @@ namespace MWClass return info; } - std::shared_ptr Apparatus::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Apparatus::use (const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionAlchemy(force)); + return std::make_unique(force); } MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index 8087c57ba3..ef21928075 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -1,12 +1,15 @@ #ifndef GAME_MWCLASS_APPARATUS_H #define GAME_MWCLASS_APPARATUS_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Apparatus : public MWWorld::Class + class Apparatus : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Apparatus(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; @@ -17,12 +20,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -35,8 +36,6 @@ namespace MWClass MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -46,7 +45,7 @@ namespace MWClass std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 3f9bfb859f..2fb6ecba34 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -1,8 +1,10 @@ #include "armor.hpp" -#include -#include -#include +#include + +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -24,8 +26,14 @@ #include "../mwgui/tooltips.hpp" +#include "classmodel.hpp" + namespace MWClass { + Armor::Armor() + : MWWorld::RegisteredClass(ESM::Armor::sRecordId) + { + } void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -34,20 +42,9 @@ namespace MWClass } } - void Armor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Armor::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Armor::getName (const MWWorld::ConstPtr& ptr) const @@ -58,7 +55,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Armor::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Armor::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -164,13 +161,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Armor::registerSelf() - { - std::shared_ptr instance (new Armor); - - registerClass (typeid (ESM::Armor).name(), instance); - } - std::string Armor::getUpSoundId (const MWWorld::ConstPtr& ptr) const { int es = getEquipmentSkill(ptr); @@ -213,7 +203,9 @@ namespace MWClass // get armor type string (light/medium/heavy) std::string typeText; if (ref->mBase->mData.mWeight == 0) - typeText = ""; + { + // no type + } else { int armorType = getEquipmentSkill(ptr); @@ -263,7 +255,7 @@ namespace MWClass const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Armor newItem = *ref->mBase; - newItem.mId=""; + newItem.mId.clear(); newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; @@ -327,7 +319,7 @@ namespace MWClass if(*slot == MWWorld::InventoryStore::Slot_CarriedLeft) { MWWorld::ConstContainerStoreIterator weapon = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != invStore.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + if(weapon != invStore.end() && weapon->getType() == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); if (MWMechanics::getWeaponType(ref->mBase->mData.mType)->mFlags & ESM::WeaponType::TwoHanded) @@ -340,9 +332,9 @@ namespace MWClass return std::make_pair(1,""); } - std::shared_ptr Armor::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Armor::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index 4f04e0824b..b185596c08 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_ARMOR_H #define GAME_MWCLASS_ARMOR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Armor : public MWWorld::Class + class Armor : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Armor(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -16,12 +20,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -48,8 +50,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -69,7 +69,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. \n /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/bodypart.cpp b/apps/openmw/mwclass/bodypart.cpp index 0315d3ddb0..d9438f80c1 100644 --- a/apps/openmw/mwclass/bodypart.cpp +++ b/apps/openmw/mwclass/bodypart.cpp @@ -5,8 +5,14 @@ #include "../mwworld/cellstore.hpp" +#include "classmodel.hpp" + namespace MWClass { + BodyPart::BodyPart() + : MWWorld::RegisteredClass(ESM::BodyPart::sRecordId) + { + } MWWorld::Ptr BodyPart::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { @@ -22,10 +28,6 @@ namespace MWClass } } - void BodyPart::insertObject(const MWWorld::Ptr &ptr, const std::string &model, MWPhysics::PhysicsSystem &physics) const - { - } - std::string BodyPart::getName(const MWWorld::ConstPtr &ptr) const { return std::string(); @@ -36,22 +38,9 @@ namespace MWClass return false; } - void BodyPart::registerSelf() - { - std::shared_ptr instance (new BodyPart); - - registerClass (typeid (ESM::BodyPart).name(), instance); - } - std::string BodyPart::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } } diff --git a/apps/openmw/mwclass/bodypart.hpp b/apps/openmw/mwclass/bodypart.hpp index 13d9141386..fb6c813f53 100644 --- a/apps/openmw/mwclass/bodypart.hpp +++ b/apps/openmw/mwclass/bodypart.hpp @@ -1,13 +1,17 @@ #ifndef GAME_MWCLASS_BODYPART_H #define GAME_MWCLASS_BODYPART_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class BodyPart : public MWWorld::Class + class BodyPart : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + BodyPart(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -15,16 +19,12 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; }; diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 4ea71e3ac2..75648a1180 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -1,6 +1,8 @@ #include "book.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -21,8 +23,14 @@ #include "../mwmechanics/npcstats.hpp" +#include "classmodel.hpp" + namespace MWClass { + Book::Book() + : MWWorld::RegisteredClass(ESM::Book::sRecordId) + { + } void Book::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -31,20 +39,9 @@ namespace MWClass } } - void Book::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Book::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Book::getName (const MWWorld::ConstPtr& ptr) const @@ -55,21 +52,22 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Book::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Book::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfItem"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfItem", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; } - return std::shared_ptr(new MWWorld::ActionRead(ptr)); + return std::make_unique(ptr); } std::string Book::getScript (const MWWorld::ConstPtr& ptr) const @@ -86,13 +84,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Book::registerSelf() - { - std::shared_ptr instance (new Book); - - registerClass (typeid (ESM::Book).name(), instance); - } - std::string Book::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Book Up"); @@ -147,7 +138,7 @@ namespace MWClass const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Book newItem = *ref->mBase; - newItem.mId=""; + newItem.mId.clear(); newItem.mName=newName; newItem.mData.mIsScroll = 1; newItem.mData.mEnchant=enchCharge; @@ -156,9 +147,9 @@ namespace MWClass return record->mId; } - std::shared_ptr Book::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Book::use (const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionRead(ptr)); + return std::make_unique(ptr); } MWWorld::Ptr Book::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index c58e68ad87..a5152fa812 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_BOOK_H #define GAME_MWCLASS_BOOK_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Book : public MWWorld::Class + class Book : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Book(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -32,8 +34,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -49,7 +49,7 @@ namespace MWClass std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/classmodel.hpp b/apps/openmw/mwclass/classmodel.hpp new file mode 100644 index 0000000000..3f5997cf4d --- /dev/null +++ b/apps/openmw/mwclass/classmodel.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_MWCLASS_CLASSMODEL_H +#define OPENMW_MWCLASS_CLASSMODEL_H + +#include "../mwbase/environment.hpp" + +#include "../mwworld/ptr.hpp" +#include "../mwworld/livecellref.hpp" + +#include +#include + +#include + +namespace MWClass +{ + template + std::string 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 {}; + } +} + +#endif diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 6d7960aac2..c20a7dae22 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -1,6 +1,8 @@ #include "clothing.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,8 +21,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Clothing::Clothing() + : MWWorld::RegisteredClass(ESM::Clothing::sRecordId) + { + } void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -29,20 +37,9 @@ namespace MWClass } } - void Clothing::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Clothing::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Clothing::getName (const MWWorld::ConstPtr& ptr) const @@ -53,7 +50,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Clothing::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Clothing::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -122,13 +119,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Clothing::registerSelf() - { - std::shared_ptr instance (new Clothing); - - registerClass (typeid (ESM::Clothing).name(), instance); - } - std::string Clothing::getUpSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); @@ -197,7 +187,7 @@ namespace MWClass const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Clothing newItem = *ref->mBase; - newItem.mId=""; + newItem.mId.clear(); newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; @@ -236,9 +226,9 @@ namespace MWClass return std::make_pair (1, ""); } - std::shared_ptr Clothing::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Clothing::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index a87e0cbe00..8e0a0c90bd 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_CLOTHING_H #define GAME_MWCLASS_CLOTHING_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Clothing : public MWWorld::Class + class Clothing : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Clothing(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -40,8 +42,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -61,7 +61,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index a27e3debdd..63b511cadd 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -1,18 +1,18 @@ #include "container.hpp" -#include -#include +#include + +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/nullaction.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/actionharvest.hpp" @@ -27,14 +27,17 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" -#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/inventory.hpp" + +#include "classmodel.hpp" namespace MWClass { ContainerCustomData::ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell) { - unsigned int seed = Misc::Rng::rollDice(std::numeric_limits::max()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + unsigned int seed = Misc::Rng::rollDice(std::numeric_limits::max(), prng); // setting ownership not needed, since taking items from a container inherits the // container's owner automatically mStore.fillNonRandom(container.mInventory, "", seed); @@ -45,11 +48,6 @@ namespace MWClass mStore.readState(inventory); } - MWWorld::CustomData *ContainerCustomData::clone() const - { - return new ContainerCustomData (*this); - } - ContainerCustomData& ContainerCustomData::asContainerCustomData() { return *this; @@ -59,6 +57,12 @@ namespace MWClass return *this; } + Container::Container() + : MWWorld::RegisteredClass(ESM::Container::sRecordId) + , mHarvestEnabled(Settings::Manager::getBool("graphic herbalism", "Game")) + { + } + void Container::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) @@ -66,14 +70,16 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); // store - ptr.getRefData().setCustomData (std::make_unique(*ref->mBase, ptr.getCell()).release()); + ptr.getRefData().setCustomData (std::make_unique(*ref->mBase, ptr.getCell())); MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } } - bool canBeHarvested(const MWWorld::ConstPtr& ptr) + bool Container::canBeHarvested(const MWWorld::ConstPtr& ptr) const { + if (!mHarvestEnabled) + return false; const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation == nullptr) return false; @@ -103,21 +109,19 @@ namespace MWClass } } - void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Container::getModel(const MWWorld::ConstPtr &ptr) const + void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + std::string Container::getModel(const MWWorld::ConstPtr &ptr) const + { + return getClassModel(ptr); } bool Container::useAnim() const @@ -125,18 +129,19 @@ namespace MWClass return true; } - std::shared_ptr Container::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Container::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr (new MWWorld::NullAction ()); + return std::make_unique(); if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfContainer"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfContainer", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; @@ -184,24 +189,22 @@ namespace MWClass { if (canBeHarvested(ptr)) { - std::shared_ptr action (new MWWorld::ActionHarvest(ptr)); - return action; + return std::make_unique(ptr); } - std::shared_ptr action (new MWWorld::ActionOpen(ptr)); - return action; + return std::make_unique(ptr); } else { // Activate trap - std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); + std::unique_ptr action = std::make_unique(ptr.getCellRef().getTrap(), ptr); action->setSound(trapActivationSound); return action; } } else { - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + std::unique_ptr action = std::make_unique(std::string(), ptr); action->setSound(lockedSound); return action; } @@ -230,13 +233,6 @@ namespace MWClass return ref->mBase->mScript; } - void Container::registerSelf() - { - std::shared_ptr instance (new Container); - - registerClass (typeid (ESM::Container).name(), instance); - } - bool Container::hasToolTip (const MWWorld::ConstPtr& ptr) const { if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) @@ -309,7 +305,7 @@ namespace MWClass return; const ESM::ContainerState& containerState = state.asContainerState(); - ptr.getRefData().setCustomData(std::make_unique(containerState.mInventory).release()); + ptr.getRefData().setCustomData(std::make_unique(containerState.mInventory)); } 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 57dbf0c76c..9ecf323b71 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWCLASS_CONTAINER_H #define GAME_MWCLASS_CONTAINER_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" @@ -13,38 +13,44 @@ namespace ESM namespace MWClass { - class ContainerCustomData : public MWWorld::CustomData + class ContainerCustomData : public MWWorld::TypedCustomData { MWWorld::ContainerStore mStore; public: ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell); ContainerCustomData(const ESM::InventoryState& inventory); - MWWorld::CustomData *clone() const override; - ContainerCustomData& asContainerCustomData() override; const ContainerCustomData& asContainerCustomData() const override; friend class Container; }; - class Container : public MWWorld::Class + class Container : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + const bool mHarvestEnabled; + + Container(); + void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; - public: + bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; + public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -77,8 +83,6 @@ namespace MWClass void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. - static void registerSelf(); - void respawn (const MWWorld::Ptr& ptr) const override; std::string getModel(const MWWorld::ConstPtr &ptr) const override; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index c51eab513f..c71e277c15 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -2,8 +2,8 @@ #include #include -#include -#include +#include +#include #include #include "../mwmechanics/creaturestats.hpp" @@ -12,6 +12,13 @@ #include "../mwmechanics/disease.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/combat.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturecustomdataresetter.hpp" +#include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/inventory.hpp" +#include "../mwmechanics/setbaseaisetting.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -25,20 +32,17 @@ #include "../mwworld/failedaction.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/containerstore.hpp" -#include "../mwphysics/physicssystem.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/localscripts.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/objects.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/combat.hpp" -#include "../mwmechanics/actorutil.hpp" +#include "classmodel.hpp" namespace { @@ -51,14 +55,16 @@ namespace namespace MWClass { - class CreatureCustomData : public MWWorld::CustomData + class CreatureCustomData : public MWWorld::TypedCustomData { public: MWMechanics::CreatureStats mCreatureStats; - MWWorld::ContainerStore* mContainerStore; // may be InventoryStore for some creatures + std::unique_ptr mContainerStore; // may be InventoryStore for some creatures MWMechanics::Movement mMovement; - MWWorld::CustomData *clone() const override; + CreatureCustomData() = default; + CreatureCustomData(const CreatureCustomData& other); + CreatureCustomData(CreatureCustomData&& other) = default; CreatureCustomData& asCreatureCustomData() override { @@ -68,26 +74,29 @@ namespace MWClass { return *this; } - - CreatureCustomData() : mContainerStore(0) {} - virtual ~CreatureCustomData() { delete mContainerStore; } }; - MWWorld::CustomData *CreatureCustomData::clone() const + CreatureCustomData::CreatureCustomData(const CreatureCustomData& other) + : mCreatureStats(other.mCreatureStats), + mContainerStore(other.mContainerStore->clone()), + mMovement(other.mMovement) + { + } + + Creature::Creature() + : MWWorld::RegisteredClass(ESM::Creature::sRecordId) { - CreatureCustomData* cloned = new CreatureCustomData (*this); - cloned->mContainerStore = mContainerStore->clone(); - return cloned; } const Creature::GMST& Creature::getGmst() { - static GMST gmst; - static bool inited = false; - if (!inited) + static const GMST staticGmst = [] { + GMST gmst; + const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); + gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature"); gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); @@ -101,16 +110,20 @@ namespace MWClass gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); - inited = true; - } - return gmst; + + return gmst; + } (); + return staticGmst; } void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureCustomData); + auto tempData = std::make_unique(); + CreatureCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter {ptr}; + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -131,10 +144,10 @@ namespace MWClass data->mCreatureStats.getAiSequence().fill(ref->mBase->mAiPackage); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Hello, ref->mBase->mAiData.mHello); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Fight, ref->mBase->mAiData.mFight); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Flee, ref->mBase->mAiData.mFlee); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Alarm, ref->mBase->mAiData.mAlarm); // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) @@ -148,18 +161,16 @@ namespace MWClass // inventory bool hasInventory = hasInventoryStore(ptr); if (hasInventory) - data->mContainerStore = new MWWorld::InventoryStore(); + data->mContainerStore = std::make_unique(); else - data->mContainerStore = new MWWorld::ContainerStore(); + data->mContainerStore = std::make_unique(); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); - data->mCreatureStats.setNeedRecalcDynamicStats(false); + resetter.mPtr = {}; - // store - ptr.getRefData().setCustomData(data.release()); - - getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); if (hasInventory) getInventoryStore(ptr).autoEquip(ptr); @@ -174,13 +185,7 @@ namespace MWClass std::string Creature::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } void Creature::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const @@ -229,7 +234,7 @@ namespace MWClass const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats &stats = getCreatureStats(ptr); - if (stats.getDrawState() != MWMechanics::DrawState_Weapon) + if (stats.getDrawState() != MWMechanics::DrawState::Weapon) return; // Get the weapon used (if hand-to-hand, weapon = inv.end()) @@ -238,7 +243,7 @@ namespace MWClass { MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weaponslot != inv.end() && weaponslot->getTypeName() == typeid(ESM::Weapon).name()) + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) weapon = *weaponslot; } @@ -264,8 +269,8 @@ namespace MWClass osg::Vec3f hitPosition (result.second); float hitchance = MWMechanics::getHitChance(ptr, victim, ref->mBase->mData.mCombat); - - if(Misc::Rng::roll0to99() >= hitchance) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if(Misc::Rng::roll0to99(prng) >= hitchance) { victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); @@ -305,8 +310,8 @@ namespace MWClass { damage = attack[0] + ((attack[1]-attack[0])*attackStrength); MWMechanics::adjustWeaponDamage(damage, weapon, ptr); - MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); + MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); } // Apply "On hit" enchanted weapons @@ -392,7 +397,8 @@ namespace MWClass float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + getGmst().iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? @@ -423,15 +429,16 @@ namespace MWClass } } - std::shared_ptr Creature::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Creature::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfCreature"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfCreature", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; @@ -445,20 +452,20 @@ namespace MWClass // by default user can loot friendly actors during death animation if (canLoot && !stats.getAiSequence().isInCombat()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); // otherwise wait until death animation if(stats.isDeathAnimationFinished()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); } else if (!stats.getAiSequence().isInCombat() && !stats.getKnockedDown()) - return std::shared_ptr(new MWWorld::ActionTalk(ptr)); + return std::make_unique(ptr); // 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::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); - return std::shared_ptr(new MWWorld::FailedAction("")); + return std::make_unique(); } MWWorld::ContainerStore& Creature::getContainerStore (const MWWorld::Ptr& ptr) const @@ -493,13 +500,6 @@ namespace MWClass return isFlagBitSet(ptr, ESM::Creature::Essential); } - void Creature::registerSelf() - { - std::shared_ptr instance (new Creature); - - registerClass (typeid (ESM::Creature).name(), instance); - } - float Creature::getMaxSpeed(const MWWorld::Ptr &ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); @@ -590,7 +590,7 @@ namespace MWClass bool Creature::isPersistent(const MWWorld::ConstPtr &actor) const { const MWWorld::LiveCellRef* ref = actor.get(); - return ref->mBase->mPersistent; + return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; } std::string Creature::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const @@ -622,10 +622,12 @@ namespace MWClass const std::string 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, "meshes\\" + creature.mModel)) + && Misc::StringUtils::ciEqual(model, + Misc::ResourceHelpers::correctMeshPath(creature.mModel, vfs))) { const std::string& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; sound = store.get().begin(); @@ -642,10 +644,11 @@ namespace MWClass } } + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (!sounds.empty()) - return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; + return sounds[Misc::Rng::rollDice(sounds.size(), prng)]->mSound; if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; return std::string(); } @@ -750,26 +753,33 @@ namespace MWClass if (!state.mHasCustomState) return; + const ESM::CreatureState& creatureState = state.asCreatureState(); + if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { - // Create a CustomData, but don't fill it from ESM records (not needed) - std::unique_ptr data (new CreatureCustomData); - - if (hasInventoryStore(ptr)) - data->mContainerStore = new MWWorld::InventoryStore(); + if (creatureState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); else - data->mContainerStore = new MWWorld::ContainerStore(); + { + // 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 + data->mContainerStore = std::make_unique(); - ptr.getRefData().setCustomData (data.release()); + 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(); - const ESM::CreatureState& creatureState = state.asCreatureState(); + customData.mContainerStore->readState (creatureState.mInventory); bool spellsInitialised = customData.mCreatureStats.getSpells().setSpells(ptr.get()->mBase->mId); if(spellsInitialised) @@ -832,12 +842,12 @@ namespace MWClass } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } @@ -854,7 +864,7 @@ namespace MWClass scale *= ref->mBase->mScale; } - void Creature::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Creature::setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } @@ -882,12 +892,8 @@ namespace MWClass float Creature::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - const GMST& gmst = getGmst(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); - return getWalkSpeed(ptr) - * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) - * (gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + return getSwimSpeedImpl(ptr, getGmst(), mageffects, getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 5bb5030348..a1696d80f0 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -1,6 +1,10 @@ #ifndef GAME_MWCLASS_CREATURE_H #define GAME_MWCLASS_CREATURE_H +#include + +#include "../mwworld/registeredclass.hpp" + #include "actor.hpp" namespace ESM @@ -10,8 +14,12 @@ namespace ESM namespace MWClass { - class Creature : public Actor + class Creature : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Creature(); + void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; @@ -59,7 +67,7 @@ namespace MWClass 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; - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -96,8 +104,6 @@ namespace MWClass float getMaxSpeed (const MWWorld::Ptr& ptr) const override; - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; @@ -128,7 +134,7 @@ namespace MWClass 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 - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; + void setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const override; void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index e3e52901e0..431f9675b6 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -1,24 +1,29 @@ #include "creaturelevlist.hpp" -#include -#include +#include +#include #include "../mwmechanics/levelledlist.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" + #include "../mwmechanics/creaturestats.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + namespace MWClass { - class CreatureLevListCustomData : public MWWorld::CustomData + class CreatureLevListCustomData : public MWWorld::TypedCustomData { public: // actorId of the creature we spawned int mSpawnActorId; bool mSpawn; // Should a new creature be spawned? - MWWorld::CustomData *clone() const override; - CreatureLevListCustomData& asCreatureLevListCustomData() override { return *this; @@ -29,9 +34,27 @@ namespace MWClass } }; - MWWorld::CustomData *CreatureLevListCustomData::clone() const + CreatureLevList::CreatureLevList() + : MWWorld::RegisteredClass(ESM::CreatureLevList::sRecordId) + { + } + + MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { - return new CreatureLevListCustomData (*this); + const MWWorld::LiveCellRef *ref = ptr.get(); + + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const + { + if (ptr.getRefData().getCustomData() == nullptr) + return; + + CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); + MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (!creature.isEmpty()) + MWBase::Environment::get().getWorld()->adjustPosition(creature, force); } std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const @@ -52,7 +75,13 @@ namespace MWClass if (customData.mSpawn) return; - MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + MWWorld::Ptr creature; + if(customData.mSpawnActorId != -1) + { + creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if(creature.isEmpty()) + creature = ptr.getCell()->getMovedActor(customData.mSpawnActorId); + } if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); @@ -73,13 +102,6 @@ namespace MWClass customData.mSpawn = true; } - void CreatureLevList::registerSelf() - { - std::shared_ptr instance (new CreatureLevList); - - registerClass (typeid (ESM::CreatureLevList).name(), instance); - } - void CreatureLevList::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const { // disable for now, too many false positives @@ -109,7 +131,8 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - std::string id = MWMechanics::getLevelledItem(ref->mBase, true); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + std::string id = MWMechanics::getLevelledItem(ref->mBase, true, prng); if (!id.empty()) { @@ -138,11 +161,11 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureLevListCustomData); + std::unique_ptr data = std::make_unique(); data->mSpawnActorId = -1; data->mSpawn = true; - ptr.getRefData().setCustomData(data.release()); + ptr.getRefData().setCustomData(std::move(data)); } } diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 35152a9422..81c2f80070 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -1,13 +1,17 @@ #ifndef GAME_MWCLASS_CREATURELEVLIST_H #define GAME_MWCLASS_CREATURELEVLIST_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class CreatureLevList : public MWWorld::Class + class CreatureLevList : public MWWorld::RegisteredClass { - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; + + CreatureLevList(); + + void ensureCustomData (const MWWorld::Ptr& ptr) const; public: @@ -17,8 +21,6 @@ namespace MWClass bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - static void registerSelf(); - 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(). @@ -32,6 +34,10 @@ namespace MWClass ///< Write additional state from \a ptr into \a state. void respawn (const MWWorld::Ptr& ptr) const override; + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + + void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index ba51d9c2bc..1a8c01571d 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -1,7 +1,9 @@ #include "door.hpp" -#include -#include +#include + +#include +#include #include #include "../mwbase/environment.hpp" @@ -19,6 +21,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwworld/customdata.hpp" +#include "../mwworld/cellutils.hpp" #include "../mwgui/tooltips.hpp" @@ -29,15 +32,15 @@ #include "../mwmechanics/actorutil.hpp" +#include "classmodel.hpp" + namespace MWClass { - class DoorCustomData : public MWWorld::CustomData + class DoorCustomData : public MWWorld::TypedCustomData { public: MWWorld::DoorState mDoorState = MWWorld::DoorState::Idle; - MWWorld::CustomData *clone() const override; - DoorCustomData& asDoorCustomData() override { return *this; @@ -48,9 +51,9 @@ namespace MWClass } }; - MWWorld::CustomData *DoorCustomData::clone() const + Door::Door() + : MWWorld::RegisteredClass(ESM::Door::sRecordId) { - return new DoorCustomData (*this); } void Door::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const @@ -62,10 +65,9 @@ namespace MWClass } } - void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model, MWPhysics::CollisionType_Door); + insertObjectPhysics(ptr, model, rotation, physics); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) @@ -78,6 +80,11 @@ 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); + } + bool Door::isDoor() const { return true; @@ -90,13 +97,7 @@ namespace MWClass std::string Door::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Door::getName (const MWWorld::ConstPtr& ptr) const @@ -107,7 +108,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Door::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Door::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { MWWorld::LiveCellRef *ref = ptr.get(); @@ -128,7 +129,7 @@ namespace MWClass // Make such activation a no-op for now, like how it is in the vanilla game. if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) { - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + std::unique_ptr action = std::make_unique(std::string(), ptr); action->setSound(lockedSound); return action; } @@ -139,12 +140,14 @@ namespace MWClass MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if(animation) + { + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); + const ESM::MagicEffect *effect = store.get().find(index); - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); - const ESM::MagicEffect *effect = store.get().find(index); - - animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing + animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing + } } const std::string keyId = ptr.getCellRef().getKey(); @@ -177,7 +180,7 @@ namespace MWClass if(isTrapped) { // Trap activation - std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); + std::unique_ptr action = std::make_unique(ptr.getCellRef().getTrap(), ptr); action->setSound(trapActivationSound); return action; } @@ -187,12 +190,11 @@ namespace MWClass if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { // player activated teleport door with telekinesis - std::shared_ptr action(new MWWorld::FailedAction); - return action; + return std::make_unique(); } else { - std::shared_ptr action(new MWWorld::ActionTeleport (ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true)); + std::unique_ptr action = std::make_unique(ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true); action->setSound(openSound); return action; } @@ -200,7 +202,7 @@ namespace MWClass else { // animated door - std::shared_ptr action(new MWWorld::ActionDoor(ptr)); + std::unique_ptr action = std::make_unique(ptr); const auto doorState = getDoorState(ptr); bool opening = true; float doorRot = ptr.getRefData().getPosition().rot[2] - ptr.getCellRef().getPosition().rot[2]; @@ -234,7 +236,7 @@ namespace MWClass else { // locked, and we can't open. - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + std::unique_ptr action = std::make_unique(std::string(), ptr); action->setSound(lockedSound); return action; } @@ -260,13 +262,6 @@ namespace MWClass return ref->mBase->mScript; } - void Door::registerSelf() - { - std::shared_ptr instance (new Door); - - registerClass (typeid (ESM::Door).name(), instance); - } - MWGui::ToolTipInfo Door::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); @@ -302,30 +297,15 @@ namespace MWClass std::string Door::getDestination (const MWWorld::LiveCellRef& door) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - - std::string dest; - if (door.mRef.getDestCell() != "") - { - // door leads to an interior, use interior name as tooltip - dest = door.mRef.getDestCell(); - } - else + std::string dest = door.mRef.getDestCell(); + if (dest.empty()) { // door leads to exterior, use cell name (if any), otherwise translated region name - int x,y; - MWBase::Environment::get().getWorld()->positionToIndex (door.mRef.getDoorDest().pos[0], door.mRef.getDoorDest().pos[1], x, y); - const ESM::Cell* cell = store.get().find(x,y); - if (cell->mName != "") - dest = cell->mName; - else - { - const ESM::Region* region = - store.get().find(cell->mRegion); - - //name as is, not a token - return MyGUI::TextIterator::toTagsString(region->mName); - } + auto world = MWBase::Environment::get().getWorld(); + const osg::Vec2i index = MWWorld::positionToCellIndex(door.mRef.getDoorDest().pos[0], + door.mRef.getDoorDest().pos[1]); + const ESM::Cell* cell = world->getStore().get().search(index.x(), index.y()); + dest = world->getCellName(cell); } return "#{sCell=" + dest + "}"; @@ -342,8 +322,7 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new DoorCustomData); - ptr.getRefData().setCustomData(data.release()); + ptr.getRefData().setCustomData(std::make_unique()); } } diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 6c2fa26b80..d3a369d89e 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -1,14 +1,18 @@ #ifndef GAME_MWCLASS_DOOR_H #define GAME_MWCLASS_DOOR_H -#include +#include -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Door : public MWWorld::Class + class Door : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Door(); + void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; @@ -18,7 +22,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool isDoor() const override; @@ -27,7 +32,7 @@ namespace MWClass std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -45,8 +50,6 @@ namespace MWClass std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; MWWorld::DoorState getDoorState (const MWWorld::ConstPtr &ptr) const override; diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index a007ad115f..bd830dc6b7 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -1,6 +1,8 @@ #include "ingredient.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -18,8 +20,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Ingredient::Ingredient() + : MWWorld::RegisteredClass(ESM::Ingredient::sRecordId) + { + } void Ingredient::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -28,20 +36,9 @@ namespace MWClass } } - void Ingredient::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Ingredient::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Ingredient::getName (const MWWorld::ConstPtr& ptr) const @@ -52,7 +49,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Ingredient::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Ingredient::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -73,22 +70,15 @@ namespace MWClass } - std::shared_ptr Ingredient::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Ingredient::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action (new MWWorld::ActionEat (ptr)); + std::unique_ptr action = std::make_unique(ptr); action->setSound ("Swallow"); return action; } - void Ingredient::registerSelf() - { - std::shared_ptr instance (new Ingredient); - - registerClass (typeid (ESM::Ingredient).name(), instance); - } - std::string Ingredient::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Ingredient Up"); diff --git a/apps/openmw/mwclass/ingredient.hpp b/apps/openmw/mwclass/ingredient.hpp index 5219cf39ce..95b3d94e5e 100644 --- a/apps/openmw/mwclass/ingredient.hpp +++ b/apps/openmw/mwclass/ingredient.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_INGREDIENT_H #define GAME_MWCLASS_INGREDIENT_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Ingredient : public MWWorld::Class + class Ingredient : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Ingredient(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -32,10 +34,8 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + std::unique_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu - - static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id diff --git a/apps/openmw/mwclass/itemlevlist.cpp b/apps/openmw/mwclass/itemlevlist.cpp index 5608a8d233..2624be4aa8 100644 --- a/apps/openmw/mwclass/itemlevlist.cpp +++ b/apps/openmw/mwclass/itemlevlist.cpp @@ -1,9 +1,13 @@ #include "itemlevlist.hpp" -#include +#include namespace MWClass { + ItemLevList::ItemLevList() + : MWWorld::RegisteredClass(ESM::ItemLevList::sRecordId) + { + } std::string ItemLevList::getName (const MWWorld::ConstPtr& ptr) const { @@ -14,11 +18,4 @@ namespace MWClass { return false; } - - void ItemLevList::registerSelf() - { - std::shared_ptr instance (new ItemLevList); - - registerClass (typeid (ESM::ItemLevList).name(), instance); - } } diff --git a/apps/openmw/mwclass/itemlevlist.hpp b/apps/openmw/mwclass/itemlevlist.hpp index 771f8b7a76..d7377bbcb4 100644 --- a/apps/openmw/mwclass/itemlevlist.hpp +++ b/apps/openmw/mwclass/itemlevlist.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_ITEMLEVLIST_H #define GAME_MWCLASS_ITEMLEVLIST_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class ItemLevList : public MWWorld::Class + class ItemLevList : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + ItemLevList(); + public: std::string getName (const MWWorld::ConstPtr& ptr) const override; @@ -14,8 +18,6 @@ namespace MWClass bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - - static void registerSelf(); }; } diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 3bdf10f475..564775b5d2 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -1,7 +1,9 @@ #include "light.hpp" -#include -#include +#include + +#include +#include #include #include "../mwbase/environment.hpp" @@ -21,8 +23,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Light::Light() + : MWWorld::RegisteredClass(ESM::Light::sRecordId) + { + } void Light::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -33,15 +41,13 @@ namespace MWClass renderingInterface.getObjects().insertModel(ptr, model, true, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } - void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { MWWorld::LiveCellRef *ref = ptr.get(); assert (ref->mBase != nullptr); - // TODO: add option somewhere to enable collision for placeable objects - if (!model.empty() && (ref->mBase->mData.mFlags & ESM::Light::Carry) == 0) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, @@ -49,6 +55,13 @@ namespace MWClass MWSound::PlayMode::Loop); } + void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const + { + // 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); + } + bool Light::useAnim() const { return true; @@ -56,13 +69,7 @@ namespace MWClass std::string Light::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Light::getName (const MWWorld::ConstPtr& ptr) const @@ -76,15 +83,15 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Light::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Light::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr(new MWWorld::NullAction()); + return std::make_unique(); MWWorld::LiveCellRef *ref = ptr.get(); if(!(ref->mBase->mData.mFlags&ESM::Light::Carry)) - return std::shared_ptr(new MWWorld::FailedAction()); + return std::make_unique(); return defaultItemActivate(ptr, actor); } @@ -115,13 +122,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Light::registerSelf() - { - std::shared_ptr instance (new Light); - - registerClass (typeid (ESM::Light).name(), instance); - } - std::string Light::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Misc Up"); @@ -182,9 +182,9 @@ namespace MWClass return Class::showsInInventory(ptr); } - std::shared_ptr Light::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Light::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index e37dddc250..0104e88b6b 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_LIGHT_H #define GAME_MWCLASS_LIGHT_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Light : public MWWorld::Class + class Light : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Light(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,7 +18,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; @@ -29,7 +34,7 @@ namespace MWClass bool showsInInventory (const MWWorld::ConstPtr& ptr) const override; - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -43,8 +48,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -54,7 +57,7 @@ namespace MWClass std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + std::unique_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu void setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const override; diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 9b8abc8f23..ee32e2945c 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -1,6 +1,8 @@ #include "lockpick.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -18,8 +20,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Lockpick::Lockpick() + : MWWorld::RegisteredClass(ESM::Lockpick::sRecordId) + { + } void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -28,20 +36,9 @@ namespace MWClass } } - void Lockpick::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Lockpick::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Lockpick::getName (const MWWorld::ConstPtr& ptr) const @@ -52,7 +49,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Lockpick::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Lockpick::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -81,13 +78,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Lockpick::registerSelf() - { - std::shared_ptr instance (new Lockpick); - - registerClass (typeid (ESM::Lockpick).name(), instance); - } - std::string Lockpick::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Lockpick Up"); @@ -132,9 +122,9 @@ namespace MWClass return info; } - std::shared_ptr Lockpick::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Lockpick::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index fabae33435..3aac0c70b7 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_LOCKPICK_H #define GAME_MWCLASS_LOCKPICK_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Lockpick : public MWWorld::Class + class Lockpick : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Lockpick(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -36,8 +38,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -49,7 +49,7 @@ namespace MWClass std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 8d3cda6fe5..be5cf9c143 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -1,6 +1,8 @@ #include "misc.hpp" -#include +#include + +#include #include #include "../mwbase/environment.hpp" @@ -19,8 +21,15 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Miscellaneous::Miscellaneous() + : MWWorld::RegisteredClass(ESM::Miscellaneous::sRecordId) + { + } + bool Miscellaneous::isGold (const MWWorld::ConstPtr& ptr) const { return Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_001") @@ -37,20 +46,9 @@ namespace MWClass } } - void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Miscellaneous::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Miscellaneous::getName (const MWWorld::ConstPtr& ptr) const @@ -61,7 +59,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Miscellaneous::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Miscellaneous::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -107,13 +105,6 @@ namespace MWClass return value; } - void Miscellaneous::registerSelf() - { - std::shared_ptr instance (new Miscellaneous); - - registerClass (typeid (ESM::Miscellaneous).name(), instance); - } - std::string Miscellaneous::getUpSoundId (const MWWorld::ConstPtr& ptr) const { if (isGold(ptr)) @@ -210,12 +201,11 @@ namespace MWClass return newPtr; } - std::shared_ptr Miscellaneous::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Miscellaneous::use (const MWWorld::Ptr& ptr, bool force) const { if (ptr.getCellRef().getSoul().empty() || !MWBase::Environment::get().getWorld()->getStore().get().search(ptr.getCellRef().getSoul())) - return std::shared_ptr(new MWWorld::NullAction()); - else - return std::shared_ptr(new MWWorld::ActionSoulgem(ptr)); + return std::make_unique(); + return std::make_unique(ptr); } bool Miscellaneous::canSell (const MWWorld::ConstPtr& item, int npcServices) const diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index 9bff85ca56..ebf38b10be 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_MISC_H #define GAME_MWCLASS_MISC_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Miscellaneous : public MWWorld::Class + class Miscellaneous : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Miscellaneous(); + public: MWWorld::Ptr copyToCell(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell, int count) const override; @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -32,8 +34,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -45,7 +45,7 @@ namespace MWClass std::string getModel(const MWWorld::ConstPtr &ptr) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + std::unique_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu float getWeight (const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 27c98bbce5..2c98b4dd3c 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1,15 +1,21 @@ #include "npc.hpp" +#include + #include #include #include +#include #include -#include -#include -#include +#include +#include +#include #include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -17,6 +23,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -28,6 +35,10 @@ #include "../mwmechanics/difficultyscaling.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturecustomdataresetter.hpp" +#include "../mwmechanics/inventory.hpp" +#include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/setbaseaisetting.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" @@ -35,9 +46,9 @@ #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/customdata.hpp" -#include "../mwphysics/physicssystem.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/localscripts.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -245,16 +256,18 @@ namespace namespace MWClass { + Npc::Npc() + : MWWorld::RegisteredClass(ESM::NPC::sRecordId) + { + } - class NpcCustomData : public MWWorld::CustomData + class NpcCustomData : public MWWorld::TypedCustomData { public: MWMechanics::NpcStats mNpcStats; MWMechanics::Movement mMovement; MWWorld::InventoryStore mInventoryStore; - MWWorld::CustomData *clone() const override; - NpcCustomData& asNpcCustomData() override { return *this; @@ -265,17 +278,12 @@ namespace MWClass } }; - MWWorld::CustomData *NpcCustomData::clone() const - { - return new NpcCustomData (*this); - } - const Npc::GMST& Npc::getGmst() { - static GMST gmst; - static bool inited = false; - if(!inited) + static const GMST staticGmst = [] { + GMST gmst; + const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); @@ -300,16 +308,20 @@ namespace MWClass gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); - inited = true; - } - return gmst; + return gmst; + } (); + return staticGmst; } void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new NpcCustomData); + bool recalculate = false; + auto tempData = std::make_unique(); + NpcCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter {ptr}; + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -340,8 +352,6 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); - - data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -357,7 +367,7 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); - data->mNpcStats.setNeedRecalcDynamicStats(true); + recalculate = true; } // Persistent actors with 0 health do not play death animation @@ -381,23 +391,26 @@ namespace MWClass data->mNpcStats.getAiSequence().fill(ref->mBase->mAiPackage); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Hello, ref->mBase->mAiData.mHello); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Fight, ref->mBase->mAiData.mFight); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Flee, ref->mBase->mAiData.mFlee); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Alarm, ref->mBase->mAiData.mAlarm); // spells if (!spellsInitialised) data->mNpcStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); - // inventory - // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items - data->mInventoryStore.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); - data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData (data.release()); + resetter.mPtr = {}; + if(recalculate) + data->mNpcStats.recalculateMagicka(); + + // 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(ptr); } @@ -411,17 +424,17 @@ namespace MWClass bool Npc::isPersistent(const MWWorld::ConstPtr &actor) const { const MWWorld::LiveCellRef* ref = actor.get(); - return ref->mBase->mPersistent; + return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; } std::string Npc::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); - std::string model = "meshes\\base_anim.nif"; + std::string model = Settings::Manager::getString("baseanim", "Models"); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); if(race->mData.mFlags & ESM::Race::Beast) - model = "meshes\\base_animkna.nif"; + model = Settings::Manager::getString("baseanimkna", "Models"); return model; } @@ -431,27 +444,29 @@ namespace MWClass const MWWorld::LiveCellRef *npc = ptr.get(); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mRace); if(race && race->mData.mFlags & ESM::Race::Beast) - models.emplace_back("meshes\\base_animkna.nif"); + models.emplace_back(Settings::Manager::getString("baseanimkna", "Models")); // keep these always loaded just in case - models.emplace_back("meshes/xargonian_swimkna.nif"); - models.emplace_back("meshes/xbase_anim_female.nif"); - models.emplace_back("meshes/xbase_anim.nif"); + 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(); if (!npc->mBase->mModel.empty()) - models.push_back("meshes/"+npc->mBase->mModel); + models.push_back(Misc::ResourceHelpers::correctMeshPath(npc->mBase->mModel, vfs)); if (!npc->mBase->mHead.empty()) { const ESM::BodyPart* head = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHead); if (head) - models.push_back("meshes/"+head->mModel); + models.push_back(Misc::ResourceHelpers::correctMeshPath(head->mModel, vfs)); } if (!npc->mBase->mHair.empty()) { const ESM::BodyPart* hair = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHair); if (hair) - models.push_back("meshes/"+hair->mModel); + models.push_back(Misc::ResourceHelpers::correctMeshPath(hair->mModel, vfs)); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); @@ -465,12 +480,12 @@ namespace MWClass if (equipped != invStore.end()) { std::vector parts; - if(equipped->getTypeName() == typeid(ESM::Clothing).name()) + if(equipped->getType() == ESM::Clothing::sRecordId) { const ESM::Clothing *clothes = equipped->get()->mBase; parts = clothes->mParts.mParts; } - else if(equipped->getTypeName() == typeid(ESM::Armor).name()) + else if(equipped->getType() == ESM::Armor::sRecordId) { const ESM::Armor *armor = equipped->get()->mBase; parts = armor->mParts.mParts; @@ -489,7 +504,7 @@ namespace MWClass partname = female ? it->mMale : it->mFemale; const ESM::BodyPart* part = MWBase::Environment::get().getWorld()->getStore().get().search(partname); if (part && !part->mModel.empty()) - models.push_back("meshes/"+part->mModel); + models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); } } } @@ -502,7 +517,7 @@ namespace MWClass { const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) - models.push_back("meshes/"+part->mModel); + models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); } } @@ -549,7 +564,7 @@ namespace MWClass MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr weapon = ((weaponslot != inv.end()) ? *weaponslot : MWWorld::Ptr()); - if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) + if(!weapon.isEmpty() && weapon.getType() != ESM::Weapon::sRecordId) weapon = MWWorld::Ptr(); MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); @@ -587,7 +602,7 @@ namespace MWClass float hitchance = MWMechanics::getHitChance(ptr, victim, getSkill(ptr, weapskill)); - if (Misc::Rng::roll0to99() >= hitchance) + if (Misc::Rng::roll0to99(world->getPrng()) >= hitchance) { othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); @@ -610,9 +625,9 @@ namespace MWClass damage = attack[0] + ((attack[1]-attack[0])*attackStrength); } MWMechanics::adjustWeaponDamage(damage, weapon, ptr); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::applyWerewolfDamageMult(victim, weapon, damage); - MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); healthdmg = true; } else @@ -729,15 +744,16 @@ namespace MWClass const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const GMST& gmst = getGmst(); - int chance = store.get().find("iVoiceHitOdds")->mValue.getInteger(); - if (Misc::Rng::roll0to99() < chance) + int chance = store.get().find("iVoiceHitOdds")->mValue.getInteger(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) < chance) MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); // Check for knockdown float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) + if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? @@ -760,7 +776,7 @@ namespace MWClass MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; - int hitslot = hitslots[Misc::Rng::rollDice(20)]; + int hitslot = hitslots[Misc::Rng::rollDice(20, prng)]; float unmitigatedDamage = damage; float x = damage / (damage + getArmorRating(ptr)); @@ -772,11 +788,11 @@ namespace MWClass MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); - bool hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; // If there's no item in the carried left slot or if it is not a shield redistribute the hit. if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) { - if (Misc::Rng::rollDice(2) == 0) + if (Misc::Rng::rollDice(2, prng) == 0) hitslot = MWWorld::InventoryStore::Slot_Cuirass; else hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; @@ -784,7 +800,7 @@ namespace MWClass if (armorslot != inv.end()) { armor = *armorslot; - hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; } } if (hasArmor) @@ -857,20 +873,21 @@ namespace MWClass } } - std::shared_ptr Npc::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Npc::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { // player got activated by another NPC if(ptr == MWMechanics::getPlayer()) - return std::shared_ptr(new MWWorld::ActionTalk(actor)); + return std::make_unique(actor); // Werewolfs can't activate NPCs if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfNPC"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfNPC", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; @@ -884,33 +901,33 @@ namespace MWClass // by default user can loot friendly actors during death animation if (canLoot && !stats.getAiSequence().isInCombat()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); // otherwise wait until death animation if(stats.isDeathAnimationFinished()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); } else if (!stats.getAiSequence().isInCombat()) { if (stats.getKnockedDown() || MWBase::Environment::get().getMechanicsManager()->isSneaking(actor)) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing + return std::make_unique(ptr); // stealing // Can't talk to werewolves if (!getNpcStats(ptr).isWerewolf()) - return std::shared_ptr(new MWWorld::ActionTalk(ptr)); + return std::make_unique(ptr); } else // In combat { const bool stealingInCombat = Settings::Manager::getBool ("always allow stealing from knocked out actors", "Game"); if (stealingInCombat && stats.getKnockedDown()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing + 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::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); - return std::shared_ptr (new MWWorld::FailedAction("")); + return std::make_unique(); } MWWorld::ContainerStore& Npc::getContainerStore (const MWWorld::Ptr& ptr) @@ -940,7 +957,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::CreatureStats& stats = getCreatureStats(ptr); + const MWMechanics::NpcStats& stats = getNpcStats(ptr); bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) return 0.f; @@ -948,8 +965,7 @@ namespace MWClass const MWBase::World *world = MWBase::Environment::get().getWorld(); const GMST& gmst = getGmst(); - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); - const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); + const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); @@ -965,7 +981,7 @@ namespace MWClass else if(mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { - float flySpeed = 0.01f*(npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + + float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed*(gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; @@ -979,7 +995,7 @@ namespace MWClass else moveSpeed = getWalkSpeed(ptr); - if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) + if(stats.isWerewolf() && running && stats.getDrawState() == MWMechanics::DrawState::Nothing) moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat(); return moveSpeed; @@ -990,14 +1006,13 @@ namespace MWClass if(getEncumbrance(ptr) > getCapacity(ptr)) return 0.f; - const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + const MWMechanics::NpcStats& stats = getNpcStats(ptr); bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) return 0.f; - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const GMST& gmst = getGmst(); - const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const float encumbranceTerm = gmst.fJumpEncumbranceBase->mValue.getFloat() + gmst.fJumpEncumbranceMultiplier->mValue.getFloat() * (1.0f - Npc::getNormalizedEncumbrance(ptr)); @@ -1018,7 +1033,7 @@ namespace MWClass if(stats.getStance(MWMechanics::CreatureStats::Stance_Run)) x *= gmst.fJumpRunMultiplier->mValue.getFloat(); - x *= npcdata->mNpcStats.getFatigueTerm(); + x *= stats.getFatigueTerm(); x -= -Constants::GravityConst * Constants::UnitsPerMeter; x /= 3.0f; @@ -1039,12 +1054,6 @@ namespace MWClass return (ref->mBase->mFlags & ESM::NPC::Essential) != 0; } - void Npc::registerSelf() - { - std::shared_ptr instance (new Npc); - registerClass (typeid (ESM::NPC).name(), instance); - } - bool Npc::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) @@ -1100,11 +1109,14 @@ namespace MWClass return getNpcStats(ptr).isWerewolf() ? 0.0f : Actor::getEncumbrance(ptr); } - bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const + bool Npc::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const { - MWMechanics::CastSpell cast(ptr, ptr); - return cast.cast(id); + MWBase::Environment::get().getWorld()->breakInvisibility(actor); + MWMechanics::CastSpell cast(actor, actor); + std::string recordId = consumable.getCellRef().getRefId(); + MWBase::Environment::get().getLuaManager()->itemConsumed(consumable, actor); + actor.getClass().getContainerStore(actor).remove(consumable, 1, actor); + return cast.cast(recordId); } void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const @@ -1140,7 +1152,7 @@ namespace MWClass for(int i = 0;i < MWWorld::InventoryStore::Slots;i++) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(i); - if (it == invStore.end() || it->getTypeName() != typeid(ESM::Armor).name()) + if (it == invStore.end() || it->getType() != ESM::Armor::sRecordId) { // unarmored ratings[i] = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); @@ -1237,7 +1249,7 @@ namespace MWClass const MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); - if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) + if(boots == inv.end() || boots->getType() != ESM::Armor::sRecordId) return (name == "left") ? "FootBareLeft" : "FootBareRight"; switch(boots->getClass().getEquipmentSkill(*boots)) @@ -1297,20 +1309,24 @@ namespace MWClass if (!state.mHasCustomState) return; + const ESM::NpcState& npcState = state.asNpcState(); + if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { - // Create a CustomData, but don't fill it from ESM records (not needed) - std::unique_ptr data (new NpcCustomData); - ptr.getRefData().setCustomData (data.release()); + 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()); } } 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(); - const ESM::NpcState& npcState = state.asNpcState(); + customData.mInventoryStore.readState (npcState.mInventory); customData.mNpcStats.readState (npcState.mNpcStats); bool spellsInitialised = customData.mNpcStats.getSpells().setSpells(ptr.get()->mBase->mId); @@ -1391,12 +1407,12 @@ namespace MWClass } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } @@ -1437,7 +1453,7 @@ namespace MWClass return ref->mBase->getFactionRank(); } - void Npc::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Npc::setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } @@ -1450,12 +1466,12 @@ namespace MWClass float Npc::getWalkSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); - const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); + const MWMechanics::NpcStats& stats = getNpcStats(ptr); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); const bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); float walkSpeed = gmst.fMinWalkSpeed->mValue.getFloat() - + 0.01f * npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified() * (gmst.fMaxWalkSpeed->mValue.getFloat() - gmst.fMinWalkSpeed->mValue.getFloat()); walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat()*normalizedEncumbrance; walkSpeed = std::max(0.0f, walkSpeed); @@ -1475,27 +1491,14 @@ namespace MWClass float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { - const GMST& gmst = getGmst(); const MWBase::World* world = MWBase::Environment::get().getWorld(); - const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); - const MWMechanics::MagicEffects& mageffects = npcdata->mNpcStats.getMagicEffects(); + const MWMechanics::NpcStats& stats = getNpcStats(ptr); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const bool swimming = world->isSwimming(ptr); const bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); const bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run) && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); - float swimSpeed; - - if (running) - swimSpeed = getRunSpeed(ptr); - else - swimSpeed = getWalkSpeed(ptr); - - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); - swimSpeed *= gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat(); - - return swimSpeed; + return getSwimSpeedImpl(ptr, getGmst(), mageffects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 612763d12e..264612944e 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWCLASS_NPC_H #define GAME_MWCLASS_NPC_H +#include "../mwworld/registeredclass.hpp" + #include "actor.hpp" namespace ESM @@ -10,8 +12,12 @@ namespace ESM namespace MWClass { - class Npc : public Actor + class Npc : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Npc(); + void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; @@ -77,7 +83,7 @@ namespace MWClass 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(). - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -104,11 +110,7 @@ namespace MWClass float getArmorRating (const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor - bool apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const override; - ///< Apply \a id on \a ptr. - /// \param actor Actor that is resposible for the ID being applied to \a ptr. - /// \return Any effect? + 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 @@ -125,8 +127,6 @@ namespace MWClass std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; float getSkill(const MWWorld::Ptr& ptr, int skill) const override; @@ -162,7 +162,7 @@ namespace MWClass std::string getPrimaryFaction(const MWWorld::ConstPtr &ptr) const override; int getPrimaryFactionRank(const MWWorld::ConstPtr &ptr) const override; - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; + void setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const override; void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 4af97e6345..6e3bf24a5b 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -1,6 +1,8 @@ #include "potion.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -20,8 +22,14 @@ #include "../mwmechanics/alchemy.hpp" +#include "classmodel.hpp" + namespace MWClass { + Potion::Potion() + : MWWorld::RegisteredClass(ESM::Potion::sRecordId) + { + } void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -30,20 +38,9 @@ namespace MWClass } } - void Potion::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Potion::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Potion::getName (const MWWorld::ConstPtr& ptr) const @@ -54,7 +51,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Potion::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Potion::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -75,13 +72,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Potion::registerSelf() - { - std::shared_ptr instance (new Potion); - - registerClass (typeid (ESM::Potion).name(), instance); - } - std::string Potion::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Potion Up"); @@ -131,12 +121,12 @@ namespace MWClass return info; } - std::shared_ptr Potion::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Potion::use (const MWWorld::Ptr& ptr, bool force) const { MWWorld::LiveCellRef *ref = ptr.get(); - std::shared_ptr action ( + std::unique_ptr action ( new MWWorld::ActionApply (ptr, ref->mBase->mId)); action->setSound ("Drink"); diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index 75d923f0ba..d5964cff85 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_POTION_H #define GAME_MWCLASS_POTION_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Potion : public MWWorld::Class + class Potion : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Potion(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -32,11 +34,9 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + std::unique_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index dba4e8c063..e3b22514d5 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -1,6 +1,8 @@ #include "probe.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -18,8 +20,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Probe::Probe() + : MWWorld::RegisteredClass(ESM::Probe::sRecordId) + { + } void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -28,20 +36,9 @@ namespace MWClass } } - void Probe::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Probe::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Probe::getName (const MWWorld::ConstPtr& ptr) const @@ -51,7 +48,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Probe::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Probe::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -81,13 +78,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Probe::registerSelf() - { - std::shared_ptr instance (new Probe); - - registerClass (typeid (ESM::Probe).name(), instance); - } - std::string Probe::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Probe Up"); @@ -132,9 +122,9 @@ namespace MWClass return info; } - std::shared_ptr Probe::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Probe::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index a0a41dcfb6..b048b37f92 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_PROBE_H #define GAME_MWCLASS_PROBE_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Probe : public MWWorld::Class + class Probe : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Probe(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -36,8 +38,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -49,7 +49,7 @@ namespace MWClass std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index 8907c8212e..9da2f7775f 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -1,6 +1,8 @@ #include "repair.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -15,8 +17,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Repair::Repair() + : MWWorld::RegisteredClass(ESM::Repair::sRecordId) + { + } void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -25,20 +33,9 @@ namespace MWClass } } - void Repair::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Repair::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Repair::getName (const MWWorld::ConstPtr& ptr) const @@ -49,7 +46,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Repair::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Repair::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -70,13 +67,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Repair::registerSelf() - { - std::shared_ptr instance (new Repair); - - registerClass (typeid (ESM::Repair).name(), instance); - } - std::string Repair::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Repair Up"); @@ -140,9 +130,9 @@ namespace MWClass return MWWorld::Ptr(cell.insert(ref), &cell); } - std::shared_ptr Repair::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Repair::use (const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionRepair(ptr, force)); + return std::make_unique(ptr, force); } bool Repair::canSell (const MWWorld::ConstPtr& item, int npcServices) const diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index b9791e9cf4..4ca0dd2511 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_REPAIR_H #define GAME_MWCLASS_REPAIR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Repair : public MWWorld::Class + class Repair : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Repair(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,12 +18,10 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -32,8 +34,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -45,7 +45,7 @@ namespace MWClass std::string getModel(const MWWorld::ConstPtr &ptr) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) 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 /// null action). diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 5551b3d731..f587199237 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -1,6 +1,6 @@ #include "static.hpp" -#include +#include #include #include "../mwworld/ptr.hpp" @@ -11,8 +11,14 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwrender/vismask.hpp" +#include "classmodel.hpp" + namespace MWClass { + Static::Static() + : MWWorld::RegisteredClass(ESM::Static::sRecordId) + { + } void Static::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -23,21 +29,19 @@ namespace MWClass } } - void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Static::getModel(const MWWorld::ConstPtr &ptr) const + void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + std::string Static::getModel(const MWWorld::ConstPtr &ptr) const + { + return getClassModel(ptr); } std::string Static::getName (const MWWorld::ConstPtr& ptr) const @@ -50,13 +54,6 @@ namespace MWClass return false; } - void Static::registerSelf() - { - std::shared_ptr instance (new Static); - - registerClass (typeid (ESM::Static).name(), instance); - } - MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index 6bc783dad0..6dbeb46662 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -1,12 +1,16 @@ #ifndef GAME_MWCLASS_STATIC_H #define GAME_MWCLASS_STATIC_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Static : public MWWorld::Class + class Static : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + + Static(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: @@ -14,7 +18,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. @@ -22,8 +27,6 @@ namespace MWClass bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - static void registerSelf(); - std::string getModel(const MWWorld::ConstPtr &ptr) const override; }; } diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 0d6a27cf60..43bb2b110d 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -1,6 +1,8 @@ #include "weapon.hpp" -#include +#include + +#include #include #include @@ -24,8 +26,14 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" + namespace MWClass { + Weapon::Weapon() + : MWWorld::RegisteredClass(ESM::Weapon::sRecordId) + { + } void Weapon::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -34,20 +42,9 @@ namespace MWClass } } - void Weapon::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const - { - // TODO: add option somewhere to enable collision for placeable objects - } - std::string Weapon::getModel(const MWWorld::ConstPtr &ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } std::string Weapon::getName (const MWWorld::ConstPtr& ptr) const @@ -58,7 +55,7 @@ namespace MWClass return !name.empty() ? name : ref->mBase->mId; } - std::shared_ptr Weapon::activate (const MWWorld::Ptr& ptr, + std::unique_ptr Weapon::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); @@ -126,13 +123,6 @@ namespace MWClass return ref->mBase->mData.mValue; } - void Weapon::registerSelf() - { - std::shared_ptr instance (new Weapon); - - registerClass (typeid (ESM::Weapon).name(), instance); - } - std::string Weapon::getUpSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); @@ -274,7 +264,7 @@ namespace MWClass const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Weapon newItem = *ref->mBase; - newItem.mId=""; + newItem.mId.clear(); newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; @@ -307,9 +297,9 @@ namespace MWClass return std::make_pair(1, ""); } - std::shared_ptr Weapon::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Weapon::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index f1824b7d14..f09c590183 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -1,26 +1,27 @@ #ifndef GAME_MWCLASS_WEAPON_H #define GAME_MWCLASS_WEAPON_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Weapon : public MWWorld::Class + class Weapon : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: + Weapon(); void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, + std::unique_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -47,8 +48,6 @@ namespace MWClass int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id @@ -68,7 +67,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; + 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; diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 34fd5828fd..a8ea3fdf02 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -2,13 +2,14 @@ #include #include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include @@ -52,8 +53,9 @@ namespace MWDialogue , mCompilerContext (MWScript::CompilerContext::Type_Dialogue) , mErrorHandler() , mTalkedTo(false) - , mTemporaryDispositionChange(0.f) - , mPermanentDispositionChange(0.f) + , mOriginalDisposition(0) + , mCurrentDisposition(0) + , mPermanentDispositionChange(0) { mChoice = -1; mIsInChoice = false; @@ -65,18 +67,20 @@ namespace MWDialogue { mKnownTopics.clear(); mTalkedTo = false; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; mPermanentDispositionChange = 0; } - void DialogueManager::addTopic (const std::string& topic) + void DialogueManager::addTopic(std::string_view topic) { mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) ); } - void DialogueManager::parseText (const std::string& text) + std::vector DialogueManager::parseTopicIdsFromText (const std::string& text) { - updateActorKnownTopics(); + std::vector topicIdList; + std::vector hypertext = HyperTextParser::parseHyperText(text); for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) @@ -93,11 +97,37 @@ namespace MWDialogue topicId = mTranslationDataStorage.topicStandardForm(topicId); } + topicIdList.push_back(topicId); + } + + return topicIdList; + } + + void DialogueManager::addTopicsFromText (const std::string& text) + { + updateActorKnownTopics(); + + for (const auto& topicId : parseTopicIdsFromText(text)) + { if (mActorKnownTopics.count( topicId )) mKnownTopics.insert( topicId ); } } + void DialogueManager::updateOriginalDisposition() + { + if(mActor.getClass().isNpc()) + { + const auto& stats = mActor.getClass().getNpcStats(mActor); + // Disposition changed by script; discard our preconceived notions + if(stats.getBaseDisposition() != mCurrentDisposition) + { + mCurrentDisposition = stats.getBaseDisposition(); + mOriginalDisposition = mCurrentDisposition; + } + } + } + bool DialogueManager::startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) { updateGlobals(); @@ -106,9 +136,8 @@ namespace MWDialogue if (actor.getClass().getCreatureStats(actor).isDead()) return false; - mLastTopic = ""; - mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + mLastTopic.clear(); + // Note that we intentionally don't reset mPermanentDispositionChange mChoice = -1; mIsInChoice = false; @@ -121,7 +150,6 @@ namespace MWDialogue mTalkedTo = creatureStats.hasTalkedToPlayer(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); //greeting const MWWorld::Store &dialogs = @@ -148,7 +176,7 @@ namespace MWDialogue executeScript (info->mResultScript, mActor); mLastTopic = it->mId; - parseText (info->mResponse); + addTopicsFromText (info->mResponse); return true; } @@ -262,7 +290,10 @@ namespace MWDialogue const ESM::Dialogue& dialogue = *dialogues.find (topic); - const ESM::DialInfo* info = filter.search(dialogue, true); + const ESM::DialInfo* info = + mChoice == -1 && mActorKnownTopics.count(topic) ? + mActorKnownTopics[topic].mInfo : filter.search(dialogue, true); + if (info) { std::string title; @@ -305,7 +336,7 @@ namespace MWDialogue executeScript (info->mResultScript, mActor); - parseText (info->mResponse); + addTopicsFromText (info->mResponse); } } @@ -324,7 +355,6 @@ namespace MWDialogue updateGlobals(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get(); @@ -339,21 +369,41 @@ namespace MWDialogue if (answer != nullptr) { - int flag = 0; + int topicFlags = 0; if(!inJournal(topicId, answer->mId)) { // Does this dialogue contains some actor-specific answer? - if (answer->mActor == mActor.getCellRef().getRefId()) - flag |= MWBase::DialogueManager::TopicType::Specific; + if (Misc::StringUtils::ciEqual(answer->mActor, mActor.getCellRef().getRefId())) + topicFlags |= MWBase::DialogueManager::TopicType::Specific; } else - flag |= MWBase::DialogueManager::TopicType::Exhausted; - mActorKnownTopics.insert (dialog.mId); - mActorKnownTopicsFlag[dialog.mId] = flag; + topicFlags |= MWBase::DialogueManager::TopicType::Exhausted; + mActorKnownTopics.insert (std::make_pair(dialog.mId, ActorKnownTopicInfo {topicFlags, answer})); } } } + + // If response to a topic leads to a new topic, the original topic is not exhausted. + + for (auto& [dialogId, topicInfo] : mActorKnownTopics) + { + // If the topic is not marked as exhausted, we don't need to do anything about it. + // If the topic will not be shown to the player, the flag actually does not matter. + + if (!(topicInfo.mFlags & MWBase::DialogueManager::TopicType::Exhausted) || + !mKnownTopics.count(dialogId)) + continue; + + for (const auto& topicId : parseTopicIdsFromText(topicInfo.mInfo->mResponse)) + { + if (mActorKnownTopics.count( topicId ) && !mKnownTopics.count( topicId )) + { + topicInfo.mFlags &= ~MWBase::DialogueManager::TopicType::Exhausted; + break; + } + } + } } std::list DialogueManager::getAvailableTopics() @@ -362,7 +412,7 @@ namespace MWDialogue std::list keywordList; - for (const std::string& topic : mActorKnownTopics) + for (const auto& [topic, topicInfo] : mActorKnownTopics) { //does the player know the topic? if (mKnownTopics.count(topic)) @@ -376,7 +426,7 @@ namespace MWDialogue int DialogueManager::getTopicFlag(const std::string& topicId) { - return mActorKnownTopicsFlag[topicId]; + return mActorKnownTopics[topicId].mFlags; } void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback) @@ -398,19 +448,21 @@ namespace MWDialogue void DialogueManager::goodbyeSelected() { - // Apply disposition change to NPC's base disposition - if (mActor.getClass().isNpc()) + // Apply disposition change to NPC's base disposition if we **think** we need to change something + if ((mPermanentDispositionChange || mOriginalDisposition != mCurrentDisposition) && mActor.getClass().isNpc()) { - // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mPermanentDispositionChange < 0) - mPermanentDispositionChange = -curDisp; - + updateOriginalDisposition(); MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); - npcStats.setBaseDisposition(static_cast(npcStats.getBaseDisposition() + mPermanentDispositionChange)); + // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) + npcStats.setBaseDisposition(0); + int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); + + npcStats.setBaseDisposition(disposition); } mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; } void DialogueManager::questionAnswered (int answer, ResponseCallback* callback) @@ -427,7 +479,7 @@ namespace MWDialogue if (const ESM::DialInfo *info = filter.search (*dialogue, true)) { std::string text = info->mResponse; - parseText (text); + addTopicsFromText (text); mChoice = -1; mIsInChoice = false; @@ -465,7 +517,7 @@ namespace MWDialogue updateActorKnownTopics(); } - void DialogueManager::addChoice (const std::string& text, int choice) + void DialogueManager::addChoice(std::string_view text, int choice) { mIsInChoice = true; mChoices.emplace_back(text, choice); @@ -490,20 +542,17 @@ namespace MWDialogue void DialogueManager::persuade(int type, ResponseCallback* callback) { bool success; - float temp, perm; + int temp, perm; MWBase::Environment::get().getMechanicsManager()->getPersuasionDispositionChange( mActor, MWBase::MechanicsManager::PersuasionType(type), success, temp, perm); - mTemporaryDispositionChange += temp; + updateOriginalDisposition(); + if(temp > 0 && perm > 0 && mOriginalDisposition + perm + mPermanentDispositionChange < 0) + perm = -(mOriginalDisposition + mPermanentDispositionChange); + mCurrentDisposition += temp; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); mPermanentDispositionChange += perm; - // change temp disposition so that final disposition is between 0...100 - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mTemporaryDispositionChange < 0) - mTemporaryDispositionChange = -curDisp; - else if (curDisp + mTemporaryDispositionChange > 100) - mTemporaryDispositionChange = 100 - curDisp; - MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); @@ -539,16 +588,16 @@ namespace MWDialogue executeTopic (text + (success ? " Success" : " Fail"), callback); } - int DialogueManager::getTemporaryDispositionChange() const - { - return static_cast(mTemporaryDispositionChange); - } - void DialogueManager::applyBarterDispositionChange(int delta) { - mTemporaryDispositionChange += delta; - if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) - mPermanentDispositionChange += delta; + if(mActor.getClass().isNpc()) + { + updateOriginalDisposition(); + mCurrentDisposition += delta; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); + if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) + mPermanentDispositionChange += delta; + } } bool DialogueManager::checkServiceRefused(ResponseCallback* callback, ServiceType service) @@ -565,7 +614,7 @@ namespace MWDialogue { const ESM::DialInfo* info = infos[0]; - parseText (info->mResponse); + addTopicsFromText (info->mResponse); const MWWorld::Store& gmsts = MWBase::Environment::get().getWorld()->getStore().get(); @@ -628,11 +677,8 @@ namespace MWDialogue { ESM::DialogueState state; - for (std::set::const_iterator iter (mKnownTopics.begin()); - iter!=mKnownTopics.end(); ++iter) - { - state.mKnownTopics.push_back (*iter); - } + state.mKnownTopics.reserve(mKnownTopics.size()); + std::copy(mKnownTopics.begin(), mKnownTopics.end(), std::back_inserter(state.mKnownTopics)); state.mChangedFactionReaction = mChangedFactionReaction; @@ -659,7 +705,7 @@ namespace MWDialogue } } - void DialogueManager::modFactionReaction(const std::string &faction1, const std::string &faction2, int diff) + void DialogueManager::modFactionReaction(std::string_view faction1, std::string_view faction2, int diff) { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); @@ -674,7 +720,7 @@ namespace MWDialogue map[fact2] = newValue; } - void DialogueManager::setFactionReaction(const std::string &faction1, const std::string &faction2, int absolute) + void DialogueManager::setFactionReaction(std::string_view faction1, std::string_view faction2, int absolute) { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); @@ -687,7 +733,7 @@ namespace MWDialogue map[fact2] = absolute; } - int DialogueManager::getFactionReaction(const std::string &faction1, const std::string &faction2) const + int DialogueManager::getFactionReaction(std::string_view faction1, std::string_view faction2) const { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index b35bee6d43..5d0c5279d2 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwworld/ptr.hpp" @@ -24,14 +25,19 @@ namespace MWDialogue { class DialogueManager : public MWBase::DialogueManager { + struct ActorKnownTopicInfo + { + int mFlags; + const ESM::DialInfo* mInfo; + }; + std::set mKnownTopics;// Those are the topics the player knows. // Modified faction reactions. > typedef std::map > ModFactionReactionMap; ModFactionReactionMap mChangedFactionReaction; - std::set mActorKnownTopics; - std::unordered_map mActorKnownTopicsFlag; + std::map mActorKnownTopics; Translation::Storage& mTranslationDataStorage; MWScript::CompilerContext mCompilerContext; @@ -47,10 +53,12 @@ namespace MWDialogue std::vector > mChoices; - float mTemporaryDispositionChange; - float mPermanentDispositionChange; + int mOriginalDisposition; + int mCurrentDisposition; + int mPermanentDispositionChange; - void parseText (const std::string& text); + std::vector parseTopicIdsFromText (const std::string& text); + void addTopicsFromText (const std::string& text); void updateActorKnownTopics(); void updateGlobals(); @@ -62,6 +70,8 @@ namespace MWDialogue const ESM::Dialogue* searchDialogue(const std::string& id); + void updateOriginalDisposition(); + public: DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); @@ -77,9 +87,9 @@ namespace MWDialogue bool inJournal (const std::string& topicId, const std::string& infoId) override; - void addTopic (const std::string& topic) override; + void addTopic(std::string_view topic) override; - void addChoice (const std::string& text,int choice) override; + void addChoice(std::string_view text,int choice) override; const std::vector >& getChoices() override; bool isGoodbye() override; @@ -96,7 +106,6 @@ namespace MWDialogue void questionAnswered (int answer, ResponseCallback* callback) override; void persuade (int type, ResponseCallback* callback) override; - int getTemporaryDispositionChange () const override; /// @note Controlled by an option, gets discarded when dialogue ends by default void applyBarterDispositionChange (int delta) override; @@ -108,12 +117,12 @@ namespace MWDialogue void readRecord (ESM::ESMReader& reader, uint32_t type) override; /// Changes faction1's opinion of faction2 by \a diff. - void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) override; + void modFactionReaction (std::string_view faction1, std::string_view faction2, int diff) override; - void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) override; + void setFactionReaction (std::string_view faction1, std::string_view faction2, int absolute) override; /// @return faction1's opinion of faction2 - int getFactionReaction (const std::string& faction1, const std::string& faction2) const override; + int getFactionReaction (std::string_view faction1, std::string_view faction2) 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 a3c326ab8f..b855d75238 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -23,7 +23,7 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); // actor id if (!info.mActor.empty()) @@ -160,7 +160,7 @@ bool MWDialogue::Filter::testSelectStructs (const ESM::DialInfo& info) const bool MWDialogue::Filter::testDisposition (const ESM::DialInfo& info, bool invert) const { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); if (isCreature) return true; @@ -207,7 +207,7 @@ bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& sele bool MWDialogue::Filter::testSelectStruct (const SelectWrapper& select) const { - if (select.isNpcOnly() && (mActor.getTypeName() != typeid (ESM::NPC).name())) + if (select.isNpcOnly() && (mActor.getType() != ESM::NPC::sRecordId)) // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; @@ -257,11 +257,7 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c case SelectWrapper::Function_PcHealthPercent: { MWWorld::Ptr player = MWMechanics::getPlayer(); - - float ratio = player.getClass().getCreatureStats (player).getHealth().getCurrent() / - player.getClass().getCreatureStats (player).getHealth().getModified(); - - return select.selectCompare (static_cast(ratio*100)); + return select.selectCompare(static_cast(player.getClass().getCreatureStats(player).getHealth().getRatio() * 100)); } case SelectWrapper::Function_PcDynamicStat: @@ -276,10 +272,7 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c case SelectWrapper::Function_HealthPercent: { - float ratio = mActor.getClass().getCreatureStats (mActor).getHealth().getCurrent() / - mActor.getClass().getCreatureStats (mActor).getHealth().getModified(); - - return select.selectCompare (static_cast(ratio*100)); + return select.selectCompare(static_cast(mActor.getClass().getCreatureStats(mActor).getHealth().getRatio() * 100)); } default: @@ -316,7 +309,7 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con case SelectWrapper::Function_AiSetting: return mActor.getClass().getCreatureStats (mActor).getAiSetting ( - (MWMechanics::CreatureStats::AiSetting)select.getArgument()).getModified(); + (MWMechanics::AiSetting)select.getArgument()).getModified(false); case SelectWrapper::Function_PcAttribute: @@ -452,7 +445,7 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con { if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) return 2; - if (target.getTypeName() == typeid(ESM::Creature).name()) + if (target.getType() == ESM::Creature::sRecordId) return 1; } } diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp index 28e450e2b2..732cdb1f8f 100644 --- a/apps/openmw/mwdialogue/hypertextparser.cpp +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -1,4 +1,4 @@ -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -17,7 +17,7 @@ namespace MWDialogue std::vector parseHyperText(const std::string & text) { std::vector result; - size_t pos_end, iteration_pos = 0; + size_t pos_end = std::string::npos, iteration_pos = 0; for(;;) { size_t pos_begin = text.find('@', iteration_pos); @@ -47,18 +47,8 @@ namespace MWDialogue void tokenizeKeywords(const std::string & text, std::vector & tokens) { - const MWWorld::Store & dialogs = - MWBase::Environment::get().getWorld()->getStore().get(); - - std::list keywordList; - for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) - keywordList.push_back(Misc::StringUtils::lowerCase(it->mId)); - keywordList.sort(Misc::StringUtils::ciLess); - - KeywordSearch keywordSearch; - - for (std::list::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it) - keywordSearch.seed(*it, 0 /*unused*/); + const auto& keywordSearch = + MWBase::Environment::get().getWorld()->getStore().get().getDialogIdKeywordSearch(); std::vector::Match> matches; keywordSearch.highlightKeywords(text.begin(), text.end(), matches); diff --git a/apps/openmw/mwdialogue/journalentry.cpp b/apps/openmw/mwdialogue/journalentry.cpp index 5eab6d5cae..7af71d034e 100644 --- a/apps/openmw/mwdialogue/journalentry.cpp +++ b/apps/openmw/mwdialogue/journalentry.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include @@ -16,8 +16,6 @@ namespace MWDialogue { - Entry::Entry() {} - Entry::Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) : mInfoId (infoId) { @@ -60,8 +58,6 @@ namespace MWDialogue } - JournalEntry::JournalEntry() {} - JournalEntry::JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) : Entry (topic, infoId, actor), mTopic (topic) {} diff --git a/apps/openmw/mwdialogue/journalentry.hpp b/apps/openmw/mwdialogue/journalentry.hpp index 8711ab53a7..af09908891 100644 --- a/apps/openmw/mwdialogue/journalentry.hpp +++ b/apps/openmw/mwdialogue/journalentry.hpp @@ -22,7 +22,7 @@ namespace MWDialogue std::string mText; std::string mActorName; // optional - Entry(); + Entry() = default; /// actor is optional Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); @@ -41,7 +41,7 @@ namespace MWDialogue { std::string mTopic; - JournalEntry(); + JournalEntry() = default; JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index b219516183..41b30a95ce 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -2,10 +2,12 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include + +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -93,7 +95,16 @@ namespace MWDialogue StampedJournalEntry entry = StampedJournalEntry::makeFromQuest (id, index, actor); Quest& quest = getQuest (id); - quest.addEntry (entry); // we are doing slicing on purpose here + if(quest.addEntry(entry)) // we are doing slicing on purpose here + { + // Restart all "other" quests with the same name as well + std::string name = quest.getName(); + for(auto& it : mQuests) + { + if(it.second.isFinished() && Misc::StringUtils::ciEqual(it.second.getName(), name)) + it.second.setFinished(false); + } + } // there is no need to show empty entries in journal if (!entry.getText().empty()) diff --git a/apps/openmw/mwdialogue/journalimp.hpp b/apps/openmw/mwdialogue/journalimp.hpp index a6501e54e1..0c778d2ba2 100644 --- a/apps/openmw/mwdialogue/journalimp.hpp +++ b/apps/openmw/mwdialogue/journalimp.hpp @@ -3,7 +3,6 @@ #include "../mwbase/journal.hpp" -#include "journalentry.hpp" #include "quest.hpp" namespace MWDialogue diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index f296f223fb..3f932084fe 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,8 +1,8 @@ #ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H -#include #include +#include #include #include #include // std::reverse @@ -30,7 +30,7 @@ public: { if (keyword.empty()) return; - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), 0, mRoot); + seed_impl (std::move(keyword), std::move (value), 0, mRoot); } void clear () @@ -39,7 +39,7 @@ public: mRoot.mKeyword.clear (); } - bool containsKeyword (string_t keyword, value_t& value) + bool containsKeyword (const string_t& keyword, value_t& value) { typename Entry::childen_t::iterator current; typename Entry::childen_t::iterator next; @@ -68,28 +68,19 @@ public: return false; } + static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; } - void highlightKeywords (Point beg, Point end, std::vector& out) + void highlightKeywords (Point beg, Point end, std::vector& out) const { std::vector matches; for (Point i = beg; i != end; ++i) { - // check if previous character marked start of new word - if (i != beg) - { - Point prev = i; - --prev; - if(isalpha(*prev)) - continue; - } - - // check first character - typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i)); + typename Entry::childen_t::const_iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i)); // no match, on to next character if (candidate == mRoot.mChildren.end ()) @@ -100,11 +91,11 @@ public: // 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< typename std::pair > candidates; + std::vector< typename std::pair > candidates; while ((j + 1) != end) { - typename Entry::childen_t::iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j)); + typename Entry::childen_t::const_iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j)); if (next == candidate->second.mChildren.end ()) { @@ -125,7 +116,7 @@ public: // shorter candidates will be added to the vector first. however, we want to check against longer candidates first std::reverse(candidates.begin(), candidates.end()); - for (typename std::vector< std::pair >::iterator it = candidates.begin(); + for (typename std::vector< std::pair >::iterator it = candidates.begin(); it != candidates.end(); ++it) { candidate = it->second; @@ -218,8 +209,8 @@ private: if (j == entry.mChildren.end ()) { - entry.mChildren [ch].mValue = /*std::move*/ (value); - entry.mChildren [ch].mKeyword = /*std::move*/ (keyword); + entry.mChildren [ch].mValue = std::move (value); + entry.mChildren [ch].mKeyword = std::move (keyword); } else { @@ -228,22 +219,22 @@ private: if (keyword == j->second.mKeyword) throw std::runtime_error ("duplicate keyword inserted"); - value_t pushValue = /*std::move*/ (j->second.mValue); - string_t pushKeyword = /*std::move*/ (j->second.mKeyword); + value_t pushValue = j->second.mValue; + string_t pushKeyword = j->second.mKeyword; if (depth >= pushKeyword.size ()) throw std::runtime_error ("unexpected"); if (depth+1 < pushKeyword.size()) { - seed_impl (/*std::move*/ (pushKeyword), /*std::move*/ (pushValue), depth+1, j->second); + seed_impl (std::move (pushKeyword), std::move (pushValue), depth+1, j->second); j->second.mKeyword.clear (); } } if (depth+1 == keyword.size()) j->second.mKeyword = value; else // depth+1 < keyword.size() - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), depth+1, j->second); + seed_impl (std::move (keyword), std::move (value), depth+1, j->second); } } diff --git a/apps/openmw/mwdialogue/quest.cpp b/apps/openmw/mwdialogue/quest.cpp index 5f20a8abb2..ce05676965 100644 --- a/apps/openmw/mwdialogue/quest.cpp +++ b/apps/openmw/mwdialogue/quest.cpp @@ -1,6 +1,8 @@ #include "quest.hpp" -#include +#include + +#include #include "../mwworld/esmstore.hpp" @@ -50,42 +52,33 @@ namespace MWDialogue return mFinished; } - void Quest::addEntry (const JournalEntry& entry) + void Quest::setFinished(bool finished) { - int index = -1; + mFinished = finished; + } + bool Quest::addEntry (const JournalEntry& entry) + { const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().find (entry.mTopic); - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) - if (iter->mId == entry.mInfoId) - { - index = iter->mData.mJournalIndex; - break; - } + auto info = std::find_if(dialogue->mInfo.begin(), dialogue->mInfo.end(), [&](const auto& info) { return info.mId == entry.mInfoId; }); - if (index==-1) + if (info == dialogue->mInfo.end() || info->mData.mJournalIndex == -1) throw std::runtime_error ("unknown journal entry for topic " + mTopic); - for (auto &info : dialogue->mInfo) - { - if (info.mData.mJournalIndex == index - && (info.mQuestStatus == ESM::DialInfo::QS_Finished || info.mQuestStatus == ESM::DialInfo::QS_Restart)) - { - mFinished = (info.mQuestStatus == ESM::DialInfo::QS_Finished); - break; - } - } + if (info->mQuestStatus == ESM::DialInfo::QS_Finished || info->mQuestStatus == ESM::DialInfo::QS_Restart) + mFinished = info->mQuestStatus == ESM::DialInfo::QS_Finished; - if (index > mIndex) - mIndex = index; + if (info->mData.mJournalIndex > mIndex) + mIndex = info->mData.mJournalIndex; for (TEntryIter iter (mEntries.begin()); iter!=mEntries.end(); ++iter) if (iter->mInfoId==entry.mInfoId) - return; + return info->mQuestStatus == ESM::DialInfo::QS_Restart; mEntries.push_back (entry); // we want slicing here + return info->mQuestStatus == ESM::DialInfo::QS_Restart; } void Quest::write (ESM::QuestState& state) const diff --git a/apps/openmw/mwdialogue/quest.hpp b/apps/openmw/mwdialogue/quest.hpp index 712f94fae4..53b4d02f65 100644 --- a/apps/openmw/mwdialogue/quest.hpp +++ b/apps/openmw/mwdialogue/quest.hpp @@ -33,9 +33,10 @@ namespace MWDialogue ///< Calling this function with a non-existent index will throw an exception. bool isFinished() const; + void setFinished(bool finished); - void addEntry (const JournalEntry& entry) override; - ///< Add entry and adjust index accordingly. + bool addEntry (const JournalEntry& entry) override; + ///< Add entry and adjust index accordingly. Returns true if the quest should be restarted. /// /// \note Redundant entries are ignored, but the index is still adjusted. diff --git a/apps/openmw/mwdialogue/scripttest.cpp b/apps/openmw/mwdialogue/scripttest.cpp index c3d7b12c7d..1b192aec15 100644 --- a/apps/openmw/mwdialogue/scripttest.cpp +++ b/apps/openmw/mwdialogue/scripttest.cpp @@ -19,6 +19,8 @@ #include "filter.hpp" +#include + namespace { diff --git a/apps/openmw/mwdialogue/selectwrapper.hpp b/apps/openmw/mwdialogue/selectwrapper.hpp index ef787d8eec..dff484562d 100644 --- a/apps/openmw/mwdialogue/selectwrapper.hpp +++ b/apps/openmw/mwdialogue/selectwrapper.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWDIALOGUE_SELECTWRAPPER_H #define GAME_MWDIALOGUE_SELECTWRAPPER_H -#include +#include namespace MWDialogue { diff --git a/apps/openmw/mwdialogue/topic.cpp b/apps/openmw/mwdialogue/topic.cpp index eb7fbdc1de..9d56028184 100644 --- a/apps/openmw/mwdialogue/topic.cpp +++ b/apps/openmw/mwdialogue/topic.cpp @@ -18,7 +18,7 @@ namespace MWDialogue Topic::~Topic() {} - void Topic::addEntry (const JournalEntry& entry) + bool Topic::addEntry (const JournalEntry& entry) { if (entry.mTopic!=mTopic) throw std::runtime_error ("topic does not match: " + mTopic); @@ -27,10 +27,11 @@ namespace MWDialogue for (Topic::TEntryIter it = mEntries.begin(); it != mEntries.end(); ++it) { if (it->mInfoId == entry.mInfoId) - return; + return false; } mEntries.push_back (entry); // we want slicing here + return false; } void Topic::insertEntry (const ESM::JournalEntry& entry) diff --git a/apps/openmw/mwdialogue/topic.hpp b/apps/openmw/mwdialogue/topic.hpp index 72486ef8af..12df484aa7 100644 --- a/apps/openmw/mwdialogue/topic.hpp +++ b/apps/openmw/mwdialogue/topic.hpp @@ -35,7 +35,7 @@ namespace MWDialogue virtual ~Topic(); - virtual void addEntry (const JournalEntry& entry); + virtual bool addEntry (const JournalEntry& entry); ///< Add entry /// /// \note Redundant entries are ignored. diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index bacd1c7695..fd4a21cf10 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -194,7 +194,7 @@ namespace MWGui for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; - if (item.getTypeName() != typeid(ESM::Ingredient).name()) + if (item.getType() != ESM::Ingredient::sRecordId) continue; itemNames.insert(item.getClass().getName(item)); diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 41711f5e4e..f5fc164ee2 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" @@ -190,7 +193,8 @@ namespace MWGui const ESM::BirthSign *birth = store.get().find(mCurrentBirthId); - mBirthImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctTexturePath(birth->mTexture)); + mBirthImage->setImageTexture(Misc::ResourceHelpers::correctTexturePath(birth->mTexture, + MWBase::Environment::get().getResourceSystem()->getVFS())); std::vector abilities, powers, spells; diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 8a6ec998da..5d8fb898fc 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1,14 +1,15 @@ #include "bookpage.hpp" +#include + #include "MyGUI_RenderItem.h" #include "MyGUI_RenderManager.h" #include "MyGUI_TextureUtility.h" #include "MyGUI_FactoryManager.h" #include +#include -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" namespace MWGui { @@ -105,7 +106,7 @@ struct TypesetBookImpl : TypesetBook virtual ~TypesetBookImpl () {} - Range addContent (BookTypesetter::Utf8Span text) + Range addContent (const BookTypesetter::Utf8Span &text) { Contents::iterator i = mContents.insert (mContents.end (), Content (text.first, text.second)); @@ -488,7 +489,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter { add_partial_text(); stream.consume (); - mLine = nullptr, mRun = nullptr; + mLine = nullptr; + mRun = nullptr; continue; } @@ -548,7 +550,9 @@ struct TypesetBookImpl::Typesetter : BookTypesetter if (left + space_width + word_width > mPageWidth) { - mLine = nullptr, mRun = nullptr, left = 0; + mLine = nullptr; + mRun = nullptr; + left = 0; } else { @@ -744,9 +748,7 @@ namespace mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat(); } - ~GlyphStream () - { - } + ~GlyphStream () = default; MyGUI::Vertex* end () const { return mVertices; } @@ -894,6 +896,21 @@ protected: return mIsPageReset || (mPage != page); } + std::optional getAdjustedPos(int left, int top, bool move = false) + { + if (!mBook) + return {}; + + if (mPage >= mBook->mPages.size()) + return {}; + + MyGUI::IntPoint pos (left, top); + pos.left -= mCroppedParent->getAbsoluteLeft (); + pos.top -= mCroppedParent->getAbsoluteTop (); + pos.top += mViewTop; + return pos; + } + public: typedef TypesetBookImpl::StyleImpl Style; @@ -925,7 +942,7 @@ public: void dirtyFocusItem () { - if (mFocusItem != 0) + if (mFocusItem != nullptr) { MyGUI::IFont* Font = mBook->affectedFont (mFocusItem); @@ -946,22 +963,16 @@ public: dirtyFocusItem (); - mFocusItem = 0; + mFocusItem = nullptr; mItemActive = false; } void onMouseMove (int left, int top) { - if (!mBook) - return; - - if (mPage >= mBook->mPages.size()) - return; - - left -= mCroppedParent->getAbsoluteLeft (); - top -= mCroppedParent->getAbsoluteTop (); - - Style * hit = mBook->hitTestWithMargin (left, mViewTop + top); + Style * hit = nullptr; + if(auto pos = getAdjustedPos(left, top, true)) + if(pos->top <= mViewBottom) + hit = mBook->hitTestWithMargin (pos->left, pos->top); if (mLastDown == MyGUI::MouseButton::None) { @@ -976,7 +987,7 @@ public: } } else - if (mFocusItem != 0) + if (mFocusItem != nullptr) { bool newItemActive = hit == mFocusItem; @@ -991,24 +1002,11 @@ public: void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) { - if (!mBook) - return; - - if (mPage >= mBook->mPages.size()) - return; + auto pos = getAdjustedPos(left, top); - // work around inconsistency in MyGUI where the mouse press coordinates aren't - // transformed by the current Layer (even though mouse *move* events are). - MyGUI::IntPoint pos (left, top); -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - pos = mNode->getLayer()->getPosition(left, top); -#endif - pos.left -= mCroppedParent->getAbsoluteLeft (); - pos.top -= mCroppedParent->getAbsoluteTop (); - - if (mLastDown == MyGUI::MouseButton::None) + if (pos && mLastDown == MyGUI::MouseButton::None) { - mFocusItem = mBook->hitTestWithMargin (pos.left, mViewTop + pos.top); + mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; mItemActive = true; dirtyFocusItem (); @@ -1019,25 +1017,11 @@ public: void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) { - if (!mBook) - return; - - if (mPage >= mBook->mPages.size()) - return; + auto pos = getAdjustedPos(left, top); - // work around inconsistency in MyGUI where the mouse release coordinates aren't - // transformed by the current Layer (even though mouse *move* events are). - MyGUI::IntPoint pos (left, top); -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - pos = mNode->getLayer()->getPosition(left, top); -#endif - - pos.left -= mCroppedParent->getAbsoluteLeft (); - pos.top -= mCroppedParent->getAbsoluteTop (); - - if (mLastDown == id) + if (pos && mLastDown == id) { - Style * item = mBook->hitTestWithMargin (pos.left, mViewTop + pos.top); + Style * item = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; bool clicked = mFocusItem == item; @@ -1063,7 +1047,7 @@ public: for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) { - if (mNode != nullptr) + if (mNode != nullptr && i->second != nullptr) i->second->destroyDrawItem (mNode); i->second.reset(); } @@ -1132,7 +1116,7 @@ public: if (j == this_->mActiveTextFormats.end ()) { - std::unique_ptr textFormat(new TextFormat (Font, this_)); + auto textFormat = std::make_unique(Font, this_); textFormat->mTexture = Font->getTextureFont (); @@ -1229,8 +1213,10 @@ public: RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); + float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f; + GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), - -1 /*mNode->getNodeDepth()*/, vertices, renderXform); + z /*mNode->getNodeDepth()*/, vertices, renderXform); int visit_top = (std::max) (mViewTop, mViewTop + int (renderXform.clipTop )); int visit_bottom = (std::min) (mViewBottom, mViewTop + int (renderXform.clipBottom)); diff --git a/apps/openmw/mwgui/bookpage.hpp b/apps/openmw/mwgui/bookpage.hpp index 4e49b8f67e..ab6ef555b0 100644 --- a/apps/openmw/mwgui/bookpage.hpp +++ b/apps/openmw/mwgui/bookpage.hpp @@ -7,10 +7,9 @@ #include #include -#include +#include #include -#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -53,7 +52,7 @@ namespace MWGui { static const int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); - MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch); + const MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch); if (gi) { const float scale = font->getDefaultHeight() / (float) fontHeight; diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 86089051d6..8dbb90ca6d 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 31fe3afb04..5300f00106 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -1,5 +1,7 @@ #include "charactercreation.hpp" +#include + #include #include #include @@ -72,13 +74,6 @@ namespace return {question, {r2, r1, r0}, sound}; } } - - void updatePlayerHealth() - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); - npcStats.updateHealth(); - } } namespace MWGui @@ -87,15 +82,15 @@ namespace MWGui CharacterCreation::CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) , mResourceSystem(resourceSystem) - , mNameDialog(0) - , mRaceDialog(0) - , mClassChoiceDialog(0) - , mGenerateClassQuestionDialog(0) - , mGenerateClassResultDialog(0) - , mPickClassDialog(0) - , mCreateClassDialog(0) - , mBirthSignDialog(0) - , mReviewDialog(0) + , mNameDialog(nullptr) + , mRaceDialog(nullptr) + , mClassChoiceDialog(nullptr) + , mGenerateClassQuestionDialog(nullptr) + , mGenerateClassResultDialog(nullptr) + , mPickClassDialog(nullptr) + , mCreateClassDialog(nullptr) + , mBirthSignDialog(nullptr) + , mReviewDialog(nullptr) , mGenerateClassStep(0) { mCreationStage = CSE_NotStarted; @@ -184,7 +179,7 @@ namespace MWGui { case GM_Name: MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); - mNameDialog = 0; + mNameDialog = nullptr; mNameDialog = new TextInputDialog(); mNameDialog->setTextLabel(MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "Name")); mNameDialog->setTextInput(mPlayerName); @@ -195,7 +190,7 @@ namespace MWGui case GM_Race: MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); - mRaceDialog = 0; + mRaceDialog = nullptr; mRaceDialog = new RaceDialog(mParent, mResourceSystem); mRaceDialog->setNextButtonShow(mCreationStage >= CSE_RaceChosen); mRaceDialog->setRaceId(mPlayerRaceId); @@ -208,7 +203,7 @@ namespace MWGui case GM_Class: MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); - mClassChoiceDialog = 0; + mClassChoiceDialog = nullptr; mClassChoiceDialog = new ClassChoiceDialog(); mClassChoiceDialog->eventButtonSelected += MyGUI::newDelegate(this, &CharacterCreation::onClassChoice); mClassChoiceDialog->setVisible(true); @@ -218,7 +213,7 @@ namespace MWGui case GM_ClassPick: MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); - mPickClassDialog = 0; + mPickClassDialog = nullptr; mPickClassDialog = new PickClassDialog(); mPickClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mPickClassDialog->setClassId(mPlayerClass.mId); @@ -231,7 +226,7 @@ namespace MWGui case GM_Birth: MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); - mBirthSignDialog = 0; + mBirthSignDialog = nullptr; mBirthSignDialog = new BirthDialog(); mBirthSignDialog->setNextButtonShow(mCreationStage >= CSE_BirthSignChosen); mBirthSignDialog->setBirthId(mPlayerBirthSignId); @@ -256,7 +251,7 @@ namespace MWGui break; case GM_ClassGenerate: mGenerateClassStep = 0; - mGenerateClass = ""; + mGenerateClass.clear(); mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; @@ -266,7 +261,7 @@ namespace MWGui break; case GM_Review: MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = 0; + mReviewDialog = nullptr; mReviewDialog = new ReviewDialog(); MWBase::World *world = MWBase::Environment::get().getWorld(); @@ -316,7 +311,7 @@ namespace MWGui void CharacterCreation::onReviewDialogDone(WindowBase* parWindow) { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = 0; + mReviewDialog = nullptr; MWBase::Environment::get().getWindowManager()->popGuiMode(); } @@ -324,7 +319,7 @@ namespace MWGui void CharacterCreation::onReviewDialogBack() { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = 0; + mReviewDialog = nullptr; mCreationStage = CSE_ReviewBack; MWBase::Environment::get().getWindowManager()->popGuiMode(); @@ -334,7 +329,7 @@ namespace MWGui void CharacterCreation::onReviewActivateDialog(int parDialog) { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = 0; + mReviewDialog = nullptr; mCreationStage = CSE_ReviewNext; MWBase::Environment::get().getWindowManager()->popGuiMode(); @@ -370,10 +365,8 @@ namespace MWGui mPlayerClass = *klass; } MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); - mPickClassDialog = 0; + mPickClassDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) @@ -394,7 +387,7 @@ namespace MWGui void CharacterCreation::onClassChoice(int _index) { MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); - mClassChoiceDialog = 0; + mClassChoiceDialog = nullptr; MWBase::Environment::get().getWindowManager()->popGuiMode(); @@ -423,7 +416,7 @@ namespace MWGui mPlayerName = mNameDialog->getTextInput(); MWBase::Environment::get().getMechanicsManager()->setPlayerName(mPlayerName); MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); - mNameDialog = 0; + mNameDialog = nullptr; } handleDialogDone(CSE_NameChosen, GM_Race); @@ -446,10 +439,8 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->getInventoryWindow()->rebuildAvatar(); MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); - mRaceDialog = 0; + mRaceDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() @@ -475,10 +466,8 @@ namespace MWGui if (!mPlayerBirthSignId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerBirthsign(mPlayerBirthSignId); MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); - mBirthSignDialog = 0; + mBirthSignDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) @@ -505,6 +494,7 @@ namespace MWGui klass.mDescription = mCreateClassDialog->getDescription(); klass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); klass.mData.mIsPlayable = 0x1; + klass.mRecordFlags = 0; std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); assert(attributes.size() == 2); @@ -527,7 +517,6 @@ namespace MWGui // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } - updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) @@ -551,7 +540,7 @@ namespace MWGui MWBase::Environment::get().getSoundManager()->stopSay(); MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); - mGenerateClassQuestionDialog = 0; + mGenerateClassQuestionDialog = nullptr; if (_index < 0 || _index >= 3) { @@ -669,7 +658,7 @@ namespace MWGui } MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); - mGenerateClassResultDialog = 0; + mGenerateClassResultDialog = nullptr; mGenerateClassResultDialog = new GenerateClassResultDialog(); mGenerateClassResultDialog->setClassId(mGenerateClass); @@ -687,7 +676,7 @@ namespace MWGui } MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); - mGenerateClassQuestionDialog = 0; + mGenerateClassQuestionDialog = nullptr; mGenerateClassQuestionDialog = new InfoBoxDialog(); @@ -711,7 +700,7 @@ namespace MWGui void CharacterCreation::selectGeneratedClass() { MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); - mGenerateClassResultDialog = 0; + mGenerateClassResultDialog = nullptr; MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); @@ -719,8 +708,6 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); mPlayerClass = *klass; - - updatePlayerHealth(); } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/charactercreation.hpp b/apps/openmw/mwgui/charactercreation.hpp index beb8715fcd..a6c2d10c2f 100644 --- a/apps/openmw/mwgui/charactercreation.hpp +++ b/apps/openmw/mwgui/charactercreation.hpp @@ -1,7 +1,7 @@ #ifndef CHARACTER_CREATION_HPP #define CHARACTER_CREATION_HPP -#include +#include #include #include diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index cfbf5e5fb5..9568ba578a 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -13,6 +13,9 @@ #include "../mwworld/esmstore.hpp" #include +#include +#include +#include #include "tooltips.hpp" @@ -517,6 +520,7 @@ namespace MWGui std::vector CreateClassDialog::getMajorSkills() const { std::vector v; + v.reserve(5); for(int i = 0; i < 5; i++) { v.push_back(mMajorSkill[i]->getSkillId()); @@ -527,6 +531,7 @@ namespace MWGui std::vector CreateClassDialog::getMinorSkills() const { std::vector v; + v.reserve(5); for(int i=0; i < 5; i++) { v.push_back(mMinorSkill[i]->getSkillId()); @@ -550,16 +555,16 @@ namespace MWGui void CreateClassDialog::onDialogCancel() { MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); - mSpecDialog = 0; + mSpecDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); - mAttribDialog = 0; + mAttribDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); - mSkillDialog = 0; + mSkillDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); - mDescDialog = 0; + mDescDialog = nullptr; } void CreateClassDialog::onSpecializationClicked(MyGUI::Widget* _sender) @@ -577,7 +582,7 @@ namespace MWGui setSpecialization(mSpecializationId); MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); - mSpecDialog = 0; + mSpecDialog = nullptr; } void CreateClassDialog::setSpecialization(int id) @@ -618,7 +623,7 @@ namespace MWGui } mAffectedAttribute->setAttributeId(id); MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); - mAttribDialog = 0; + mAttribDialog = nullptr; update(); } @@ -651,7 +656,7 @@ namespace MWGui mAffectedSkill->setSkillId(mSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); - mSkillDialog = 0; + mSkillDialog = nullptr; update(); } @@ -667,7 +672,7 @@ namespace MWGui { mDescription = mDescDialog->getTextInput(); MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); - mDescDialog = 0; + mDescDialog = nullptr; } void CreateClassDialog::onOkClicked(MyGUI::Widget* _sender) @@ -918,8 +923,9 @@ namespace MWGui void setClassImage(MyGUI::ImageBox* imageBox, const std::string &classId) { - std::string classImage = std::string("textures\\levelup\\") + classId + ".dds"; - if (!MWBase::Environment::get().getWindowManager()->textureExists(classImage)) + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + std::string classImage = Misc::ResourceHelpers::correctTexturePath("textures\\levelup\\" + classId + ".dds", vfs); + if (!vfs->exists(classImage)) { Log(Debug::Warning) << "No class image for " << classId << ", falling back to default"; classImage = "textures\\levelup\\warrior.dds"; diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index bb34a05530..f765fa3da2 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -1,8 +1,11 @@ #ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H +#include + #include -#include +#include + #include "widgets.hpp" #include "windowbase.hpp" diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 926456219e..852efa7583 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -2,7 +2,9 @@ #include +#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index e56cd170c8..a382bbeb07 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -4,11 +4,15 @@ #include #include -#include -#include +#include +#include #include #include +#include +#include +#include +#include #include "../mwscript/extensions.hpp" @@ -16,6 +20,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -36,7 +41,7 @@ namespace MWGui ConsoleInterpreterContext::ConsoleInterpreterContext (Console& console, MWWorld::Ptr reference) : MWScript::InterpreterContext ( - reference.isEmpty() ? 0 : &reference.getRefData().getLocals(), reference), + reference.isEmpty() ? nullptr : &reference.getRefData().getLocals(), reference), mConsole (console) {} @@ -157,25 +162,34 @@ namespace MWGui MyGUI::LayerManager::getInstance().upLayerItem(mMainWidget); } - void Console::print(const std::string &msg, const std::string& color) + void Console::print(const std::string &msg, std::string_view color) { - mHistory->addText(color + MyGUI::TextIterator::toTagsString(msg)); + mHistory->addText(std::string(color) + MyGUI::TextIterator::toTagsString(msg)); } void Console::printOK(const std::string &msg) { - print(msg + "\n", "#FF00FF"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Success); } void Console::printError(const std::string &msg) { - print(msg + "\n", "#FF2222"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Error); } void Console::execute (const std::string& command) { // Log the command - print("> " + command + "\n"); + if (mConsoleMode.empty()) + print("> " + command + "\n"); + else + print(mConsoleMode + " " + command + "\n"); + + if (!mConsoleMode.empty() || (command.size() >= 3 && std::string_view(command).substr(0, 3) == "lua")) + { + MWBase::Environment::get().getLuaManager()->handleConsoleCommand(mConsoleMode, command, mPtr); + return; + } Compiler::Locals locals; if (!mPtr.isEmpty()) @@ -206,8 +220,7 @@ namespace MWGui void Console::executeFile (const std::string& path) { - namespace bfs = boost::filesystem; - bfs::ifstream stream ((bfs::path(path))); + std::ifstream stream ((std::filesystem::path(path))); if (!stream.is_open()) printError ("failed to open file: " + path); @@ -249,7 +262,7 @@ namespace MWGui size_t length = mCommandLine->getTextCursor() - max; if(length > 0) { - std::string text = caption; + auto text = caption; text.erase(max, length); mCommandLine->setCaption(text); mCommandLine->setTextCursor(max); @@ -259,14 +272,14 @@ namespace MWGui { if(mCommandLine->getTextCursor() > 0) { - std::string text = mCommandLine->getCaption(); + auto text = mCommandLine->getCaption(); text.erase(0, mCommandLine->getTextCursor()); mCommandLine->setCaption(text); mCommandLine->setTextCursor(0); } } } - else if(key == MyGUI::KeyCode::Tab) + else if(key == MyGUI::KeyCode::Tab && mConsoleMode.empty()) { std::vector matches; listNames(); @@ -331,6 +344,7 @@ namespace MWGui mCommandHistory.push_back(cm); mCurrent = mCommandHistory.end(); mEditString.clear(); + mHistory->setTextCursor(mHistory->getTextLength()); // Reset the command line before the command execution. // It prevents the re-triggering of the acceptCommand() event for the same command @@ -469,7 +483,7 @@ namespace MWGui void Console::onResChange(int width, int height) { - setCoord(10,10, width-10, height/2); + setCoord(10, 10, width-10, height/2); } void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) @@ -483,23 +497,31 @@ namespace MWGui if (!object.isEmpty()) { if (object == mPtr) - { - setTitle("#{sConsoleTitle}"); - mPtr=MWWorld::Ptr(); - } + mPtr = MWWorld::Ptr(); else - { - setTitle("#{sConsoleTitle} (" + object.getCellRef().getRefId() + ")"); mPtr = object; - } // User clicked on an object. Restore focus to the console command line. MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); } else - { - setTitle("#{sConsoleTitle}"); mPtr = MWWorld::Ptr(); - } + updateConsoleTitle(); + } + + void Console::updateConsoleTitle() + { + std::string title = "#{sConsoleTitle}"; + if (!mConsoleMode.empty()) + title = mConsoleMode + " " + title; + if (!mPtr.isEmpty()) + title.append(" (" + mPtr.getCellRef().getRefId() + ")"); + setTitle(title); + } + + void Console::setConsoleMode(std::string_view mode) + { + mConsoleMode = std::string(mode); + updateConsoleTitle(); } void Console::onReferenceUnavailable() diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 52aa42f2af..891c4e0ac7 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -6,12 +6,10 @@ #include #include -#include -#include -#include #include #include -#include + +#include "../mwbase/windowmanager.hpp" #include "../mwscript/compilercontext.hpp" #include "../mwscript/interpretercontext.hpp" @@ -44,7 +42,7 @@ namespace MWGui void onResChange(int width, int height) override; // Print a message to the console, in specified color. - void print(const std::string &msg, const std::string& color = "#FFFFFF"); + void print(const std::string &msg, std::string_view color = MWBase::WindowManager::sConsoleColor_Default); // These are pre-colored versions that you should use. @@ -64,12 +62,19 @@ namespace MWGui void resetReference () override; + const std::string& getConsoleMode() const { return mConsoleMode; } + void setConsoleMode(std::string_view mode); + protected: void onReferenceUnavailable() override; private: + std::string mConsoleMode; + + void updateConsoleTitle(); + void keyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 16b38eaf93..fdfd192cc1 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -12,7 +12,9 @@ #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwmechanics/aipackage.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/summoning.hpp" #include "../mwscript/interpretercontext.hpp" @@ -36,6 +38,7 @@ namespace MWGui , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) + , mTreatNextOpenAsLoot(false) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); getWidget(mTakeButton, "TakeButton"); @@ -119,13 +122,15 @@ namespace MWGui void ContainerWindow::setPtr(const MWWorld::Ptr& container) { + bool lootAnyway = mTreatNextOpenAsLoot; + mTreatNextOpenAsLoot = false; mPtr = container; bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead(); if (mPtr.getClass().hasInventoryStore(mPtr)) { - if (mPtr.getClass().isNpc() && !loot) + if (mPtr.getClass().isNpc() && !loot && !lootAnyway) { // we are stealing stuff mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container), @@ -260,10 +265,28 @@ namespace MWGui } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); + + // Check if we are a summon and inform our master we've bit the dust + for(const auto& package : creatureStats.getAiSequence()) + { + if(package->followTargetThroughDoors() && !package->getTarget().isEmpty()) + { + const auto& summoner = package->getTarget(); + auto& summons = summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap(); + auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); + if(it != summons.end()) + { + auto summon = *it; + summons.erase(it); + MWMechanics::purgeSummonEffect(summoner, summon); + break; + } + } + } } MWBase::Environment::get().getWorld()->deleteObject(ptr); @@ -283,4 +306,9 @@ namespace MWGui return mModel->onTakeItem(item.mBase, count); } + void ContainerWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + if(mModel && mModel->usesContainer(ptr)) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); + } } diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index feda123fb7..66a20e7ef5 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -6,11 +6,6 @@ #include "itemmodel.hpp" -namespace MWWorld -{ - class Environment; -} - namespace MyGUI { class Gui; @@ -19,7 +14,6 @@ namespace MyGUI namespace MWGui { - class WindowManager; class ContainerWindow; class ItemView; class SortFilterItemModel; @@ -41,6 +35,9 @@ namespace MWGui void resetReference() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + + void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; }; private: DragAndDrop* mDragAndDrop; @@ -48,7 +45,7 @@ namespace MWGui SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; - + bool mTreatNextOpenAsLoot; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; MyGUI::Button* mCloseButton; diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index 56f084bb9d..9f202108a2 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -86,7 +86,7 @@ size_t ContainerItemModel::getItemCount() return mItems.size(); } -ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) +ItemModel::ModelIndex ContainerItemModel::getIndex (const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) @@ -209,7 +209,7 @@ bool ContainerItemModel::onDropItem(const MWWorld::Ptr &item, int count) MWWorld::Ptr target = mItemSources[0].first; - if (target.getTypeName() != typeid(ESM::Container).name()) + if (target.getType() != ESM::Container::sRecordId) return true; // check container organic flag @@ -249,4 +249,14 @@ bool ContainerItemModel::onTakeItem(const MWWorld::Ptr &item, int count) return true; } +bool ContainerItemModel::usesContainer(const MWWorld::Ptr& container) +{ + for(const auto& source : mItemSources) + { + if(source.first == container) + return true; + } + return false; +} + } diff --git a/apps/openmw/mwgui/containeritemmodel.hpp b/apps/openmw/mwgui/containeritemmodel.hpp index c54f113147..11fed06913 100644 --- a/apps/openmw/mwgui/containeritemmodel.hpp +++ b/apps/openmw/mwgui/containeritemmodel.hpp @@ -28,7 +28,7 @@ namespace MWGui bool onTakeItem(const MWWorld::Ptr &item, int count) override; ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; size_t getItemCount() override; MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; @@ -36,6 +36,8 @@ namespace MWGui void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + private: std::vector> mItemSources; std::vector mWorldItems; diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp index a29910f000..f4a4814654 100644 --- a/apps/openmw/mwgui/debugwindow.cpp +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -2,10 +2,13 @@ #include #include -#include #include #include +#include +#include + +#include #ifndef BT_NO_PROFILE @@ -85,33 +88,112 @@ namespace MWGui // Ideas for other tabs: // - Texture / compositor texture viewer - // - Log viewer // - Material editor // - Shader editor + MyGUI::TabItem* itemLV = mTabControl->addItem("Log Viewer"); + itemLV->setCaptionWithReplacing("#{DebugMenu:LogViewer}"); + mLogView = itemLV->createWidgetReal + ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); + mLogView->setEditReadOnly(true); + +#ifndef BT_NO_PROFILE MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); + item->setCaptionWithReplacing("#{DebugMenu:PhysicsProfiler}"); mBulletProfilerEdit = item->createWidgetReal ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); +#else + mBulletProfilerEdit = nullptr; +#endif + } - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mMainWidget->setSize(viewSize); - + static std::vector sLogCircularBuffer; + static std::mutex sBufferMutex; + static int64_t sLogStartIndex; + static int64_t sLogEndIndex; + void DebugWindow::startLogRecording() + { + sLogCircularBuffer.resize(std::max(0, Settings::Manager::getInt64("log buffer size", "General"))); + Debug::setLogListener([](Debug::Level level, std::string_view prefix, std::string_view msg) + { + if (sLogCircularBuffer.empty()) + return; // Log viewer is disabled. + std::string_view color; + switch (level) + { + case Debug::Error: color = "#FF0000"; break; + case Debug::Warning: color = "#FFFF00"; break; + case Debug::Info: color = "#FFFFFF"; break; + case Debug::Verbose: + case Debug::Debug: color = "#666666"; break; + default: color = "#FFFFFF"; + } + bool bufferOverflow = false; + std::lock_guard lock(sBufferMutex); + const int64_t bufSize = sLogCircularBuffer.size(); + auto addChar = [&](char c) + { + sLogCircularBuffer[sLogEndIndex++] = c; + if (sLogEndIndex == bufSize) + sLogEndIndex = 0; + bufferOverflow = bufferOverflow || sLogEndIndex == sLogStartIndex; + }; + auto addShieldedStr = [&](std::string_view s) + { + for (char c : s) + { + addChar(c); + if (c == '#') + addChar(c); + } + }; + for (char c : color) + addChar(c); + addShieldedStr(prefix); + addShieldedStr(msg); + if (bufferOverflow) + sLogStartIndex = (sLogEndIndex + 1) % bufSize; + }); } - void DebugWindow::onFrame(float dt) + void DebugWindow::updateLogView() { -#ifndef BT_NO_PROFILE - if (!isVisible()) + std::lock_guard lock(sBufferMutex); + + if (!mLogView || sLogCircularBuffer.empty() || sLogStartIndex == sLogEndIndex) return; + if (mLogView->isTextSelection()) + return; // Don't change text while player is trying to copy something - static float timer = 0; - timer -= dt; + std::string addition; + const int64_t bufSize = sLogCircularBuffer.size(); + { + if (sLogStartIndex < sLogEndIndex) + addition = std::string(sLogCircularBuffer.data() + sLogStartIndex, sLogEndIndex - sLogStartIndex); + else + { + addition = std::string(sLogCircularBuffer.data() + sLogStartIndex, bufSize - sLogStartIndex); + addition.append(sLogCircularBuffer.data(), sLogEndIndex); + } + sLogStartIndex = sLogEndIndex; + } - if (timer > 0) - return; - timer = 1; + size_t scrollPos = mLogView->getVScrollPosition(); + bool scrolledToTheEnd = scrollPos+1 >= mLogView->getVScrollRange(); + int64_t newSizeEstimation = mLogView->getTextLength() + addition.size(); + if (newSizeEstimation > bufSize) + mLogView->eraseText(0, newSizeEstimation - bufSize); + mLogView->addText(addition); + if (scrolledToTheEnd && mLogView->getVScrollRange() > 0) + mLogView->setVScrollPosition(mLogView->getVScrollRange() - 1); + else + mLogView->setVScrollPosition(scrollPos); + } + void DebugWindow::updateBulletProfile() + { +#ifndef BT_NO_PROFILE std::stringstream stream; bulletDumpAll(stream); @@ -124,4 +206,17 @@ namespace MWGui #endif } + void DebugWindow::onFrame(float dt) + { + static float timer = 0; + timer -= dt; + if (timer > 0 || !isVisible()) + return; + timer = 0.25; + + if (mTabControl->getIndexSelected() == 0) + updateLogView(); + else + updateBulletProfile(); + } } diff --git a/apps/openmw/mwgui/debugwindow.hpp b/apps/openmw/mwgui/debugwindow.hpp index 33647c0789..9b8711137a 100644 --- a/apps/openmw/mwgui/debugwindow.hpp +++ b/apps/openmw/mwgui/debugwindow.hpp @@ -13,9 +13,14 @@ namespace MWGui void onFrame(float dt) override; + static void startLogRecording(); + private: - MyGUI::TabControl* mTabControl; + void updateLogView(); + void updateBulletProfile(); + MyGUI::TabControl* mTabControl; + MyGUI::EditBox* mLogView; MyGUI::EditBox* mBulletProfilerEdit; }; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index fde029d77f..1e740abe22 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -148,7 +148,7 @@ namespace MWGui // We need this copy for when @# hyperlinks are replaced std::string text = mText; - size_t pos_end; + size_t pos_end = std::string::npos; for(;;) { size_t pos_begin = text.find('@'); @@ -306,7 +306,7 @@ namespace MWGui deleteLater(); for (Link* link : mLinks) delete link; - for (auto link : mTopicLinks) + for (const auto& link : mTopicLinks) delete link.second; for (auto history : mHistoryContents) delete history; @@ -347,8 +347,7 @@ namespace MWGui { if (!mScrollBar->getVisible()) return; - mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), - std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); + mScrollBar->setScrollPosition(std::clamp(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } @@ -369,15 +368,15 @@ namespace MWGui const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - const std::string sPersuasion = gmst.find("sPersuasion")->mValue.getString(); - const std::string sCompanionShare = gmst.find("sCompanionShare")->mValue.getString(); - const std::string sBarter = gmst.find("sBarter")->mValue.getString(); - const std::string sSpells = gmst.find("sSpells")->mValue.getString(); - const std::string sTravel = gmst.find("sTravel")->mValue.getString(); - const std::string sSpellMakingMenuTitle = gmst.find("sSpellMakingMenuTitle")->mValue.getString(); - const std::string sEnchanting = gmst.find("sEnchanting")->mValue.getString(); - const std::string sServiceTrainingTitle = gmst.find("sServiceTrainingTitle")->mValue.getString(); - const std::string sRepair = gmst.find("sRepair")->mValue.getString(); + const std::string& sPersuasion = gmst.find("sPersuasion")->mValue.getString(); + const std::string& sCompanionShare = gmst.find("sCompanionShare")->mValue.getString(); + const std::string& sBarter = gmst.find("sBarter")->mValue.getString(); + const std::string& sSpells = gmst.find("sSpells")->mValue.getString(); + const std::string& sTravel = gmst.find("sTravel")->mValue.getString(); + const std::string& sSpellMakingMenuTitle = gmst.find("sSpellMakingMenuTitle")->mValue.getString(); + const std::string& sEnchanting = gmst.find("sEnchanting")->mValue.getString(); + const std::string& sServiceTrainingTitle = gmst.find("sServiceTrainingTitle")->mValue.getString(); + const std::string& sRepair = gmst.find("sRepair")->mValue.getString(); if (topic != sPersuasion && topic != sCompanionShare && topic != sBarter && topic != sSpells && topic != sTravel && topic != sSpellMakingMenuTitle @@ -451,6 +450,7 @@ namespace MWGui setTitle(mPtr.getClass().getName(mPtr)); updateTopics(); + updateTopicsPane(); // force update for new services updateDisposition(); restock(); @@ -487,12 +487,14 @@ namespace MWGui mHistoryContents.clear(); } - void DialogueWindow::setKeywords(std::list keyWords) + bool DialogueWindow::setKeywords(const std::list& keyWords) { if (mKeywords == keyWords && isCompanion() == mIsCompanion) - return; + return false; mIsCompanion = isCompanion(); mKeywords = keyWords; + updateTopicsPane(); + return true; } void DialogueWindow::updateTopicsPane() @@ -505,13 +507,13 @@ namespace MWGui int services = mPtr.getClass().getServices(mPtr); - bool travel = (mPtr.getTypeName() == typeid(ESM::NPC).name() && !mPtr.get()->mBase->getTransport().empty()) - || (mPtr.getTypeName() == typeid(ESM::Creature).name() && !mPtr.get()->mBase->getTransport().empty()); + bool travel = (mPtr.getType() == ESM::NPC::sRecordId && !mPtr.get()->mBase->getTransport().empty()) + || (mPtr.getType() == ESM::Creature::sRecordId && !mPtr.get()->mBase->getTransport().empty()); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - if (mPtr.getTypeName() == typeid(ESM::NPC).name()) + if (mPtr.getType() == ESM::NPC::sRecordId) mTopicsList->addItem(gmst.find("sPersuasion")->mValue.getString()); if (services & ESM::NPC::AllItems) @@ -556,6 +558,8 @@ namespace MWGui mTopicsList->adjustSize(); updateHistory(); + // The topics list has been regenerated so topic formatting needs to be updated + updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) @@ -601,10 +605,10 @@ namespace MWGui Goodbye* link = new Goodbye(); link->eventActivated += MyGUI::newDelegate(this, &DialogueWindow::onGoodbyeActivated); mLinks.push_back(link); - std::string goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->mValue.getString(); + const std::string& goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->mValue.getString(); BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, textColours.answer, textColours.answerOver, - textColours.answerPressed, - TypesetBook::InteractiveId(link)); + textColours.answerPressed, + TypesetBook::InteractiveId(link)); typesetter->lineBreak(); typesetter->write(questionStyle, to_utf8_span(goodbye.c_str())); } @@ -758,9 +762,9 @@ namespace MWGui void DialogueWindow::updateTopics() { - setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics()); - updateTopicsPane(); - updateTopicFormat(); + // Topic formatting needs to be updated regardless of whether the topic list has changed + if (!setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics())) + updateTopicFormat(); } bool DialogueWindow::isCompanion() diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index ed4c39afed..aff09c0211 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -15,11 +15,6 @@ namespace Gui class MWList; } -namespace MWGui -{ - class WindowManager; -} - namespace MWGui { class ResponseCallback; @@ -122,7 +117,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; - void setKeywords(std::list keyWord); + /// @return true if stale keywords were updated successfully + bool setKeywords(const std::list& keyWord); void addResponse (const std::string& title, const std::string& text, bool needMargin = true); diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp index daf9f66367..cfe60db5dd 100644 --- a/apps/openmw/mwgui/draganddrop.cpp +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -135,7 +135,7 @@ void DragAndDrop::finish() MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); - mDraggedWidget = 0; + mDraggedWidget = nullptr; MWBase::Environment::get().getWindowManager()->setDragDrop(false); } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index d0d2118c6e..ee0067f082 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -109,7 +109,7 @@ namespace MWGui { mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); - mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance())))); + mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 75bf8a3426..64e3847887 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -5,19 +5,19 @@ #include #include -// correctBookartPath -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" - #include #include +#include +#include #include +#include +#include +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwscript/interpretercontext.hpp" -namespace MWGui -{ - namespace Formatting +namespace MWGui::Formatting { /* BookTextParser */ BookTextParser::BookTextParser(const std::string & text) @@ -30,14 +30,18 @@ namespace MWGui // vanilla game does not show any text after the last EOL tag. const std::string lowerText = Misc::StringUtils::lowerCase(mText); - int brIndex = lowerText.rfind("
"); - int pIndex = lowerText.rfind("

"); - if (brIndex == pIndex) - mText = ""; - else if (brIndex > pIndex) - mText = mText.substr(0, brIndex+4); - else - mText = mText.substr(0, pIndex+3); + 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; + } registerTag("br", Event_BrTag); registerTag("p", Event_PTag); @@ -103,7 +107,8 @@ namespace MWGui { if (!mIgnoreLineEndings || ch != '\n') { - mBuffer.push_back(ch); + if (mIndex < mPlainTextEnd) + mBuffer.push_back(ch); mIgnoreLineEndings = false; mIgnoreNewlineTags = false; } @@ -282,8 +287,9 @@ namespace MWGui int width = MyGUI::utility::parseInt(attr.at("width")); int height = MyGUI::utility::parseInt(attr.at("height")); - bool exists; - std::string correctedSrc = MWBase::Environment::get().getWindowManager()->correctBookartPath(src, width, height, &exists); + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + std::string correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); + bool exists = vfs->exists(correctedSrc); if (!exists) { @@ -293,8 +299,7 @@ namespace MWGui pag.setIgnoreLeadingEmptyLines(false); - ImageElement elem(paper, pag, mBlockStyle, - correctedSrc, width, height); + ImageElement elem(paper, pag, mBlockStyle, correctedSrc, width, height); elem.paginate(); break; } @@ -363,7 +368,9 @@ namespace MWGui if (attr.find("face") != attr.end()) { std::string face = attr.at("face"); - mTextStyle.mFont = "Journalbook "+face; + std::string name = Gui::FontLoader::getFontForFace(face); + + mTextStyle.mFont = "Journalbook "+name; } if (attr.find("size") != attr.end()) { @@ -484,4 +491,3 @@ namespace MWGui return mPaginator.getCurrentTop(); } } -} diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index d563514148..387acd58c8 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -14,7 +14,7 @@ namespace MWGui { TextStyle() : mColour(0,0,0) - , mFont("Journalbook Magic Cards") + , mFont("Journalbook DefaultFont") , mTextSize(16) { } @@ -73,6 +73,8 @@ namespace MWGui bool mClosingTag; std::map mTagTypes; std::string mBuffer; + + size_t mPlainTextEnd; }; class Paginator diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index a4ab20fd68..989cd8e258 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -16,7 +18,6 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" @@ -53,10 +54,11 @@ namespace MWGui } void removeItem (const ItemStack& item, size_t count) override { throw std::runtime_error("removeItem not implemented"); } - ModelIndex getIndex (ItemStack item) override { throw std::runtime_error("getIndex not implemented"); } + ModelIndex getIndex (const ItemStack &item) override { throw std::runtime_error("getIndex not implemented"); } void update() override {} size_t getItemCount() override { return 0; } ItemStack getItem (ModelIndex index) override { throw std::runtime_error("getItem not implemented"); } + bool usesContainer(const MWWorld::Ptr&) override { return false; } private: // Where to drop the item @@ -80,7 +82,7 @@ namespace MWGui , mMinimap(nullptr) , mCrosshair(nullptr) , mCellNameBox(nullptr) - , mDrowningFrame(nullptr) + , mDrowningBar(nullptr) , mDrowningFlash(nullptr) , mHealthManaStaminaBaseLeft(0) , mWeapBoxBaseLeft(0) @@ -99,8 +101,6 @@ namespace MWGui , mIsDrowning(false) , mDrowningFlashTheta(0.f) { - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - // Energy bars getWidget(mHealthFrame, "HealthFrame"); getWidget(mHealth, "Health"); @@ -118,6 +118,7 @@ namespace MWGui fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); //Drowning bar + getWidget(mDrowningBar, "DrowningBar"); getWidget(mDrowningFrame, "DrowningFrame"); getWidget(mDrowning, "Drowning"); getWidget(mDrowningFlash, "Flash"); @@ -223,7 +224,7 @@ namespace MWGui void HUD::setDrowningBarVisible(bool visible) { - mDrowningFrame->setVisible(visible); + mDrowningBar->setVisible(visible); } void HUD::onWorldClicked(MyGUI::Widget* _sender) @@ -367,9 +368,6 @@ namespace MWGui mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0,20)); } - if (mIsDrowning) - mDrowningFlashTheta += dt * osg::PI*2; - mSpellIcons->updateWidgets(mEffectBox, true); if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) @@ -377,8 +375,13 @@ namespace MWGui updateEnemyHealthBar(); } + if (mDrowningBar->getVisible()) + mDrowningBar->setPosition(mMainWidget->getWidth()/2 - mDrowningFrame->getWidth()/2, mMainWidget->getTop()); + if (mIsDrowning) { + mDrowningFlashTheta += dt * osg::PI*2; + float intensity = (cos(mDrowningFlashTheta) + 2.0f) / 3.0f; mDrowningFlash->setAlpha(intensity); @@ -412,7 +415,7 @@ namespace MWGui std::string icon = effect->mIcon; int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); + icon = Misc::ResourceHelpers::correctIconPath(icon, MWBase::Environment::get().getResourceSystem()->getVFS()); mSpellImage->setSpellIcon(icon); } @@ -587,7 +590,7 @@ namespace MWGui // effect box can have variable width -> variable left coordinate int effectsDx = 0; if (!mMinimapBox->getVisible ()) - effectsDx = (viewSize.width - mMinimapBoxBaseRight) - (viewSize.width - mEffectBoxBaseRight); + effectsDx = mEffectBoxBaseRight - mMinimapBoxBaseRight; mMapVisible = mMinimapBox->getVisible (); if (!mMapVisible) @@ -605,11 +608,11 @@ namespace MWGui mEnemyHealth->setProgressRange(100); // Health is usually cast to int before displaying. Actors die whenever they are < 1 health. // Therefore any value < 1 should show as an empty health bar. We do the same in statswindow :) - mEnemyHealth->setProgressPosition(static_cast(stats.getHealth().getCurrent() / stats.getHealth().getModified() * 100)); + mEnemyHealth->setProgressPosition(static_cast(stats.getHealth().getRatio() * 100)); static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) - mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); + mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 8a89320d8c..ef591bec97 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -73,7 +73,7 @@ namespace MWGui MyGUI::ImageBox* mCrosshair; MyGUI::TextBox* mCellNameBox; MyGUI::TextBox* mWeaponSpellBox; - MyGUI::Widget *mDrowningFrame, *mDrowningFlash; + MyGUI::Widget *mDrowningBar, *mDrowningFrame, *mDrowningFlash; // bottom left elements int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index f2ff64aa16..f96cfd865e 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -34,7 +34,7 @@ size_t InventoryItemModel::getItemCount() return mItems.size(); } -ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) +ItemModel::ModelIndex InventoryItemModel::getIndex (const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) @@ -131,4 +131,9 @@ bool InventoryItemModel::onTakeItem(const MWWorld::Ptr &item, int count) return true; } +bool InventoryItemModel::usesContainer(const MWWorld::Ptr& container) +{ + return mActor == container; +} + } diff --git a/apps/openmw/mwgui/inventoryitemmodel.hpp b/apps/openmw/mwgui/inventoryitemmodel.hpp index 30d17f3e6c..87d874c946 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.hpp +++ b/apps/openmw/mwgui/inventoryitemmodel.hpp @@ -12,7 +12,7 @@ namespace MWGui InventoryItemModel (const MWWorld::Ptr& actor); ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; size_t getItemCount() override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; @@ -25,6 +25,8 @@ namespace MWGui void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + protected: MWWorld::Ptr mActor; private: diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index b0749d4bdf..f7cbc30ae1 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -27,8 +27,8 @@ #include "../mwworld/class.hpp" #include "../mwworld/actionequip.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "itemview.hpp" #include "inventoryitemmodel.hpp" @@ -37,7 +37,6 @@ #include "countdialog.hpp" #include "tradewindow.hpp" #include "draganddrop.hpp" -#include "widgets.hpp" #include "tooltips.hpp" namespace @@ -45,7 +44,7 @@ namespace bool isRightHandWeapon(const MWWorld::Ptr& item) { - if (item.getClass().getTypeName() != typeid(ESM::Weapon).name()) + if (item.getClass().getType() != ESM::Weapon::sRecordId) return false; std::vector equipmentSlots = item.getClass().getEquipmentSlots(item).first; return (!equipmentSlots.empty() && equipmentSlots.front() == MWWorld::InventoryStore::Slot_CarriedRight); @@ -65,16 +64,11 @@ namespace MWGui , mGuiMode(GM_Inventory) , mLastXSize(0) , mLastYSize(0) - , mPreview(new MWRender::InventoryPreview(parent, resourceSystem, MWMechanics::getPlayer())) + , mPreview(std::make_unique(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) - , mScaleFactor(1.0f) , mUpdateTimer(0.f) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 1.0) - mScaleFactor = uiScale; - - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); @@ -285,7 +279,7 @@ namespace MWGui // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { - bool isWeapon = item.mBase.getTypeName() == typeid(ESM::Weapon).name(); + bool isWeapon = item.mBase.getType() == ESM::Weapon::sRecordId; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if (isWeapon && invStore.isEquipped(item.mBase)) @@ -466,13 +460,10 @@ namespace MWGui void InventoryWindow::updatePreviewSize() { - MyGUI::IntSize size = mAvatarImage->getSize(); - int width = std::min(mPreview->getTextureWidth(), size.width); - int height = std::min(mPreview->getTextureHeight(), size.height); - mPreview->setViewport(int(width*mScaleFactor), int(height*mScaleFactor)); - + const MyGUI::IntSize viewport = getPreviewViewportSize(); + mPreview->setViewport(viewport.width, viewport.height); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, - width*mScaleFactor/float(mPreview->getTextureWidth()), height*mScaleFactor/float(mPreview->getTextureHeight()))); + viewport.width / float(mPreview->getTextureWidth()), viewport.height / float(mPreview->getTextureHeight()))); } void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender) @@ -562,16 +553,16 @@ namespace MWGui if (!script.empty()) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here - const std::string& type = ptr.getTypeName(); - bool isBook = type == typeid(ESM::Book).name(); - if (!isBook && type != typeid(ESM::Ingredient).name() && type != typeid(ESM::Repair).name()) + auto type = ptr.getType(); + bool isBook = type == ESM::Book::sRecordId; + if (!isBook && type != ESM::Ingredient::sRecordId && type != ESM::Repair::sRecordId) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); // Books must have PCSkipEquip set to 1 instead else if (isBook) ptr.getRefData().getLocals().setVarByInt(script, "pcskipequip", 1); } - std::shared_ptr action = ptr.getClass().use(ptr, force); + std::unique_ptr action = ptr.getClass().use(ptr, force); action->execute(player); if (isVisible()) @@ -600,8 +591,8 @@ namespace MWGui useItem(ptr); // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 item - if ((ptr.getTypeName() == typeid(ESM::Potion).name() || - ptr.getTypeName() == typeid(ESM::Ingredient).name()) + if ((ptr.getType() == ESM::Potion::sRecordId || + ptr.getType() == ESM::Ingredient::sRecordId) && mDragAndDrop->mDraggedCount > 1) { // Item can be provided from other window for example container. @@ -633,14 +624,8 @@ namespace MWGui MWWorld::Ptr InventoryWindow::getAvatarSelectedItem(int x, int y) { - // convert to OpenGL lower-left origin - y = (mAvatarImage->getHeight()-1) - y; - - // Scale coordinates - x = int(x*mScaleFactor); - y = int(y*mScaleFactor); - - int slot = mPreview->getSlotSelected (x, y); + const osg::Vec2f viewport_coords = mapPreviewWindowToViewport(x, y); + int slot = mPreview->getSlotSelected(viewport_coords.x(), viewport_coords.y()); if (slot == -1) return MWWorld::Ptr(); @@ -717,19 +702,19 @@ namespace MWGui if (!MWBase::Environment::get().getWindowManager()->isAllowed(GW_Inventory)) return; // make sure the object is of a type that can be picked up - std::string type = object.getTypeName(); - if ( (type != typeid(ESM::Apparatus).name()) - && (type != typeid(ESM::Armor).name()) - && (type != typeid(ESM::Book).name()) - && (type != typeid(ESM::Clothing).name()) - && (type != typeid(ESM::Ingredient).name()) - && (type != typeid(ESM::Light).name()) - && (type != typeid(ESM::Miscellaneous).name()) - && (type != typeid(ESM::Lockpick).name()) - && (type != typeid(ESM::Probe).name()) - && (type != typeid(ESM::Repair).name()) - && (type != typeid(ESM::Weapon).name()) - && (type != typeid(ESM::Potion).name())) + auto type = object.getType(); + if ( (type != ESM::Apparatus::sRecordId) + && (type != ESM::Armor::sRecordId) + && (type != ESM::Book::sRecordId) + && (type != ESM::Clothing::sRecordId) + && (type != ESM::Ingredient::sRecordId) + && (type != ESM::Light::sRecordId) + && (type != ESM::Miscellaneous::sRecordId) + && (type != ESM::Lockpick::sRecordId) + && (type != ESM::Probe::sRecordId) + && (type != ESM::Repair::sRecordId) + && (type != ESM::Weapon::sRecordId) + && (type != ESM::Potion::sRecordId)) return; // An object that can be picked up must have a tooltip. @@ -746,6 +731,12 @@ namespace MWGui if (!object.getRefData().activate()) return; + // 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()) + return; + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); // add to player inventory @@ -816,7 +807,7 @@ namespace MWGui lastId = item.getCellRef().getRefId(); - if (item.getClass().getTypeName() == typeid(ESM::Weapon).name() && + if (item.getClass().getType() == ESM::Weapon::sRecordId && isRightHandWeapon(item) && item.getClass().canBeEquipped(item, player).first) { @@ -835,4 +826,26 @@ namespace MWGui { mPreview->rebuild(); } + + MyGUI::IntSize InventoryWindow::getPreviewViewportSize() const + { + const MyGUI::IntSize previewWindowSize = mAvatarImage->getSize(); + const float scale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + + return MyGUI::IntSize(std::min(mPreview->getTextureWidth(), previewWindowSize.width * scale), + std::min(mPreview->getTextureHeight(), previewWindowSize.height * scale)); + } + + osg::Vec2f InventoryWindow::mapPreviewWindowToViewport(int x, int y) const + { + const MyGUI::IntSize previewWindowSize = mAvatarImage->getSize(); + const float normalisedX = x / std::max(1.0f, previewWindowSize.width); + const float normalisedY = y / std::max(1.0f, previewWindowSize.height); + + const MyGUI::IntSize viewport = getPreviewViewportSize(); + return osg::Vec2f( + normalisedX * float(viewport.width - 1), + (1.0 - normalisedY) * float(viewport.height - 1) + ); + } } diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index dc3ee9e0cb..a89e9a945f 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -104,7 +104,6 @@ namespace MWGui std::unique_ptr mPreview; bool mTrading; - float mScaleFactor; float mUpdateTimer; void toggleMaximized(); @@ -131,6 +130,9 @@ namespace MWGui void updatePreviewSize(); void updateArmorRating(); + MyGUI::IntSize getPreviewViewportSize() const; + osg::Vec2f mapPreviewWindowToViewport(int x, int y) const; + void adjustPanes(); /// Unequips count items from mSelectedItem, if it is equipped, and then updates mSelectedItem in case the items were re-stacked diff --git a/apps/openmw/mwgui/itemchargeview.cpp b/apps/openmw/mwgui/itemchargeview.cpp index 44fa94f3a2..5f9788bc45 100644 --- a/apps/openmw/mwgui/itemchargeview.cpp +++ b/apps/openmw/mwgui/itemchargeview.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index cf88efaaed..7f74d3fb5a 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -2,7 +2,6 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -130,7 +129,7 @@ namespace MWGui return -1; } - ItemModel::ModelIndex ProxyItemModel::getIndex (ItemStack item) + ItemModel::ModelIndex ProxyItemModel::getIndex (const ItemStack& item) { return mSourceModel->getIndex(item); } @@ -163,4 +162,9 @@ namespace MWGui { return mSourceModel->onTakeItem(item, count); } + + bool ProxyItemModel::usesContainer(const MWWorld::Ptr& container) + { + return mSourceModel->usesContainer(container); + } } diff --git a/apps/openmw/mwgui/itemmodel.hpp b/apps/openmw/mwgui/itemmodel.hpp index e120dde0fa..d538a040db 100644 --- a/apps/openmw/mwgui/itemmodel.hpp +++ b/apps/openmw/mwgui/itemmodel.hpp @@ -55,7 +55,7 @@ namespace MWGui virtual size_t getItemCount() = 0; /// Returns an invalid index if the item was not found - virtual ModelIndex getIndex (ItemStack item) = 0; + virtual ModelIndex getIndex (const ItemStack &item) = 0; /// Rebuild the item model, this will invalidate existing model indices virtual void update() = 0; @@ -75,6 +75,8 @@ namespace MWGui virtual bool onDropItem(const MWWorld::Ptr &item, int count); virtual bool onTakeItem(const MWWorld::Ptr &item, int count); + virtual bool usesContainer(const MWWorld::Ptr& container) = 0; + private: ItemModel(const ItemModel&); ItemModel& operator=(const ItemModel&); @@ -96,13 +98,15 @@ namespace MWGui MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; /// @note Takes ownership of the passed pointer. void setSourceModel(ItemModel* sourceModel); ModelIndex mapToSource (ModelIndex index); ModelIndex mapFromSource (ModelIndex index); + + bool usesContainer(const MWWorld::Ptr& container) override; protected: ItemModel* mSourceModel; }; diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index 94dcc77c5e..14f2c1dd9b 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -5,9 +5,7 @@ #include #include #include -#include #include -#include #include "../mwworld/class.hpp" diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index 940e95a7e9..1ae683f3b4 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -5,7 +5,12 @@ #include #include +#include // correctIconPath +#include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -106,7 +111,14 @@ namespace MWGui std::string invIcon = ptr.getClass().getInventoryIcon(ptr); if (invIcon.empty()) invIcon = "default icon.tga"; - setIcon(MWBase::Environment::get().getWindowManager()->correctIconPath(invIcon)); + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + invIcon = Misc::ResourceHelpers::correctIconPath(invIcon, vfs); + if (!vfs->exists(invIcon)) + { + Log(Debug::Error) << "Failed to open image: '" << invIcon << "' not found, falling back to 'default-icon.tga'"; + invIcon = Misc::ResourceHelpers::correctIconPath("default icon.tga", vfs); + } + setIcon(invIcon); } @@ -136,7 +148,7 @@ namespace MWGui if (state == None) { if (!isMagic) - backgroundTex = ""; + backgroundTex.clear(); } else if (state == Equip) { diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index cc793073e3..92c0fc9edc 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -82,15 +82,13 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison - for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) - { - spell.second.mNextWorsening += mDays * 24; - } + player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); std::set skills; for (int day=0; daygetPrng(); + int skill = Misc::Rng::rollDice(ESM::Skill::Length, prng); skills.insert(skill); MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill); @@ -112,7 +110,7 @@ namespace MWGui for (const int& skill : skills) { - std::string skillName = gmst.find(ESM::Skill::sSkillNameIds[skill])->mValue.getString(); + const std::string& skillName = gmst.find(ESM::Skill::sSkillNameIds[skill])->mValue.getString(); int skillValue = player.getClass().getNpcStats(player).getSkill(skill).getBase(); std::string skillMsg = gmst.find("sNotifyMessage44")->mValue.getString(); if (skill == ESM::Skill::Sneak || skill == ESM::Skill::Security) diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index 065a503e69..40053b3c8a 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -278,7 +278,7 @@ BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex () mIndexPagesCount = 2; } - unsigned char ch[2] = {0xd0, 0x90}; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 + unsigned char ch[3] = {0xd0, 0x90, 0x00}; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 for (int i = 0; i < 32; ++i) { diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 426c3b4378..1d1c954d8a 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -1,7 +1,6 @@ #include "journalviewmodel.hpp" #include -#include #include @@ -129,7 +128,7 @@ struct JournalViewModelImpl : JournalViewModel utf8text.replace(pos_begin, pos_end+1-pos_begin, displayName); - intptr_t value; + intptr_t value = 0; if (mModel->mKeywordSearch.containsKeyword(topicName, value)) mHyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = value; } @@ -313,9 +312,9 @@ struct JournalViewModelImpl : JournalViewModel for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); - Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); + Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); - if (first != Misc::StringUtils::toLowerUtf8(character)) + if (first != Utf8Stream::toLowerUtf8(character)) continue; visitor (i->second.getName()); diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp index 3a93721303..eda51b9300 100644 --- a/apps/openmw/mwgui/journalviewmodel.hpp +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index 1474becf08..d215587d49 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -83,16 +83,16 @@ namespace setVisible (visible); } - void adviseButtonClick (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender)) + void adviseButtonClick (char const * name, void (JournalWindowImpl::*handler)(MyGUI::Widget*)) { getWidget (name) -> - eventMouseButtonClick += newDelegate(this, Handler); + eventMouseButtonClick += newDelegate(this, handler); } - void adviseKeyPress (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character)) + void adviseKeyPress (char const * name, void (JournalWindowImpl::*handler)(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char)) { getWidget (name) -> - eventKeyButtonPressed += newDelegate(this, Handler); + eventKeyButtonPressed += newDelegate(this, handler); } MWGui::BookPage* getPage (char const * name) @@ -561,7 +561,7 @@ namespace if (mAllQuests) { SetNamesInactive setInactive(list); - mModel->visitQuestNames(!mAllQuests, setInactive); + mModel->visitQuestNames(false, setInactive); } MWBase::Environment::get().getWindowManager()->playSound("book page"); diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index 6dd66029b8..37b64f276c 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -67,7 +66,7 @@ void KeyboardNavigation::saveFocus(int mode) { mKeyFocus[mode] = focus; } - else + else if(shouldAcceptKeyFocus(mCurrentFocus)) { mKeyFocus[mode] = mCurrentFocus; } @@ -79,7 +78,7 @@ void KeyboardNavigation::restoreFocus(int mode) if (found != mKeyFocus.end()) { MyGUI::Widget* w = found->second; - if (w && w->getVisible() && w->getEnabled()) + if (w && w->getVisible() && w->getEnabled() && w->getInheritedVisible() && w->getInheritedEnabled()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second); } } @@ -93,17 +92,6 @@ void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget) mCurrentFocus = nullptr; } -void styleFocusedButton(MyGUI::Widget* w) -{ - if (w) - { - if (MyGUI::Button* b = w->castType(false)) - { - b->_setWidgetState("highlighted"); - } - } -} - bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root) { while (widget && widget->getParent()) @@ -126,7 +114,6 @@ void KeyboardNavigation::onFrame() if (focus == mCurrentFocus) { - styleFocusedButton(mCurrentFocus); return; } @@ -137,19 +124,10 @@ void KeyboardNavigation::onFrame() focus = mCurrentFocus; } - // style highlighted button (won't be needed for MyGUI 3.2.3) if (focus != mCurrentFocus) { - if (mCurrentFocus) - { - if (MyGUI::Button* b = mCurrentFocus->castType(false)) - b->_setWidgetState("normal"); - } - mCurrentFocus = focus; } - - styleFocusedButton(mCurrentFocus); } void KeyboardNavigation::setDefaultFocus(MyGUI::Widget *window, MyGUI::Widget *defaultFocus) @@ -267,7 +245,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap) if (wrap) index = (index + keyFocusList.size())%keyFocusList.size(); else - index = std::min(std::max(0, index), static_cast(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 ae1c096599..0f2af30a60 100644 --- a/apps/openmw/mwgui/layout.cpp +++ b/apps/openmw/mwgui/layout.cpp @@ -8,33 +8,29 @@ namespace MWGui { - void Layout::initialise(const std::string& _layout, MyGUI::Widget* _parent) + void Layout::initialise(std::string_view _layout) { - const std::string MAIN_WINDOW = "_Main"; + const auto MAIN_WINDOW = "_Main"; mLayoutName = _layout; - if (mLayoutName.empty()) - mMainWidget = _parent; - else + mPrefix = MyGUI::utility::toString(this, "_"); + mListWindowRoot = MyGUI::LayoutManager::getInstance().loadLayout(mLayoutName, mPrefix); + + const std::string main_name = mPrefix + MAIN_WINDOW; + for (MyGUI::Widget* widget : mListWindowRoot) { - mPrefix = MyGUI::utility::toString(this, "_"); - mListWindowRoot = MyGUI::LayoutManager::getInstance().loadLayout(mLayoutName, mPrefix, _parent); + if (widget->getName() == main_name) + mMainWidget = widget; - const std::string main_name = mPrefix + MAIN_WINDOW; - for (MyGUI::Widget* widget : mListWindowRoot) - { - if (widget->getName() == main_name) - { - mMainWidget = widget; - break; - } - } - MYGUI_ASSERT(mMainWidget, "root widget name '" << MAIN_WINDOW << "' in layout '" << mLayoutName << "' not found."); + // Force the alignment to update immediately + widget->_setAlign(widget->getSize(), widget->getParentSize()); } + MYGUI_ASSERT(mMainWidget, "root widget name '" << MAIN_WINDOW << "' in layout '" << mLayoutName << "' not found."); } void Layout::shutdown() { + setVisible(false); MyGUI::Gui::getInstance().destroyWidget(mMainWidget); mListWindowRoot.clear(); } @@ -49,7 +45,7 @@ namespace MWGui mMainWidget->setVisible(b); } - void Layout::setText(const std::string &name, const std::string &caption) + void Layout::setText(std::string_view name, const std::string &caption) { MyGUI::Widget* pt; getWidget(pt, name); @@ -64,11 +60,13 @@ namespace MWGui window->setCaptionWithReplacing(title); } - MyGUI::Widget* Layout::getWidget(const std::string &_name) + MyGUI::Widget* Layout::getWidget(std::string_view _name) { + std::string target = mPrefix; + target += _name; for (MyGUI::Widget* widget : mListWindowRoot) { - MyGUI::Widget* find = widget->findWidget(mPrefix + _name); + MyGUI::Widget* find = widget->findWidget(target); if (nullptr != find) { return find; diff --git a/apps/openmw/mwgui/layout.hpp b/apps/openmw/mwgui/layout.hpp index ea51bf541e..f0fd633ee2 100644 --- a/apps/openmw/mwgui/layout.hpp +++ b/apps/openmw/mwgui/layout.hpp @@ -2,7 +2,8 @@ #define OPENMW_MWGUI_LAYOUT_H #include -#include +#include + #include #include @@ -15,9 +16,12 @@ namespace MWGui class Layout { public: - Layout(const std::string & _layout, MyGUI::Widget* _parent = nullptr) - : mMainWidget(nullptr) - { initialise(_layout, _parent); } + Layout(std::string_view layout) : mMainWidget(nullptr) + { + initialise(layout); + assert(mMainWidget); + } + virtual ~Layout() { try @@ -30,10 +34,10 @@ namespace MWGui } } - MyGUI::Widget* getWidget(const std::string& _name); + MyGUI::Widget* getWidget(std::string_view name); template - void getWidget(T * & _widget, const std::string & _name) + void getWidget(T * & _widget, std::string_view _name) { MyGUI::Widget* w = getWidget(_name); T* cast = w->castType(false); @@ -48,8 +52,7 @@ namespace MWGui } private: - void initialise(const std::string & _layout, - MyGUI::Widget* _parent = nullptr); + void initialise(std::string_view layout); void shutdown(); @@ -58,7 +61,7 @@ namespace MWGui virtual void setVisible(bool b); - void setText(const std::string& name, const std::string& caption); + void setText(std::string_view name, const std::string& caption); // NOTE: this assume that mMainWidget is of type Window. void setTitle(const std::string& title); diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 86f92db6f6..14de3fd274 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -12,7 +12,6 @@ #include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index cd0384bb02..61eca3d1d0 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -1,16 +1,18 @@ #include "loadingscreen.hpp" #include +#include #include #include +#include -#include #include #include #include +#include #include #include #include @@ -23,8 +25,6 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" -#include "../mwrender/vismask.hpp" - #include "backgroundimage.hpp" namespace MWGui @@ -44,19 +44,14 @@ namespace MWGui , mProgress(0) , mShowWallpaper(true) { - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); getWidget(mLoadingBox, "LoadingBox"); + getWidget(mSceneImage, "Scene"); + getWidget(mSplashImage, "Splash"); mProgressBar->setScrollViewPage(1); - mBackgroundImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); - mSceneImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Scene"); - findSplashScreens(); } @@ -66,41 +61,21 @@ namespace MWGui void LoadingScreen::findSplashScreens() { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string pattern = "Splash/"; - mResourceSystem->getVFS()->normalizeFilename(pattern); - - /* priority given to the left */ - const std::array supported_extensions {{".tga", ".dds", ".ktx", ".png", ".bmp", ".jpeg", ".jpg"}}; + auto isSupportedExtension = [](const std::string_view& ext) { + static const std::array supported_extensions{ {"tga", "dds", "ktx", "png", "bmp", "jpeg", "jpg"} }; + return !ext.empty() && std::find(supported_extensions.begin(), supported_extensions.end(), ext) != supported_extensions.end(); + }; - auto found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator("Splash/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos) - { - for(auto const& extension: supported_extensions) - { - if (name.compare(pos, name.size() - pos, extension) == 0) - { - mSplashScreens.push_back(found->first); - break; /* based on priority */ - } - } - } - } - else - break; - ++found; + if (isSupportedExtension(Misc::getFileExtension(name))) + mSplashScreens.push_back(name); } if (mSplashScreens.empty()) Log(Debug::Warning) << "Warning: no splash screens found!"; } - void LoadingScreen::setLabel(const std::string &label, bool important, bool center) + void LoadingScreen::setLabel(const std::string &label, bool important) { mImportantLabel = important; @@ -110,7 +85,7 @@ namespace MWGui size.width = std::max(300, size.width); mLoadingBox->setSize(size); - if (center) + if (MWBase::Environment::get().getWindowManager()->getMessagesCount() > 0) mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2); else mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8); @@ -119,7 +94,7 @@ namespace MWGui void LoadingScreen::setVisible(bool visible) { WindowBase::setVisible(visible); - mBackgroundImage->setVisible(visible); + mSplashImage->setVisible(visible); mSceneImage->setVisible(visible); } @@ -136,24 +111,28 @@ namespace MWGui { public: CopyFramebufferToTextureCallback(osg::Texture2D* texture) - : mTexture(texture) - , oneshot(true) + : mOneshot(true) + , mTexture(texture) { } void operator () (osg::RenderInfo& renderInfo) const override { - if (!oneshot) - return; - oneshot = false; int w = renderInfo.getCurrentCamera()->getViewport()->width(); int h = renderInfo.getCurrentCamera()->getViewport()->height(); mTexture->copyTexImage2D(*renderInfo.getState(), 0, 0, w, h); + + mOneshot = false; + } + + void reset() + { + mOneshot = true; } private: + mutable bool mOneshot; osg::ref_ptr mTexture; - mutable bool oneshot; }; class DontComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback @@ -222,7 +201,6 @@ namespace MWGui mViewer->getSceneData()->setComputeBoundingSphereCallback(nullptr); mViewer->getSceneData()->dirtyBound(); - //std::cout << "loading took " << mTimer.time_m() - mLoadingOnTime << std::endl; setVisible(false); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) @@ -244,8 +222,8 @@ namespace MWGui // TODO: add option (filename pattern?) to use image aspect ratio instead of 4:3 // we can't do this by default, because the Morrowind splash screens are 1024x1024, but should be displayed as 4:3 bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); - mBackgroundImage->setVisible(true); - mBackgroundImage->setBackgroundImage(randomSplash, true, stretch); + mSplashImage->setVisible(true); + mSplashImage->setBackgroundImage(randomSplash, true, stretch); } mSceneImage->setBackgroundImage(""); mSceneImage->setVisible(false); @@ -319,15 +297,24 @@ namespace MWGui if (!mGuiTexture.get()) { - mGuiTexture.reset(new osgMyGUI::OSGTexture(mTexture)); + mGuiTexture = std::make_unique(mTexture); + } + + if (!mCopyFramebufferToTextureCallback) + { + mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); } - // Notice that the next time this is called, the current CopyFramebufferToTextureCallback will be deleted - // so there's no memory leak as at most one object of type CopyFramebufferToTextureCallback is allocated at a time. - mViewer->getCamera()->setInitialDrawCallback(new CopyFramebufferToTextureCallback(mTexture)); +#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) + mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); + mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); +#else + mViewer->getCamera()->setInitialDrawCallback(mCopyFramebufferToTextureCallback); +#endif + mCopyFramebufferToTextureCallback->reset(); - mBackgroundImage->setBackgroundImage(""); - mBackgroundImage->setVisible(false); + mSplashImage->setBackgroundImage(""); + mSplashImage->setVisible(false); mSceneImage->setRenderItemTexture(mGuiTexture.get()); mSceneImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 2577827aaa..cfa97ed762 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -28,6 +28,7 @@ namespace Resource namespace MWGui { class BackgroundImage; + class CopyFramebufferToTextureCallback; class LoadingScreen : public WindowBase, public Loading::Listener { @@ -36,7 +37,7 @@ namespace MWGui virtual ~LoadingScreen(); /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details - void setLabel (const std::string& label, bool important, bool center) override; + void setLabel (const std::string& label, bool important) override; void loadingOn(bool visible=true) override; void loadingOff() override; void setProgressRange (size_t range) override; @@ -78,12 +79,13 @@ namespace MWGui MyGUI::TextBox* mLoadingText; MyGUI::ScrollBar* mProgressBar; - BackgroundImage* mBackgroundImage; + BackgroundImage* mSplashImage; BackgroundImage* mSceneImage; std::vector mSplashScreens; osg::ref_ptr mTexture; + osg::ref_ptr mCopyFramebufferToTextureCallback; std::unique_ptr mGuiTexture; void changeWallpaper(); diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index a5d8f7344d..8fff838add 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -24,7 +24,7 @@ namespace MWGui MainMenu::MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription) : WindowBase("openmw_mainmenu.layout") , mWidth (w), mHeight (h) - , mVFS(vfs), mButtonBox(0) + , mVFS(vfs), mButtonBox(nullptr) , mBackground(nullptr) , mVideoBackground(nullptr) , mVideo(nullptr) @@ -167,11 +167,11 @@ namespace MWGui { // Use black background to correct aspect ratio mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Default, "Menu"); + MyGUI::Align::Default, "MainMenuBackground"); mVideoBackground->setImageTexture("black"); mVideo = mVideoBackground->createWidget("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); + MyGUI::Align::Stretch, "MainMenuBackground"); mVideo->setVFS(mVFS); mVideo->playVideo("video\\menu_background.bik"); @@ -191,7 +191,7 @@ namespace MWGui if (!mBackground) { mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); + MyGUI::Align::Stretch, "MainMenuBackground"); mBackground->setBackgroundImage("textures\\menu_morrowind.dds", true, stretch); } mBackground->setVisible(true); diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index acf1319269..bda94f6439 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -10,9 +10,11 @@ #include #include #include +#include +#include -#include -#include +#include +#include #include #include @@ -23,6 +25,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/cellutils.hpp" #include "../mwrender/globalmap.hpp" #include "../mwrender/localmap.hpp" @@ -30,10 +33,13 @@ #include "confirmationdialog.hpp" #include "tooltips.hpp" +#include + namespace { const int cellSize = Constants::CellSizeInUnits; + constexpr float speed = 1.08f; //the zoom speed, it should be greater than 1 enum LocalMapWidgetDepth { @@ -84,6 +90,22 @@ namespace setColour(mHoverColour); } }; + + MyGUI::IntRect createRect(const MyGUI::IntPoint& center, int radius) + { + return { center.left - radius, center.top + radius, center.left + radius, center.top - radius }; + } + + int getLocalViewingDistance() + { + if (!Settings::Manager::getBool("allow zooming", "Map")) + return Constants::CellGridRadius; + if (!Settings::Manager::getBool("distant terrain", "Terrain")) + return Constants::CellGridRadius; + const int maxLocalViewingDistance = std::max(Settings::Manager::getInt("max local viewing distance", "Map"), Constants::CellGridRadius); + const int viewingDistanceInCells = Settings::Manager::getFloat("viewing distance", "Camera") / Constants::CellSizeInUnits; + return std::min(maxLocalViewingDistance, viewingDistanceInCells); + } } namespace MWGui @@ -167,7 +189,7 @@ namespace MWGui , mFogOfWarToggled(true) , mFogOfWarEnabled(fogOfWarEnabled) , mMapWidgetSize(0) - , mNumCells(0) + , mNumCells(1) , mCellDistance(0) , mCustomMarkers(markers) , mMarkerUpdateTimer(0.0f) @@ -183,12 +205,12 @@ namespace MWGui mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } - void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass) + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance) { mLocalMap = widget; mCompass = compass; mMapWidgetSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); - mCellDistance = Constants::CellGridRadius; + mCellDistance = cellDistance; mNumCells = mCellDistance * 2 + 1; mLocalMap->setCanvasSize(mMapWidgetSize*mNumCells, mMapWidgetSize*mNumCells); @@ -234,65 +256,94 @@ namespace MWGui void LocalMapBase::applyFogOfWar() { - for (int mx=0; mxsetImageTexture(""); - entry.mFogTexture.reset(); - continue; - } + entry.mFogWidget->setImageTexture(""); + entry.mFogTexture.reset(); } } redraw(); } - MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) + MyGUI::IntPoint LocalMapBase::getPosition(int cellX, int cellY, float nX, float nY) const + { + // 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) + ); + } + + MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const { - MyGUI::IntPoint widgetPos; + osg::Vec2i cellIndex; // normalized cell coordinates float nX,nY; if (!mInterior) { - int cellX, cellY; - MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); - nX = (worldX - cellSize * cellX) / cellSize; + cellIndex = MWWorld::positionToCellIndex(worldX, worldY); + nX = (worldX - cellSize * cellIndex.x()) / cellSize; // Image space is -Y up, cells are Y up - nY = 1 - (worldY - cellSize * cellY) / cellSize; + nY = 1 - (worldY - cellSize * cellIndex.y()) / cellSize; + } + else + mLocalMapRender->worldToInteriorMapPosition({ worldX, worldY }, nX, nY, cellIndex.x(), cellIndex.y()); - float cellDx = static_cast(cellX - mCurX); - float cellDy = static_cast(cellY - mCurY); + markerPos.cellX = cellIndex.x(); + markerPos.cellY = cellIndex.y(); + markerPos.nX = nX; + markerPos.nY = nY; + return getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + } - markerPos.cellX = cellX; - markerPos.cellY = cellY; + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates(float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const + { + int halfMarkerSize = markerSize / 2; + auto position = getMarkerPosition(worldX, worldY, markerPos); + return MyGUI::IntCoord(position.left - halfMarkerSize, position.top - halfMarkerSize, markerSize, markerSize); + } - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + cellDx) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - cellDy) * mMapWidgetSize)); - } - else - { - int cellX, cellY; - osg::Vec2f worldPos (worldX, worldY); - mLocalMapRender->worldToInteriorMapPosition(worldPos, nX, nY, cellX, cellY); + MyGUI::Widget* LocalMapBase::createDoorMarker(const std::string& name, const MyGUI::VectorString& notes, 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); + markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); + markerWidget->setDepth(Local_MarkerLayer); + markerWidget->setNeedMouseFocus(true); + // Used by tooltips to not show the tooltip if marker is hidden by fog of war + markerWidget->setUserString("ToolTipType", "MapMarker"); - markerPos.cellX = cellX; - markerPos.cellY = cellY; + markerWidget->setUserData(data); + return markerWidget; + } - // Image space is -Y up, cells are Y up - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + (cellX - mCurX)) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - (cellY - mCurY)) * mMapWidgetSize)); - } + void LocalMapBase::centerView() + { + MyGUI::IntPoint pos = mCompass->getPosition() + MyGUI::IntPoint{ 16, 16 }; + MyGUI::IntSize viewsize = mLocalMap->getSize(); + MyGUI::IntPoint viewOffset((viewsize.width / 2) - pos.left, (viewsize.height / 2) - pos.top); + mLocalMap->setViewOffset(viewOffset); + } - markerPos.nX = nX; - markerPos.nY = nY; - return widgetPos; + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const + { + MarkerUserData& markerPos(*widget->getUserData()); + auto position = getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + return MyGUI::IntCoord(position.left - markerSize / 2, position.top - markerSize / 2, markerSize, markerSize); + } + + std::vector& LocalMapBase::currentDoorMarkersWidgets() + { + return mInterior ? mInteriorDoorMarkerWidgets : mExteriorDoorMarkerWidgets; } void LocalMapBase::updateCustomMarkers() @@ -317,13 +368,8 @@ namespace MWGui const ESM::CustomMarker& marker = it->second; MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); - - MyGUI::IntCoord widgetCoord(widgetPos.left - 8, - widgetPos.top - 8, - 16, 16); MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); @@ -346,6 +392,38 @@ namespace MWGui if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) 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 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 }) || activeGrid.inside({ coord.first, coord.second })) + { + mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), doors.begin(), doors.end()); + doors.clear(); + } + else + mExteriorDoorMarkerWidgets.insert(mExteriorDoorMarkerWidgets.end(), doors.begin(), doors.end()); + } + + for (auto& widget : mDoorMarkersToRecycle) + widget->setVisible(false); + + for (auto const& cell : mMaps) + { + if (mHasALastActiveCell && !intersection.inside({ cell.mCellX, cell.mCellY })) + mLocalMapRender->removeExteriorCell(cell.mCellX, cell.mCellY); + } + } + mCurX = x; mCurY = y; mInterior = interior; @@ -370,6 +448,12 @@ namespace MWGui // If we don't do this, door markers that should be disabled will still appear on the map. mNeedDoorMarkersUpdate = true; + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + if (!mInterior) + mHasALastActiveCell = true; + updateMagicMarkers(); updateCustomMarkers(); } @@ -385,21 +469,26 @@ namespace MWGui mLocalMap->getParent()->_updateChilds(); } + float LocalMapBase::getWidgetSize() const + { + return mLocalMapZoom * mMapWidgetSize; + } + void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) { - MyGUI::IntPoint pos(static_cast(mMapWidgetSize * mCellDistance + nx*mMapWidgetSize - 16), static_cast(mMapWidgetSize * mCellDistance + ny*mMapWidgetSize - 16)); - pos.left += (cellX - mCurX) * mMapWidgetSize; - pos.top -= (cellY - mCurY) * mMapWidgetSize; + MyGUI::IntPoint pos = getPosition(cellX, cellY, nx, ny) - MyGUI::IntPoint{ 16, 16 }; if (pos != mCompass->getPosition()) { notifyPlayerUpdate (); mCompass->setPosition(pos); - MyGUI::IntPoint middle (pos.left+16, pos.top+16); - MyGUI::IntCoord viewsize = mLocalMap->getCoord(); - MyGUI::IntPoint viewOffset((viewsize.width / 2) - middle.left, (viewsize.height / 2) - middle.top); - mLocalMap->setViewOffset(viewOffset); + } + osg::Vec2f curPos((cellX + nx) * cellSize, (cellY + 1 - ny) * cellSize); + if ((curPos - mCurPos).length2() > 0.001) + { + mCurPos = curPos; + centerView(); } } @@ -444,22 +533,17 @@ namespace MWGui markerTexture = "textures\\detect_enchantment_icon.dds"; } - int counter = 0; for (const MWWorld::Ptr& ptr : markers) { const ESM::Position& worldPos = ptr.getRefData().getPosition(); MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(worldPos.pos[0], worldPos.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); - ++counter; MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(worldPos.pos[0], worldPos.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); markerWidget->setImageCoord(MyGUI::IntCoord(0,0,8,8)); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } } @@ -514,27 +598,27 @@ namespace MWGui osg::ref_ptr texture = mLocalMapRender->getMapTexture(entry.mCellX, entry.mCellY); if (texture) { - entry.mMapTexture.reset(new osgMyGUI::OSGTexture(texture)); + entry.mMapTexture = std::make_unique(texture); entry.mMapWidget->setRenderItemTexture(entry.mMapTexture.get()); entry.mMapWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); needRedraw = true; } else - entry.mMapTexture.reset(new osgMyGUI::OSGTexture("", nullptr)); + entry.mMapTexture = std::make_unique(std::string(), nullptr); } if (!entry.mFogTexture && mFogOfWarToggled && mFogOfWarEnabled) { osg::ref_ptr tex = mLocalMapRender->getFogOfWarTexture(entry.mCellX, entry.mCellY); if (tex) { - entry.mFogTexture.reset(new osgMyGUI::OSGTexture(tex)); + entry.mFogTexture = std::make_unique(tex); entry.mFogWidget->setRenderItemTexture(entry.mFogTexture.get()); entry.mFogWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } else { entry.mFogWidget->setImageTexture("black"); - entry.mFogTexture.reset(new osgMyGUI::OSGTexture("", nullptr)); + entry.mFogTexture = std::make_unique(std::string(), nullptr); } needRedraw = true; } @@ -545,34 +629,32 @@ namespace MWGui void LocalMapBase::updateDoorMarkers() { - // clear all previous door markers - for (MyGUI::Widget* widget : mDoorMarkerWidgets) - MyGUI::Gui::getInstance().destroyWidget(widget); - mDoorMarkerWidgets.clear(); - + std::vector doors; MWBase::World* world = MWBase::Environment::get().getWorld(); - // Retrieve the door markers we want to show - std::vector doors; + mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), mInteriorDoorMarkerWidgets.begin(), mInteriorDoorMarkerWidgets.end()); + mInteriorDoorMarkerWidgets.clear(); + if (mInterior) { + for (MyGUI::Widget* widget : mExteriorDoorMarkerWidgets) + widget->setVisible(false); + MWWorld::CellStore* cell = world->getInterior (mPrefix); world->getDoorMarkers(cell, doors); } else { - for (int dX=-mCellDistance; dX<=mCellDistance; ++dX) + for (MapEntry& entry : mMaps) { - for (int dY=-mCellDistance; dY<=mCellDistance; ++dY) - { - MWWorld::CellStore* cell = world->getExterior (mCurX+dX, mCurY+dY); - world->getDoorMarkers(cell, doors); - } + if (!entry.mMapTexture && !widgetCropped(entry.mMapWidget, mLocalMap)) + world->getDoorMarkers(world->getExterior(entry.mCellX, entry.mCellY), doors); } + if (doors.empty()) + return; } // Create a widget for each marker - int counter = 0; for (MWBase::World::DoorMarker& marker : doors) { std::vector destNotes; @@ -580,28 +662,33 @@ namespace MWGui for (CustomMarkerCollection::ContainerType::const_iterator iter = markers.first; iter != markers.second; ++iter) destNotes.push_back(iter->second.mNote); - MarkerUserData data (mLocalMapRender); - data.notes = destNotes; - data.caption = marker.name; - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, data); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); - ++counter; - MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); - markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); - markerWidget->setDepth(Local_MarkerLayer); - markerWidget->setNeedMouseFocus(true); - // Used by tooltips to not show the tooltip if marker is hidden by fog of war - markerWidget->setUserString("ToolTipType", "MapMarker"); - - markerWidget->setUserData(data); - doorMarkerCreated(markerWidget); + MyGUI::Widget* markerWidget = nullptr; + MarkerUserData* data; + if (mDoorMarkersToRecycle.empty()) + { + markerWidget = createDoorMarker(marker.name, destNotes, marker.x, marker.y); + data = markerWidget->getUserData(); + doorMarkerCreated(markerWidget); + } + else + { + markerWidget = (MarkerWidget*)mDoorMarkersToRecycle.back(); + mDoorMarkersToRecycle.pop_back(); + + data = markerWidget->getUserData(); + data->notes = destNotes; + data->caption = marker.name; + markerWidget->setCoord(getMarkerCoordinates(marker.x, marker.y, *data, 8)); + markerWidget->setVisible(true); + } - mDoorMarkerWidgets.push_back(markerWidget); + currentDoorMarkersWidgets().push_back(markerWidget); + if (!mInterior) + mExteriorDoorsByCell[{data->cellX, data->cellY}].push_back(markerWidget); } + + for (auto& widget : mDoorMarkersToRecycle) + widget->setVisible(false); } void LocalMapBase::updateMagicMarkers() @@ -623,34 +710,61 @@ namespace MWGui && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->mName, mPrefix))) { MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(markedPosition.pos[0], markedPosition.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(markedPosition.pos[0], markedPosition.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } redraw(); } - // ------------------------------------------------------------------------------------------ + void LocalMapBase::updateLocalMap() + { + auto mapWidgetSize = getWidgetSize(); + mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); + + const auto size = MyGUI::IntSize(std::ceil(mapWidgetSize), std::ceil(mapWidgetSize)); + for (auto& entry : mMaps) + { + const auto position = getPosition(entry.mCellX, entry.mCellY, 0, 0); + entry.mMapWidget->setCoord({ position, size }); + entry.mFogWidget->setCoord({ position, size }); + } + + MarkerUserData markerPos(mLocalMapRender); + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + for (MyGUI::Widget* widget : mCustomMarkerWidgets) + { + const auto& marker = *widget->getUserData(); + widget->setCoord(getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16)); + } + + for (MyGUI::Widget* widget : mMagicMarkerWidgets) + widget->setCoord(getMarkerCoordinates(widget, 8)); + } + // ------------------------------------------------------------------------------------------ MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_map_window_vr.layout") +#else : WindowPinnableBase("openmw_map_window.layout") +#endif , LocalMapBase(customMarkers, localMapRender) , NoDrop(drag, mMainWidget) - , mGlobalMap(0) + , mGlobalMap(nullptr) , mGlobalMapImage(nullptr) , mGlobalMapOverlay(nullptr) , mGlobal(Settings::Manager::getBool("global", "Map")) , mEventBoxGlobal(nullptr) , mEventBoxLocal(nullptr) - , mGlobalMapRender(new MWRender::GlobalMap(localMapRender->getRoot(), workQueue)) + , mGlobalMapRender(std::make_unique(localMapRender->getRoot(), workQueue)) , mEditNoteDialog() { static bool registered = false; @@ -690,14 +804,19 @@ namespace MWGui getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + const bool allowZooming = Settings::Manager::getBool("allow zooming", "Map"); + if(allowZooming) + mEventBoxGlobal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); + if (allowZooming) + mEventBoxLocal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, getLocalViewingDistance()); mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); @@ -745,10 +864,11 @@ namespace MWGui MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); - int x = int(widgetPos.left/float(mMapWidgetSize))-mCellDistance; - int y = (int(widgetPos.top/float(mMapWidgetSize))-mCellDistance)*-1; - float nX = widgetPos.left/float(mMapWidgetSize) - int(widgetPos.left/float(mMapWidgetSize)); - float nY = widgetPos.top/float(mMapWidgetSize) - int(widgetPos.top/float(mMapWidgetSize)); + auto mapWidgetSize = getWidgetSize(); + int x = int(widgetPos.left/float(mapWidgetSize))-mCellDistance; + int y = (int(widgetPos.top/float(mapWidgetSize))-mCellDistance)*-1; + 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; @@ -781,6 +901,110 @@ namespace MWGui mEditNoteDialog.setText(""); } + void MapWindow::onMapZoomed(MyGUI::Widget* sender, int rel) + { + const static int localWidgetSize = Settings::Manager::getInt("local map widget size", "Map"); + const static int globalCellSize = Settings::Manager::getInt("global map cell size", "Map"); + + 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(globalCellSize) * 4.f) / float(localWidgetSize), + float(mLocalMap->getWidth()) / localMapSizeInUnits, + float(mLocalMap->getHeight()) / localMapSizeInUnits + }); + + if (mGlobal) + { + const float currentGlobalZoom = mGlobalMapZoom; + const float currentMinGlobalMapZoom = std::min( + float(mGlobalMap->getWidth()) / float(mGlobalMapRender->getWidth()), + float(mGlobalMap->getHeight()) / float(mGlobalMapRender->getHeight()) + ); + + mGlobalMapZoom *= speedDiff; + + if (zoomIn && mGlobalMapZoom > 4.f) + { + mGlobalMapZoom = currentGlobalZoom; + mLocalMapZoom = currentMinLocalMapZoom; + onWorldButtonClicked(nullptr); + updateLocalMap(); + return; //the zoom in is too big + } + + if (zoomOut && mGlobalMapZoom < currentMinGlobalMapZoom) + { + mGlobalMapZoom = currentGlobalZoom; + return; //the zoom out is too big, we have reach the borders of the widget + } + } + else + { + auto const currentLocalZoom = mLocalMapZoom; + mLocalMapZoom *= speedDiff; + + if (zoomIn && mLocalMapZoom > 4.0f) + { + mLocalMapZoom = currentLocalZoom; + return; //the zoom in is too big + } + + if (zoomOut && mLocalMapZoom < currentMinLocalMapZoom) + { + mLocalMapZoom = currentLocalZoom; + + float zoomRatio = 4.f/ mGlobalMapZoom; + mGlobalMapZoom = 4.f; + onWorldButtonClicked(nullptr); + + zoomOnCursor(zoomRatio); + return; //the zoom out is too big, we switch to the global map + } + + if (zoomOut) + mNeedDoorMarkersUpdate = true; + } + zoomOnCursor(speedDiff); + } + + void MapWindow::zoomOnCursor(float speedDiff) + { + auto map = mGlobal ? mGlobalMap : mLocalMap; + auto cursor = MyGUI::InputManager::getInstance().getMousePosition() - map->getAbsolutePosition(); + auto centerView = map->getViewOffset() - cursor; + + mGlobal? updateGlobalMap() : updateLocalMap(); + + map->setViewOffset(MyGUI::IntPoint( + std::round(centerView.left * speedDiff) + cursor.left, + std::round(centerView.top * speedDiff) + cursor.top + )); + } + + void MapWindow::updateGlobalMap() + { + resizeGlobalMap(); + + float x = mCurPos.x(), y = mCurPos.y(); + if (mInterior) + { + auto pos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); + x = pos.x(); + y = pos.y(); + } + setGlobalMapPlayerPosition(x, y); + + for (auto& [marker, col] : mGlobalMapMarkers) + { + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), col.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } + } + void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) { MyGUI::IntCoord currentCoordinates = sender->getCoord(); @@ -804,13 +1028,11 @@ namespace MWGui void MapWindow::renderGlobalMap() { mGlobalMapRender->render(); - mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); - mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); + resizeGlobalMap(); } MapWindow::~MapWindow() { - delete mGlobalMapRender; } void MapWindow::setCellName(const std::string& cellName) @@ -818,6 +1040,39 @@ namespace MWGui setTitle("#{sCell=" + cellName + "}"); } + MyGUI::IntCoord MapWindow::createMarkerCoords(float x, float y, float agregatedWeight) const + { + float worldX, worldY; + worldPosToGlobalMapImageSpace((x + 0.5f) * Constants::CellSizeInUnits, (y + 0.5f)* Constants::CellSizeInUnits, worldX, worldY); + + const float markerSize = getMarkerSize(agregatedWeight); + const float halfMarkerSize = markerSize / 2.0f; + return MyGUI::IntCoord( + static_cast(worldX - halfMarkerSize), + static_cast(worldY - halfMarkerSize), + markerSize, markerSize); + } + + MyGUI::Widget* MapWindow::createMarker(const std::string& name, float x, float y, float agregatedWeight) + { + MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", + createMarkerCoords(x, y, agregatedWeight), MyGUI::Align::Default); + markerWidget->setVisible(markerWidget->getHeight() >= 6.0); + markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); + setGlobalMapMarkerTooltip(markerWidget, x, y); + + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + + markerWidget->setNeedMouseFocus(true); + markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setDepth(Global_MarkerLayer); + markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + markerWidget->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); + markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + + return markerWidget; + } + void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { CellId cell; @@ -825,31 +1080,33 @@ namespace MWGui cell.second = y; if (mMarkers.insert(cell).second) { - float worldX, worldY; - mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); - - int markerSize = 12; - int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; - MyGUI::IntCoord widgetCoord( - static_cast(worldX * mGlobalMapRender->getWidth()+offset), - static_cast(worldY * mGlobalMapRender->getHeight() + offset), - markerSize, markerSize); - - MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); - - markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); - - setGlobalMapMarkerTooltip(markerWidget, x, y); + MapMarkerType mapMarkerWidget = { osg::Vec2f(x, y), createMarker(name, x, y, 0) }; + mGlobalMapMarkers.emplace(mapMarkerWidget, std::vector()); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + std::string name_ = name.substr(0, name.find(',')); + auto& entry = mGlobalMapMarkersByName[name_]; + if (!entry.widget) + { + entry = { osg::Vec2f(x, y), entry.widget }; //update the coords - markerWidget->setNeedMouseFocus(true); - markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setDepth(Global_MarkerLayer); - markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); - markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); - mGlobalMapMarkers[std::make_pair(x,y)] = markerWidget; + entry.widget = createMarker(name_, entry.position.x(), entry.position.y(), 1); + mGlobalMapMarkers.emplace(entry, std::vector{ entry }); + } + else + { + auto it = mGlobalMapMarkers.find(entry); + auto& marker = const_cast(it->first); + auto& elements = it->second; + elements.emplace_back(mapMarkerWidget); + + //we compute the barycenter of the entry elements => it will be the place on the world map for the agregated widget + marker.position = std::accumulate(elements.begin(), elements.end(), osg::Vec2f(0.f, 0.f), [](const auto& left, const auto& right) { + return left + right.position; + }) / float(elements.size()); + + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), elements.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } } } @@ -891,17 +1148,33 @@ namespace MWGui } } + float MapWindow::getMarkerSize(size_t agregatedWeight) const + { + float markerSize = 12.f * mGlobalMapZoom; + if (mGlobalMapZoom < 1) + return markerSize * std::sqrt(agregatedWeight); //we want to see agregated object + return agregatedWeight ? 0 : markerSize; //we want to see only original markers (i.e. non agregated) + } + + void MapWindow::resizeGlobalMap() + { + mGlobalMap->setCanvasSize(mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + mGlobalMapImage->setSize(mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + } + + void MapWindow::worldPosToGlobalMapImageSpace(float x, float y, float& imageX, float& imageY) const + { + mGlobalMapRender->worldPosToImageSpace(x, y, imageX, imageY); + imageX *= mGlobalMapZoom; + imageY *= mGlobalMapZoom; + } + void MapWindow::updateCustomMarkers() { LocalMapBase::updateCustomMarkers(); - for (auto& widgetPair : mGlobalMapMarkers) - { - int x = widgetPair.first.first; - int y = widgetPair.first.second; - MyGUI::Widget* markerWidget = widgetPair.second; - setGlobalMapMarkerTooltip(markerWidget, x, y); - } + for (auto& [widgetPair, ignore]: mGlobalMapMarkers) + setGlobalMapMarkerTooltip(widgetPair.widget, widgetPair.position.x(), widgetPair.position.y()); } void MapWindow::onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) @@ -917,7 +1190,10 @@ namespace MWGui MyGUI::IntPoint diff = MyGUI::IntPoint(_left, _top) - mLastDragPos; if (!mGlobal) + { + mNeedDoorMarkersUpdate = true; mLocalMap->setViewOffset( mLocalMap->getViewOffset() + diff ); + } else mGlobalMap->setViewOffset( mGlobalMap->getViewOffset() + diff ); @@ -934,9 +1210,6 @@ namespace MWGui mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : "#{sWorld}"); - - if (mGlobal) - globalMapUpdatePlayer (); } void MapWindow::onPinToggled() @@ -978,21 +1251,23 @@ namespace MWGui setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } - void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) + void MapWindow::centerView() { - float x, y; - mGlobalMapRender->worldPosToImageSpace (worldX, worldY, x, y); - x *= mGlobalMapRender->getWidth(); - y *= mGlobalMapRender->getHeight(); - - mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); - + LocalMapBase::centerView(); // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); - MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - x), static_cast(viewsize.height *0.5 - y)); + MyGUI::IntPoint pos = mPlayerArrowGlobal->getPosition() + MyGUI::IntPoint{ 16,16 }; + MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - pos.left), static_cast(viewsize.height * 0.5f - pos.top)); mGlobalMap->setViewOffset(viewoffs); } + void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) + { + float x, y; + worldPosToGlobalMapImageSpace(worldX, worldY, x, y); + mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); + } + void MapWindow::setGlobalMapPlayerDir(const float x, const float y) { MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); @@ -1006,11 +1281,11 @@ namespace MWGui { if (!mGlobalMapTexture.get()) { - mGlobalMapTexture.reset(new osgMyGUI::OSGTexture(mGlobalMapRender->getBaseTexture())); + mGlobalMapTexture = std::make_unique(mGlobalMapRender->getBaseTexture()); mGlobalMapImage->setRenderItemTexture(mGlobalMapTexture.get()); mGlobalMapImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); - mGlobalMapOverlayTexture.reset(new osgMyGUI::OSGTexture(mGlobalMapRender->getOverlayTexture())); + mGlobalMapOverlayTexture = std::make_unique(mGlobalMapRender->getOverlayTexture()); mGlobalMapOverlay->setRenderItemTexture(mGlobalMapOverlayTexture.get()); mGlobalMapOverlay->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); @@ -1027,8 +1302,9 @@ namespace MWGui mChanged = true; for (auto& widgetPair : mGlobalMapMarkers) - MyGUI::Gui::getInstance().destroyWidget(widgetPair.second); + MyGUI::Gui::getInstance().destroyWidget(widgetPair.first.widget); mGlobalMapMarkers.clear(); + mGlobalMapMarkersByName.clear(); } void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) @@ -1075,12 +1351,19 @@ namespace MWGui marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); } void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); + } + + void MapWindow::asyncPrepareSaveMap() + { + mGlobalMapRender->asyncWritePng(); } // ------------------------------------------------------------------- diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 7e8092f289..37c971cd19 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -1,14 +1,19 @@ #ifndef MWGUI_MAPWINDOW_H #define MWGUI_MAPWINDOW_H -#include +#include #include +#include + +#include + #include "windowpinnablebase.hpp" -#include +#include -#include +#include +#include namespace MWRender { @@ -72,7 +77,7 @@ namespace MWGui public: LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled = true); virtual ~LocalMapBase(); - void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass); + 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); @@ -107,9 +112,15 @@ namespace MWGui }; protected: + void updateLocalMap(); + + float mLocalMapZoom = 1.f; MWRender::LocalMap* mLocalMapRender; - int mCurX, mCurY; + int mCurX, mCurY; //the position of the active cell on the global map (in cell coords) + bool mHasALastActiveCell = false; + osg::Vec2f mCurPos; //the position of the player in the world (in cell coords) + bool mInterior; MyGUI::ScrollView* mLocalMap; MyGUI::ImageBox* mCompass; @@ -133,25 +144,35 @@ namespace MWGui MyGUI::ImageBox* mMapWidget; MyGUI::ImageBox* mFogWidget; - std::shared_ptr mMapTexture; - std::shared_ptr mFogTexture; + std::unique_ptr mMapTexture; + std::unique_ptr mFogTexture; int mCellX; int mCellY; }; std::vector mMaps; // Keep track of created marker widgets, just to easily remove them later. - std::vector mDoorMarkerWidgets; + std::vector mExteriorDoorMarkerWidgets; + std::map, std::vector> mExteriorDoorsByCell; + std::vector mInteriorDoorMarkerWidgets; std::vector mMagicMarkerWidgets; std::vector mCustomMarkerWidgets; + std::vector mDoorMarkersToRecycle; + + std::vector& currentDoorMarkersWidgets(); virtual void updateCustomMarkers(); void applyFogOfWar(); - MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos); + MyGUI::IntPoint getPosition(int cellX, int cellY, float nx, float ny) const; + 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::IntCoord getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const; virtual void notifyPlayerUpdate() {} + virtual void centerView(); virtual void notifyMapChanged() {} virtual void customMarkerCreated(MyGUI::Widget* marker) {} @@ -163,15 +184,17 @@ namespace MWGui void addDetectionMarkers(int type); void redraw(); + float getWidgetSize() const; float mMarkerUpdateTimer; float mLastDirectionX; float mLastDirectionY; + bool mNeedDoorMarkersUpdate; + private: void updateDoorMarkers(); - bool mNeedDoorMarkersUpdate; }; class EditNoteDialog : public MWGui::WindowModal @@ -239,11 +262,16 @@ namespace MWGui void write (ESM::ESMWriter& writer, Loading::Listener& progress); void readRecord (ESM::ESMReader& reader, uint32_t type); + void asyncPrepareSaveMap(); + private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); void onMapDoubleClicked(MyGUI::Widget* sender); + void onMapZoomed(MyGUI::Widget* sender, int rel); + void zoomOnCursor(float speedDiff); + void updateGlobalMap(); void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); void onNoteEditOk(); void onNoteEditDelete(); @@ -252,6 +280,12 @@ namespace MWGui void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); void setGlobalMapMarkerTooltip(MyGUI::Widget* widget, int x, int y); + float getMarkerSize(size_t agregatedWeight) const; + void resizeGlobalMap(); + void worldPosToGlobalMapImageSpace(float x, float z, float& imageX, float& imageY) const; + MyGUI::IntCoord createMarkerCoords(float x, float y, float agregatedWeight) const; + MyGUI::Widget* createMarker(const std::string& name, float x, float y, float agregatedWeight); + MyGUI::ScrollView* mGlobalMap; std::unique_ptr mGlobalMapTexture; @@ -273,9 +307,21 @@ namespace MWGui MyGUI::Button* mEventBoxGlobal; MyGUI::Button* mEventBoxLocal; - MWRender::GlobalMap* mGlobalMapRender; + float mGlobalMapZoom = 1.0f; + std::unique_ptr mGlobalMapRender; + + struct MapMarkerType + { + osg::Vec2f position; + MyGUI::Widget* widget = nullptr; + + bool operator<(const MapMarkerType& right) const { + return widget < right.widget; + } + }; - std::map, MyGUI::Widget*> mGlobalMapMarkers; + std::map mGlobalMapMarkersByName; + std::map> mGlobalMapMarkers; EditNoteDialog mEditNoteDialog; ESM::CustomMarker mEditingMarker; @@ -288,6 +334,7 @@ namespace MWGui void notifyPlayerUpdate() override; + void centerView() override; }; } #endif diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index e737cb2b29..1542312efe 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -1,6 +1,6 @@ #include "merchantrepair.hpp" -#include +#include #include #include diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index d64ec9c37a..ccacdba108 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -13,8 +13,6 @@ #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" -#undef MessageBox - namespace MWGui { @@ -28,10 +26,7 @@ namespace MWGui MessageBoxManager::~MessageBoxManager () { - for (MessageBox* messageBox : mMessageBoxes) - { - delete messageBox; - } + MessageBoxManager::clear(); } int MessageBoxManager::getMessagesCount() @@ -104,6 +99,8 @@ namespace MWGui if(stat) mStaticMessageBox = box; + box->setVisible(mVisible); + mMessageBoxes.push_back(box); if(mMessageBoxes.size() > 3) { @@ -146,7 +143,6 @@ namespace MWGui return mInterMessageBoxe != nullptr; } - bool MessageBoxManager::removeMessageBox (MessageBox *msgbox) { std::vector::iterator it; @@ -162,6 +158,11 @@ namespace MWGui return false; } + const std::vector MessageBoxManager::getActiveMessageBoxes() + { + return mMessageBoxes; + } + int MessageBoxManager::readPressedButton (bool reset) { int pressed = mLastButtonPressed; @@ -170,8 +171,12 @@ namespace MWGui return pressed; } - - + void MessageBoxManager::setVisible(bool value) + { + mVisible = value; + for (MessageBox* messageBox : mMessageBoxes) + messageBox->setVisible(value); + } MessageBox::MessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message) : Layout("openmw_messagebox.layout") @@ -204,7 +209,10 @@ namespace MWGui return mMainWidget->getHeight()+mNextBoxPadding; } - + void MessageBox::setVisible(bool value) + { + mMainWidget->setVisible(value); + } InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") @@ -370,7 +378,9 @@ namespace MWGui { for (const std::string& keyword : keywords) { - if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), button->getCaption())) + if (Misc::StringUtils::ciEqual( + MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}").asUTF8(), + button->getCaption().asUTF8())) { return button; } diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index aeb1b83002..59118ce6c2 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -3,8 +3,6 @@ #include "windowbase.hpp" -#undef MessageBox - namespace MyGUI { class Widget; @@ -47,12 +45,17 @@ namespace MWGui void onButtonPressed(int button) { eventButtonPressed(button); eventButtonPressed.clear(); } + void setVisible(bool value); + + const std::vector getActiveMessageBoxes(); + private: std::vector mMessageBoxes; InteractiveMessageBox* mInterMessageBoxe; MessageBox* mStaticMessageBox; float mMessageBoxSpeed; int mLastButtonPressed; + bool mVisible = true; }; class MessageBox : public Layout @@ -60,15 +63,17 @@ namespace MWGui public: MessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message); void setMessage (const std::string& message); + const std::string& getMessage() { return mMessage; }; int getHeight (); void update (int height); + void setVisible(bool value); float mCurrentTime; float mMaxTime; protected: MessageBoxManager& mMessageBoxManager; - const std::string& mMessage; + std::string mMessage; MyGUI::EditBox* mMessageWidget; int mBottomPadding; int mNextBoxPadding; diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index b4de5cb502..976485c23a 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -1,18 +1,18 @@ #include "pickpocketitemmodel.hpp" #include -#include +#include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/pickpocket.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" namespace MWGui { @@ -25,13 +25,13 @@ namespace MWGui float chance = player.getClass().getSkill(player, ESM::Skill::Sneak); mSourceModel->update(); - // build list of items that player is unable to find when attempts to pickpocket. if (hideItems) { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); for (size_t i = 0; igetItemCount(); ++i) { - if (Misc::Rng::roll0to99() > chance) + if (Misc::Rng::roll0to99(prng) > chance) mHiddenItems.push_back(mSourceModel->getItem(i)); } } diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp new file mode 100644 index 0000000000..31449bbee6 --- /dev/null +++ b/apps/openmw/mwgui/postprocessorhud.cpp @@ -0,0 +1,469 @@ +#include "postprocessorhud.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "../mwrender/postprocessor.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +namespace +{ + void saveChain() + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + std::ostringstream chain; + + for (size_t i = 1; i < processor->getTechniques().size(); ++i) + { + auto technique = processor->getTechniques()[i]; + + if (!technique || technique->getDynamic()) + continue; + + chain << technique->getName(); + + if (i < processor-> getTechniques().size() - 1) + chain << ","; + } + + Settings::Manager::setString("chain", "Post Processing", chain.str()); + } +} + +namespace MWGui +{ + void PostProcessorHud::ListWrapper::onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch) + { + if (MyGUI::InputManager::getInstance().isShiftPressed() && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown)) + return; + + MyGUI::ListBox::onKeyButtonPressed(key, ch); + } + + PostProcessorHud::PostProcessorHud() + : WindowBase("openmw_postprocessor_hud.layout") + { + getWidget(mTabConfiguration, "TabConfiguration"); + getWidget(mActiveList, "ActiveList"); + getWidget(mInactiveList, "InactiveList"); + getWidget(mConfigLayout, "ConfigLayout"); + getWidget(mFilter, "Filter"); + getWidget(mButtonActivate, "ButtonActivate"); + getWidget(mButtonDeactivate, "ButtonDeactivate"); + getWidget(mButtonUp, "ButtonUp"); + getWidget(mButtonDown, "ButtonDown"); + + mButtonActivate->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyActivatePressed); + mButtonDeactivate->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyDeactivatePressed); + mButtonUp->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderUpPressed); + mButtonDown->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderDownPressed); + + mActiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed); + mInactiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed); + + mActiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition); + mInactiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition); + + mFilter->eventEditTextChange += MyGUI::newDelegate(this, &PostProcessorHud::notifyFilterChanged); + + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &PostProcessorHud::notifyWindowResize); + + mShaderInfo = mConfigLayout->createWidget("HeaderText", {}, MyGUI::Align::Default); + mShaderInfo->setUserString("VStretch", "true"); + mShaderInfo->setUserString("HStretch", "true"); + mShaderInfo->setTextAlign(MyGUI::Align::Left | MyGUI::Align::Top); + mShaderInfo->setEditReadOnly(true); + mShaderInfo->setEditWordWrap(true); + mShaderInfo->setEditMultiLine(true); + mShaderInfo->setNeedMouseFocus(false); + + mConfigLayout->setVisibleVScroll(true); + + mConfigArea = mConfigLayout->createWidget("", {}, MyGUI::Align::Default); + + mConfigLayout->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + mConfigArea->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + } + + void PostProcessorHud::notifyFilterChanged(MyGUI::EditBox* sender) + { + updateTechniques(); + } + + void PostProcessorHud::notifyWindowResize(MyGUI::Window* sender) + { + layout(); + } + + void PostProcessorHud::notifyResetButtonClicked(MyGUI::Widget* sender) + { + for (size_t i = 1; i < mConfigArea->getChildCount(); ++i) + { + if (auto* child = dynamic_cast(mConfigArea->getChildAt(i))) + child->toDefault(); + } + } + + void PostProcessorHud::notifyListChangePosition(MyGUI::ListBox* sender, size_t index) + { + if (sender == mActiveList) + mInactiveList->clearIndexSelected(); + else if (sender == mInactiveList) + mActiveList->clearIndexSelected(); + + if (index >= sender->getItemCount()) + return; + + updateConfigView(sender->getItemNameAt(index)); + } + + void PostProcessorHud::toggleTechnique(bool enabled) + { + auto* list = enabled ? mInactiveList : mActiveList; + + size_t selected = list->getIndexSelected(); + + if (selected != MyGUI::ITEM_NONE) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + mOverrideHint = list->getItemNameAt(selected); + + auto technique = *list->getItemDataAt>(selected); + if (technique->getDynamic()) + return; + + if (enabled) + processor->enableTechnique(technique); + else + processor->disableTechnique(technique); + saveChain(); + } + } + + void PostProcessorHud::notifyActivatePressed(MyGUI::Widget* sender) + { + toggleTechnique(true); + } + + void PostProcessorHud::notifyDeactivatePressed(MyGUI::Widget* sender) + { + toggleTechnique(false); + } + + void PostProcessorHud::moveShader(Direction direction) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + size_t selected = mActiveList->getIndexSelected(); + + if (selected == MyGUI::ITEM_NONE) + return; + + int index = direction == Direction::Up ? static_cast(selected) - 1 : selected + 1; + index = std::clamp(index, 0, mActiveList->getItemCount() - 1); + + if (static_cast(index) != selected) + { + auto technique = *mActiveList->getItemDataAt>(selected); + if (technique->getDynamic()) + return; + + if (processor->enableTechnique(technique, index) != MWRender::PostProcessor::Status_Error) + saveChain(); + } + } + + void PostProcessorHud::notifyShaderUpPressed(MyGUI::Widget* sender) + { + moveShader(Direction::Up); + } + + void PostProcessorHud::notifyShaderDownPressed(MyGUI::Widget* sender) + { + moveShader(Direction::Down); + } + + void PostProcessorHud::notifyKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char ch) + { + MyGUI::ListBox* list = static_cast(sender); + + if (list->getIndexSelected() == MyGUI::ITEM_NONE) + return; + + if (key == MyGUI::KeyCode::ArrowLeft && list == mActiveList) + { + if (MyGUI::InputManager::getInstance().isShiftPressed()) + { + toggleTechnique(false); + } + else + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mInactiveList); + mActiveList->clearIndexSelected(); + select(mInactiveList, 0); + } + } + else if (key == MyGUI::KeyCode::ArrowRight && list == mInactiveList) + { + if (MyGUI::InputManager::getInstance().isShiftPressed()) + { + toggleTechnique(true); + } + else + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mActiveList); + mInactiveList->clearIndexSelected(); + select(mActiveList, 0); + } + } + else if (list == mActiveList && MyGUI::InputManager::getInstance().isShiftPressed() && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown)) + { + moveShader(key == MyGUI::KeyCode::ArrowUp ? Direction::Up : Direction::Down); + } + } + + void PostProcessorHud::onOpen() + { + toggleMode(Settings::ShaderManager::Mode::Debug); + updateTechniques(); + } + + void PostProcessorHud::onClose() + { + toggleMode(Settings::ShaderManager::Mode::Normal); + } + + void PostProcessorHud::layout() + { + constexpr int padding = 12; + constexpr int padding2 = padding * 2; + mShaderInfo->setCoord(padding, padding, mConfigLayout->getSize().width - padding2 - padding, mShaderInfo->getTextSize().height); + + int totalHeight = mShaderInfo->getTop() + mShaderInfo->getTextSize().height + padding; + + mConfigArea->setCoord({padding, totalHeight, mShaderInfo->getSize().width, mConfigLayout->getHeight()}); + + int childHeights = 0; + MyGUI::EnumeratorWidgetPtr enumerator = mConfigArea->getEnumerator(); + while (enumerator.next()) + { + enumerator.current()->setCoord(padding, childHeights + padding, mShaderInfo->getSize().width - padding2, enumerator.current()->getHeight()); + childHeights += enumerator.current()->getHeight() + padding; + } + totalHeight += childHeights; + + mConfigArea->setSize(mConfigArea->getWidth(), childHeights); + + mConfigLayout->setCanvasSize(mConfigLayout->getWidth() - padding2, totalHeight); + mConfigLayout->setSize(mConfigLayout->getWidth(), mConfigLayout->getParentSize().height - padding2); + } + + void PostProcessorHud::notifyMouseWheel(MyGUI::Widget *sender, int rel) + { + int offset = mConfigLayout->getViewOffset().top + rel * 0.3; + if (offset > 0) + mConfigLayout->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mConfigLayout->setViewOffset(MyGUI::IntPoint(0, static_cast(offset))); + } + + void PostProcessorHud::select(ListWrapper* list, size_t index) + { + list->setIndexSelected(index); + notifyListChangePosition(list, index); + } + + void PostProcessorHud::toggleMode(Settings::ShaderManager::Mode mode) + { + Settings::ShaderManager::get().setMode(mode); + + MWBase::Environment::get().getWorld()->getPostProcessor()->toggleMode(); + + if (!isVisible()) + return; + + if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE) + updateConfigView(mInactiveList->getItemNameAt(mInactiveList->getIndexSelected())); + else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE) + updateConfigView(mActiveList->getItemNameAt(mActiveList->getIndexSelected())); + } + + void PostProcessorHud::updateConfigView(const std::string& name) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + auto technique = processor->loadTechnique(name); + + if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists) + return; + + while (mConfigArea->getChildCount() > 0) + MyGUI::Gui::getInstance().destroyWidget(mConfigArea->getChildAt(0)); + + mShaderInfo->setCaption(""); + + std::ostringstream ss; + + const std::string NA = "#{Interface:NotAvailableShort}"; + const std::string endl = "\n"; + + std::string author = technique->getAuthor().empty() ? NA : std::string(technique->getAuthor()); + std::string version = technique->getVersion().empty() ? NA : std::string(technique->getVersion()); + std::string description = technique->getDescription().empty() ? NA : std::string(technique->getDescription()); + + auto serializeBool = [](bool value) { + return value ? "#{sYes}" : "#{sNo}"; + }; + + const auto flags = technique->getFlags(); + + const auto flag_interior = serializeBool(!(flags & fx::Technique::Flag_Disable_Interiors)); + const auto flag_exterior = serializeBool(!(flags & fx::Technique::Flag_Disable_Exteriors)); + const auto flag_underwater = serializeBool(!(flags & fx::Technique::Flag_Disable_Underwater)); + const auto flag_abovewater = serializeBool(!(flags & fx::Technique::Flag_Disable_Abovewater)); + + switch (technique->getStatus()) + { + case fx::Technique::Status::Success: + case fx::Technique::Status::Uncompiled: + { + if (technique->getDynamic()) + ss << "#{fontcolourhtml=header}#{PostProcessing:ShaderLocked}: #{fontcolourhtml=normal} #{PostProcessing:ShaderLockedDescription}" << endl << endl; + ss << "#{fontcolourhtml=header}#{PostProcessing:Author}: #{fontcolourhtml=normal} " << author << endl << endl + << "#{fontcolourhtml=header}#{PostProcessing:Version}: #{fontcolourhtml=normal} " << version << endl << endl + << "#{fontcolourhtml=header}#{PostProcessing:Description}: #{fontcolourhtml=normal} " << description << endl << endl + << "#{fontcolourhtml=header}#{PostProcessing:InInteriors}: #{fontcolourhtml=normal} " << flag_interior + << "#{fontcolourhtml=header} #{PostProcessing:InExteriors}: #{fontcolourhtml=normal} " << flag_exterior + << "#{fontcolourhtml=header} #{PostProcessing:Underwater}: #{fontcolourhtml=normal} " << flag_underwater + << "#{fontcolourhtml=header} #{PostProcessing:Abovewater}: #{fontcolourhtml=normal} " << flag_abovewater; + break; + } + case fx::Technique::Status::Parse_Error: + ss << "#{fontcolourhtml=negative}Shader Compile Error: #{fontcolourhtml=normal} <" << std::string(technique->getName()) << "> failed to compile." << endl << endl + << technique->getLastError(); + break; + case fx::Technique::Status::File_Not_exists: + break; + } + + mShaderInfo->setCaptionWithReplacing(ss.str()); + + if (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug) + { + if (technique->getUniformMap().size() > 0) + { + MyGUI::Button* resetButton = mConfigArea->createWidget("MW_Button", {0,0,0,24}, MyGUI::Align::Default); + resetButton->setCaptionWithReplacing("#{PostProcessing:ResetShader}"); + resetButton->setTextAlign(MyGUI::Align::Center); + resetButton->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + resetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyResetButtonClicked); + } + + for (const auto& uniform : technique->getUniformMap()) + { + if (!uniform->mStatic || uniform->mSamplerType) + continue; + + if (!uniform->mHeader.empty()) + { + Gui::AutoSizedTextBox* divider = mConfigArea->createWidget("MW_UniformGroup", {0,0,0,34}, MyGUI::Align::Default); + divider->setNeedMouseFocus(false); + divider->setCaption(uniform->mHeader); + } + + fx::Widgets::UniformBase* uwidget = mConfigArea->createWidget("MW_UniformEdit", {0,0,0,22}, MyGUI::Align::Default); + uwidget->init(uniform); + uwidget->getLabel()->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + } + } + + layout(); + } + + void PostProcessorHud::updateTechniques() + { + if (!isVisible()) + return; + + std::string hint; + ListWrapper* hintWidget = nullptr; + if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE) + { + hint = mInactiveList->getItemNameAt(mInactiveList->getIndexSelected()); + hintWidget = mInactiveList; + } + else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE) + { + hint = mActiveList->getItemNameAt(mActiveList->getIndexSelected()); + hintWidget = mActiveList; + } + + mInactiveList->removeAllItems(); + mActiveList->removeAllItems(); + + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + for (const auto& [name, _] : processor->getTechniqueMap()) + { + auto technique = processor->loadTechnique(name); + + if (!technique) + continue; + + if (!technique->getHidden() && !processor->isTechniqueEnabled(technique) && name.find(mFilter->getCaption()) != std::string::npos) + mInactiveList->addItem(name, technique); + } + + for (auto technique : processor->getTechniques()) + { + if (!technique->getHidden()) + mActiveList->addItem(technique->getName(), technique); + } + + auto tryFocus = [this](ListWrapper* widget, const std::string& hint) + { + size_t index = widget->findItemIndexWith(hint); + + if (index != MyGUI::ITEM_NONE) + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(widget); + select(widget, index); + } + }; + + if (!mOverrideHint.empty()) + { + tryFocus(mActiveList, mOverrideHint); + tryFocus(mInactiveList, mOverrideHint); + + mOverrideHint.clear(); + } + else if (hintWidget && !hint.empty()) + tryFocus(hintWidget, hint); + } + + void PostProcessorHud::registerMyGUIComponents() + { + MyGUI::FactoryManager& factory = MyGUI::FactoryManager::getInstance(); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + 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 new file mode 100644 index 0000000000..7473cfcfa5 --- /dev/null +++ b/apps/openmw/mwgui/postprocessorhud.hpp @@ -0,0 +1,105 @@ +#ifndef MYGUI_POSTPROCESSOR_HUD_H +#define MYGUI_POSTPROCESSOR_HUD_H + +#include "windowbase.hpp" + +#include + +#include + +namespace MyGUI +{ + class ScrollView; + class EditBox; + class TabItem; +} +namespace Gui +{ + class AutoSizedButton; + class AutoSizedEditBox; +} + +namespace MWGui +{ + class PostProcessorHud : public WindowBase + { + class ListWrapper final : public MyGUI::ListBox + { + MYGUI_RTTI_DERIVED(ListWrapper) + protected: + void onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch) override; + }; + + public: + PostProcessorHud(); + + void onOpen() override; + + void onClose() override; + + void updateTechniques(); + + void toggleMode(Settings::ShaderManager::Mode mode); + + static void registerMyGUIComponents(); + + private: + + void notifyWindowResize(MyGUI::Window* sender); + + void notifyFilterChanged(MyGUI::EditBox* sender); + + void updateConfigView(const std::string& name); + + void notifyResetButtonClicked(MyGUI::Widget* sender); + + void notifyListChangePosition(MyGUI::ListBox* sender, size_t index); + + void notifyKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char ch); + + void notifyActivatePressed(MyGUI::Widget* sender); + + void notifyDeactivatePressed(MyGUI::Widget* sender); + + void notifyShaderUpPressed(MyGUI::Widget* sender); + + void notifyShaderDownPressed(MyGUI::Widget* sender); + + void notifyMouseWheel(MyGUI::Widget *sender, int rel); + + enum class Direction + { + Up, + Down + }; + + void moveShader(Direction direction); + + void toggleTechnique(bool enabled); + + void select(ListWrapper* list, size_t index); + + void layout(); + + MyGUI::TabItem* mTabConfiguration; + + ListWrapper* mActiveList; + ListWrapper* mInactiveList; + + Gui::AutoSizedButton* mButtonActivate; + Gui::AutoSizedButton* mButtonDeactivate; + Gui::AutoSizedButton* mButtonDown; + Gui::AutoSizedButton* mButtonUp; + + MyGUI::ScrollView* mConfigLayout; + + MyGUI::Widget* mConfigArea; + + MyGUI::EditBox* mFilter; + Gui::AutoSizedEditBox* mShaderInfo; + + std::string mOverrideHint; + }; +} + +#endif diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 214e529428..aeadc88177 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -6,8 +6,10 @@ #include #include -#include -#include +#include +#include +#include +#include #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" @@ -37,9 +39,9 @@ namespace MWGui , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) - , mAssignDialog(0) - , mItemSelectionDialog(0) - , mMagicSelectionDialog(0) + , mAssignDialog(nullptr) + , mItemSelectionDialog(nullptr) + , mMagicSelectionDialog(nullptr) { getWidget(mOkButton, "OKButton"); @@ -79,44 +81,48 @@ namespace MWGui delete mMagicSelectionDialog; } - void QuickKeysMenu::onOpen() + inline void QuickKeysMenu::validate(int index) { - WindowBase::onOpen(); - MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - - // Check if quick keys are still valid - for (int i=0; i<10; ++i) + switch (mKey[index].type) { - switch (mKey[i].type) + case Type_Unassigned: + case Type_HandToHand: + case Type_Magic: + break; + case Type_Item: + case Type_MagicItem: { - case Type_Unassigned: - case Type_HandToHand: - case Type_Magic: - break; - case Type_Item: - case Type_MagicItem: - { - MWWorld::Ptr item = *mKey[i].button->getUserData(); - // Make sure the item is available and is not broken - if (!item || item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && + MWWorld::Ptr item = *mKey[index].button->getUserData(); + // Make sure the item is available and is not broken + if (!item || item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) - { - // Try searching for a compatible replacement - item = store.findReplacement(mKey[i].id); + { + // Try searching for a compatible replacement + item = store.findReplacement(mKey[index].id); - if (item) - mKey[i].button->setUserData(MWWorld::Ptr(item)); + if (item) + mKey[index].button->setUserData(MWWorld::Ptr(item)); - break; - } + break; } } } } + void QuickKeysMenu::onOpen() + { + WindowBase::onOpen(); + + // Quick key index + for (int index = 0; index < 10; ++index) + { + validate(index); + } + } + void QuickKeysMenu::unassign(keyData* key) { key->button->clearUserStrings(); @@ -138,8 +144,8 @@ namespace MWGui else { key->type = Type_Unassigned; - key->id = ""; - key->name = ""; + key->id.clear(); + key->name.clear(); MyGUI::TextBox* textBox = key->button->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); @@ -296,7 +302,7 @@ namespace MWGui std::string path = effect->mIcon; int slashPos = path.rfind('\\'); path.insert(slashPos+1, "b_"); - path = MWBase::Environment::get().getWindowManager()->correctIconPath(path); + path = Misc::ResourceHelpers::correctIconPath(path, MWBase::Environment::get().getResourceSystem()->getVFS()); float scale = 1.f; MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic.dds"); @@ -329,11 +335,13 @@ namespace MWGui assert(index >= 1 && index <= 10); keyData *key = &mKey[index-1]; - + MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); + validate(index-1); + // Delay action executing, // if player is busy for now (casting a spell, attacking someone, etc.) bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) @@ -387,9 +395,9 @@ namespace MWGui if (key->type == Type_Item) { - bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); - bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || - item.getTypeName() == typeid(ESM::Lockpick).name(); + bool isWeapon = item.getType() == ESM::Weapon::sRecordId; + bool isTool = item.getType() == ESM::Probe::sRecordId || + item.getType() == ESM::Lockpick::sRecordId; // delay weapon switching if player is busy if (isDelayNeeded && (isWeapon || isTool)) @@ -408,7 +416,7 @@ namespace MWGui // change draw state only if the item is in player's right hand if (rightHand != store.end() && item == *rightHand) { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } } else if (key->type == Type_MagicItem) @@ -424,7 +432,7 @@ namespace MWGui } store.setSelectedEnchantItem(it); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } } else if (key->type == Type_Magic) @@ -444,12 +452,12 @@ namespace MWGui store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager() ->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } else if (key->type == Type_HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index d0e9509798..4761c98ceb 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -1,8 +1,6 @@ #ifndef MWGUI_QUICKKEYS_H #define MWGUI_QUICKKEYS_H -#include "../mwworld/ptr.hpp" - #include "windowbase.hpp" #include "spellmodel.hpp" @@ -78,7 +76,8 @@ namespace MWGui void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - + // Check if quick key is still valid + inline void validate(int index); void unassign(keyData* key); }; diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 457594697d..6477d907b7 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -134,11 +135,11 @@ namespace MWGui mPreview.reset(nullptr); mPreviewTexture.reset(nullptr); - mPreview.reset(new MWRender::RaceSelectionPreview(mParent, mResourceSystem)); + mPreview = std::make_unique(mParent, mResourceSystem); mPreview->rebuild(); mPreview->setAngle (mCurrentAngle); - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); @@ -399,7 +400,7 @@ namespace MWGui skillWidget = mSkillList->createWidget("MW_StatNameValue", coord1, MyGUI::Align::Default, std::string("Skill") + MyGUI::utility::toString(i)); skillWidget->setSkillNumber(skillId); - skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(race->mData.mBonus[i].mBonus))); + skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(race->mData.mBonus[i].mBonus), 0.f)); ToolTips::createSkillToolTip(skillWidget, skillId); diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index 0299c2a1ac..160999213e 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -4,14 +4,8 @@ #include #include "windowbase.hpp" -#include -namespace MWGui -{ - class WindowManager; -} - namespace MWRender { class RaceSelectionPreview; diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index f8d89c0cb2..df6962c78f 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -1,7 +1,6 @@ #include "recharge.hpp" #include -#include #include @@ -15,10 +14,10 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/recharge.hpp" +#include "itemselection.hpp" #include "itemwidget.hpp" #include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index 3d469bac56..c260b15547 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -3,8 +3,6 @@ #include "windowbase.hpp" -#include "itemselection.hpp" - namespace MWWorld { class Ptr; diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index ea79e0326e..351b976033 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include @@ -17,6 +15,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "itemselection.hpp" #include "itemwidget.hpp" #include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" diff --git a/apps/openmw/mwgui/repair.hpp b/apps/openmw/mwgui/repair.hpp index 594ad28235..701009f541 100644 --- a/apps/openmw/mwgui/repair.hpp +++ b/apps/openmw/mwgui/repair.hpp @@ -3,8 +3,6 @@ #include "windowbase.hpp" -#include "itemselection.hpp" - #include "../mwmechanics/repair.hpp" namespace MWGui diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index e76cbe7706..77249948ab 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -86,7 +87,7 @@ namespace MWGui for (int i = 0; i < ESM::Skill::Length; ++i) { mSkillValues.insert(std::make_pair(i, MWMechanics::SkillValue())); - mSkillWidgetMap.insert(std::make_pair(i, static_cast (0))); + mSkillWidgetMap.insert(std::make_pair(i, static_cast (nullptr))); } MyGUI::Button* backButton; diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index bd17c7afb3..cf3d693e97 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -2,7 +2,7 @@ #define MWGUI_REVIEW_H #include -#include +#include #include "windowbase.hpp" #include "widgets.hpp" @@ -11,11 +11,6 @@ namespace ESM struct Spell; } -namespace MWGui -{ - class WindowManager; -} - namespace MWGui { class ReviewDialog : public WindowModal diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index c4d608443c..700b06e81e 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -208,7 +207,7 @@ namespace MWGui mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) - mCharacterSelection->setCaption("Select Character ..."); + mCharacterSelection->setCaptionWithReplacing("#{SavegameMenu:SelectCharacter}"); fillSaveList(); @@ -424,7 +423,7 @@ namespace MWGui if (Settings::Manager::getBool("timeplayed","Saves")) { - text << "\n" << "Time played: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); + text << "\n" << "#{SavegameMenu:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); } mInfoText->setCaptionWithReplacing(text.str()); @@ -450,6 +449,7 @@ namespace MWGui osg::ref_ptr texture (new osg::Texture2D); texture->setImage(result.getImage()); + texture->setInternalFormat(GL_RGB); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); @@ -457,7 +457,7 @@ namespace MWGui texture->setResizeNonPowerOfTwoHint(false); texture->setUnRefImageDataAfterApply(true); - mScreenshotTexture.reset(new osgMyGUI::OSGTexture(texture)); + mScreenshotTexture = std::make_unique(texture); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); mScreenshot->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/screenfader.cpp b/apps/openmw/mwgui/screenfader.cpp index 619852a22b..aa47d0821c 100644 --- a/apps/openmw/mwgui/screenfader.cpp +++ b/apps/openmw/mwgui/screenfader.cpp @@ -1,6 +1,5 @@ #include "screenfader.hpp" -#include #include #include @@ -91,8 +90,6 @@ namespace MWGui { MyGUI::Gui::getInstance().eventFrameStart += MyGUI::newDelegate(this, &ScreenFader::onFrameStart); - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - MyGUI::ImageBox* imageBox = mMainWidget->castType(false); if (imageBox) { diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index f2c967da4e..df703dcb61 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include "../mwbase/environment.hpp" diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 68dac4a95c..d38763663a 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -1,22 +1,29 @@ #include "settingswindow.hpp" +#include +#include +#include +#include + #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/world.hpp" @@ -29,15 +36,34 @@ namespace { - std::string textureMipmappingToStr(const std::string& val) { - if (val == "linear") return "Trilinear"; - if (val == "nearest") return "Bilinear"; - if (val != "none") - Log(Debug::Warning) << "Warning: Invalid texture mipmap option: "<< val; + if (val == "linear") return "#{SettingsMenu:TextureFilteringTrilinear}"; + if (val == "nearest") return "#{SettingsMenu:TextureFilteringBilinear}"; + if (val == "none") return "#{SettingsMenu:TextureFilteringDisabled}"; + + Log(Debug::Warning) << "Warning: Invalid texture mipmap option: "<< val; + return "#{SettingsMenu:TextureFilteringOther}"; + } + + std::string lightingMethodToStr(SceneUtil::LightingMethod method) + { + std::string result; + switch (method) + { + case SceneUtil::LightingMethod::FFP: + result = "#{SettingsMenu:LightingMethodLegacy}"; + break; + case SceneUtil::LightingMethod::PerObjectUniform: + result = "#{SettingsMenu:LightingMethodShadersCompatibility}"; + break; + case SceneUtil::LightingMethod::SingleUBO: + default: + result = "#{SettingsMenu:LightingMethodShaders}"; + break; + } - return "Other"; + return MyGUI::LanguageManager::getInstance().replaceTags(result); } void parseResolution (int &x, int &y, const std::string& str) @@ -61,6 +87,9 @@ namespace 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 @@ -103,11 +132,24 @@ namespace if (!widget->getUserString(settingMax).empty()) max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } + + void updateMaxLightsComboBox(MyGUI::ComboBox* box) + { + constexpr int min = 8; + constexpr int max = 32; + constexpr int increment = 8; + int maxLights = Settings::Manager::getInt("max lights", "Shaders"); + // show increments of 8 in dropdown + if (maxLights >= min && maxLights <= max && !(maxLights % increment)) + box->setIndexSelected((maxLights / increment)-1); + else + box->setIndexSelected(MyGUI::ITEM_NONE); + } } namespace MWGui { - void SettingsWindow::configureWidgets(MyGUI::Widget* widget) + void SettingsWindow::configureWidgets(MyGUI::Widget* widget, bool init) { MyGUI::EnumeratorWidgetPtr widgets = widget->getEnumerator(); while (widgets.next()) @@ -121,7 +163,8 @@ namespace MWGui getSettingCategory(current)) ? "#{sOn}" : "#{sOff}"; current->castType()->setCaptionWithReplacing(initialValue); - current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); + if (init) + current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); } if (type == sliderType) { @@ -141,10 +184,16 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); - value = std::max(min, std::min(value, max)); + value = std::clamp(value, min, max); value = (value-min)/(max-min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); @@ -155,12 +204,13 @@ namespace MWGui valueStr = MyGUI::utility::toString(value); scroll->setScrollPosition(value); } - scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); + if (init) + scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } - configureWidgets(current); + configureWidgets(current, init); } } @@ -177,9 +227,9 @@ namespace MWGui } } - SettingsWindow::SettingsWindow() : - WindowBase("openmw_settings_window.layout"), - mKeyboardMode(true) + SettingsWindow::SettingsWindow() : WindowBase("openmw_settings_window.layout") + , mKeyboardMode(true) + , mCurrentPage(-1) { bool terrain = Settings::Manager::getBool("distant terrain", "Terrain"); const std::string widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; @@ -187,23 +237,31 @@ namespace MWGui getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); - configureWidgets(mMainWidget); + configureWidgets(mMainWidget, true); setTitle("#{sOptions}"); getWidget(mSettingsTab, "SettingsTab"); getWidget(mOkButton, "OkButton"); getWidget(mResolutionList, "ResolutionList"); - getWidget(mFullscreenButton, "FullscreenButton"); + getWidget(mWindowModeList, "WindowModeList"); getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mTextureFilteringButton, "TextureFilteringButton"); - getWidget(mAnisotropyBox, "AnisotropyBox"); getWidget(mControlsBox, "ControlsBox"); getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mKeyboardSwitch, "KeyboardButton"); getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mWaterRainRippleDetail, "WaterRainRippleDetail"); + getWidget(mLightingMethodButton, "LightingMethodButton"); + getWidget(mLightsResetButton, "LightsResetButton"); + getWidget(mMaxLights, "MaxLights"); + getWidget(mScriptFilter, "ScriptFilter"); + getWidget(mScriptList, "ScriptList"); + getWidget(mScriptBox, "ScriptBox"); + getWidget(mScriptView, "ScriptView"); + getWidget(mScriptAdapter, "ScriptAdapter"); #ifndef WIN32 // hide gamma controls since it currently does not work under Linux @@ -228,10 +286,19 @@ namespace MWGui mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mWaterRainRippleDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterRainRippleDetailChanged); + + mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); + mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); + mMaxLights->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onMaxLightsChanged); + + mWindowModeList->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWindowModeChanged); mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + computeMinimumWindowSize(); + center(); mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); @@ -249,8 +316,10 @@ 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) - + " (" + getAspect(resolution.first, resolution.second) + ")"; + 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 + ")"; if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) mResolutionList->addItem(str); @@ -258,7 +327,7 @@ namespace MWGui highlightCurrentResolution(); std::string tmip = Settings::Manager::getString("texture mipmap", "General"); - mTextureFilteringButton->setCaption(textureMipmappingToStr(tmip)); + mTextureFilteringButton->setCaptionWithReplacing(textureMipmappingToStr(tmip)); int waterTextureSize = Settings::Manager::getInt("rtt size", "Water"); if (waterTextureSize >= 512) @@ -268,14 +337,22 @@ namespace MWGui if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); - int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - waterReflectionDetail = std::min(4, std::max(0, waterReflectionDetail)); + int waterReflectionDetail = std::clamp(Settings::Manager::getInt("reflection detail", "Water"), 0, 5); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); - mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); + int waterRainRippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2); + mWaterRainRippleDetail->setIndexSelected(waterRainRippleDetail); + + updateMaxLightsComboBox(mMaxLights); + + Settings::WindowMode windowMode = static_cast(Settings::Manager::getInt("window mode", "Video")); + mWindowBorderButton->setEnabled(windowMode != Settings::WindowMode::Fullscreen && windowMode != Settings::WindowMode::WindowedFullscreen); mKeyboardSwitch->setStateSelected(true); mControllerSwitch->setStateSelected(false); + + mScriptFilter->eventEditTextChange += MyGUI::newDelegate(this, &SettingsWindow::onScriptFilterChange); + mScriptList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SettingsWindow::onScriptListSelection); } void SettingsWindow::onTabChanged(MyGUI::TabControl* /*_sender*/, size_t /*index*/) @@ -353,11 +430,76 @@ namespace MWGui void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { - unsigned int level = std::min((unsigned int)4, (unsigned int)pos); + unsigned int level = static_cast(std::min(pos, 5)); Settings::Manager::setInt("reflection detail", "Water", level); 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); + apply(); + } + + void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + MWBase::Environment::get().getWindowManager()->interactiveMessageBox("#{SettingsMenu:ChangeRequiresRestart}", {"#{sOK}"}, true); + + const auto settingsNames = _sender->getUserData>(); + Settings::Manager::setString("lighting method", "Shaders", settingsNames->at(pos)); + apply(); + } + + void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* _sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + Settings::Manager::setInt("window mode", "Video", static_cast(_sender->getIndexSelected())); + apply(); + } + + void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) + { + int count = 8 * (pos + 1); + + Settings::Manager::setInt("max lights", "Shaders", count); + apply(); + configureWidgets(mMainWidget, false); + } + + void SettingsWindow::onLightsResetButtonClicked(MyGUI::Widget* _sender) + { + std::vector buttons = {"#{sYes}", "#{sNo}"}; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox("#{SettingsMenu:LightingResetToDefaults}", buttons, true); + int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); + if (selectedButton == 1 || selectedButton == -1) + return; + + constexpr std::array settings = { + "light bounds multiplier", + "maximum light distance", + "light fade start", + "minimum interior brightness", + "max lights", + "lighting method", + }; + for (const auto& setting : settings) + Settings::Manager::setString(setting, "Shaders", Settings::Manager::mDefaultSettings[{"Shaders", setting}]); + + auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::mDefaultSettings[{"Shaders", "lighting method"}]); + auto lightIndex = mLightingMethodButton->findItemIndexWith(lightingMethodToStr(lightingMethod)); + mLightingMethodButton->setIndexSelected(lightIndex); + updateMaxLightsComboBox(mMaxLights); + + apply(); + configureWidgets(mMainWidget, false); + } + void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); @@ -374,49 +516,6 @@ namespace MWGui newState = true; } - if (_sender == mFullscreenButton) - { - // check if this resolution is supported in fullscreen - if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) - { - 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); - } - - bool supported = false; - int fallbackX = 0, fallbackY = 0; - for (unsigned int i=0; igetItemCount(); ++i) - { - std::string resStr = mResolutionList->getItemNameAt(i); - int resX, resY; - parseResolution (resX, resY, resStr); - - if (i == 0) - { - fallbackX = resX; - fallbackY = resY; - } - - if (resX == Settings::Manager::getInt("resolution x", "Video") - && resY == Settings::Manager::getInt("resolution y", "Video")) - supported = true; - } - - if (!supported && mResolutionList->getItemCount()) - { - if (fallbackX != 0 && fallbackY != 0) - { - Settings::Manager::setInt("resolution x", "Video", fallbackX); - Settings::Manager::setInt("resolution y", "Video", fallbackY); - } - } - - mWindowBorderButton->setEnabled(!newState); - } - if (getSettingType(_sender) == checkButtonType) { Settings::Manager::setBool(getSettingName(_sender), getSettingCategory(_sender), newState); @@ -460,6 +559,12 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); } @@ -550,6 +655,86 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::updateLightSettings() + { + auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); + std::string lightingMethodStr = lightingMethodToStr(lightingMethod); + + mLightingMethodButton->removeAllItems(); + + std::array methods = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + + std::vector userData; + for (const auto& method : methods) + { + if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) + continue; + + mLightingMethodButton->addItem(lightingMethodToStr(method)); + userData.emplace_back(SceneUtil::LightManager::getLightingMethodString(method)); + } + + mLightingMethodButton->setUserData(userData); + mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(lightingMethodStr)); + } + + void SettingsWindow::updateWindowModeSettings() + { + size_t index = static_cast(Settings::Manager::getInt("window mode", "Video")); + + if (index > static_cast(Settings::WindowMode::Windowed)) + index = MyGUI::ITEM_NONE; + + mWindowModeList->setIndexSelected(index); + + if (index != static_cast(Settings::WindowMode::Windowed) && index != MyGUI::ITEM_NONE) + { + // check if this resolution is supported in fullscreen + if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) + { + 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); + } + + bool supported = false; + int fallbackX = 0, fallbackY = 0; + for (unsigned int i=0; igetItemCount(); ++i) + { + std::string resStr = mResolutionList->getItemNameAt(i); + int resX, resY; + parseResolution (resX, resY, resStr); + + if (i == 0) + { + fallbackX = resX; + fallbackY = resY; + } + + if (resX == Settings::Manager::getInt("resolution x", "Video") + && resY == Settings::Manager::getInt("resolution y", "Video")) + supported = true; + } + + if (!supported && mResolutionList->getItemCount()) + { + if (fallbackX != 0 && fallbackY != 0) + { + Settings::Manager::setInt("resolution x", "Video", fallbackX); + Settings::Manager::setInt("resolution y", "Video", fallbackY); + } + } + + mWindowBorderButton->setEnabled(false); + } + } + void SettingsWindow::layoutControlsBox() { const int h = 18; @@ -569,6 +754,102 @@ namespace MWGui mControlsBox->setVisibleVScroll(true); } + namespace + { + std::string escapeRegex(const std::string& str) + { + static const std::regex specialChars(R"r([\^\.\[\$\(\)\|\*\+\?\{])r", std::regex_constants::extended); + return std::regex_replace(str, specialChars, R"(\$&)"); + } + + std::regex wordSearch(const std::string& query) + { + static const std::regex wordsRegex(R"([^[:space:]]+)", std::regex_constants::extended); + auto wordsBegin = std::sregex_iterator(query.begin(), query.end(), wordsRegex); + auto wordsEnd = std::sregex_iterator(); + std::string searchRegex("("); + for (auto it = wordsBegin; it != wordsEnd; ++it) + { + if (it != wordsBegin) + searchRegex += '|'; + searchRegex += escapeRegex(query.substr(it->position(), it->length())); + } + searchRegex += ')'; + // query had only whitespace characters + if (searchRegex == "()") + searchRegex = "^(.*)$"; + return std::regex(searchRegex, std::regex_constants::extended | std::regex_constants::icase); + } + + double weightedSearch(const std::regex& regex, const std::string& text) + { + std::smatch matches; + std::regex_search(text, matches, regex); + // need a signed value, so cast to double (not an integer type to guarantee no overflow) + return static_cast(matches.size()); + } + } + + void SettingsWindow::renderScriptSettings() + { + mScriptAdapter->detach(); + + mScriptList->removeAllItems(); + mScriptView->setCanvasSize({0, 0}); + + struct WeightedPage { + size_t mIndex; + std::string mName; + double mNameWeight; + double mHintWeight; + + constexpr auto tie() const { return std::tie(mNameWeight, mHintWeight, mName); } + + constexpr bool operator<(const WeightedPage& rhs) const { return tie() < rhs.tie(); } + }; + + std::regex searchRegex = wordSearch(mScriptFilter->getCaption()); + std::vector weightedPages; + weightedPages.reserve(LuaUi::scriptSettingsPageCount()); + for (size_t i = 0; i < LuaUi::scriptSettingsPageCount(); ++i) + { + LuaUi::ScriptSettingsPage page = LuaUi::scriptSettingsPageAt(i); + double nameWeight = weightedSearch(searchRegex, page.mName); + double hintWeight = weightedSearch(searchRegex, page.mSearchHints); + if ((nameWeight + hintWeight) > 0) + weightedPages.push_back({ i, page.mName, -nameWeight, -hintWeight }); + } + std::sort(weightedPages.begin(), weightedPages.end()); + for (const WeightedPage& weightedPage : weightedPages) + mScriptList->addItem(weightedPage.mName, weightedPage.mIndex); + + // Hide script settings tab when the game world isn't loaded and scripts couldn't add their settings + bool disabled = LuaUi::scriptSettingsPageCount() == 0; + mScriptFilter->setVisible(!disabled); + mScriptList->setVisible(!disabled); + mScriptBox->setVisible(!disabled); + + LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); + mScriptView->setCanvasSize(mScriptAdapter->getSize()); + } + + void SettingsWindow::onScriptFilterChange(MyGUI::EditBox*) + { + renderScriptSettings(); + } + + void SettingsWindow::onScriptListSelection(MyGUI::ListBox*, size_t index) + { + mScriptAdapter->detach(); + mCurrentPage = -1; + if (index < mScriptList->getItemCount()) + { + mCurrentPage = *mScriptList->getItemDataAt(index); + LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); + } + mScriptView->setCanvasSize(mScriptAdapter->getSize()); + } + void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) { int actionId = *_sender->getUserData(); @@ -612,7 +893,10 @@ namespace MWGui { highlightCurrentResolution(); updateControlsBox(); + updateLightSettings(); + updateWindowModeSettings(); resetScrollbars(); + renderScriptSettings(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } @@ -621,6 +905,32 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::computeMinimumWindowSize() + { + auto* window = mMainWidget->castType(); + auto minSize = window->getMinSize(); + + // Window should be at minimum wide enough to show all tabs. + int tabBarWidth = 0; + for (uint32_t i = 0; i < mSettingsTab->getItemCount(); i++) + { + tabBarWidth += mSettingsTab->getButtonWidthAt(i); + } + + // Need to include window margins + int margins = mMainWidget->getWidth() - mSettingsTab->getWidth(); + int minimumWindowWidth = tabBarWidth + margins; + + if (minimumWindowWidth > minSize.width) + { + minSize.width = minimumWindowWidth; + window->setMinSize(minSize); + + // Make a dummy call to setSize so MyGUI can apply any resize resulting from the change in MinSize + mMainWidget->setSize(mMainWidget->getSize()); + } + } + void SettingsWindow::resetScrollbars() { mResolutionList->setScrollPosition(0); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 6f25dd1143..6bc6b7c610 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -1,12 +1,9 @@ #ifndef MWGUI_SETTINGS_H #define MWGUI_SETTINGS_H -#include "windowbase.hpp" +#include -namespace MWGui -{ - class WindowManager; -} +#include "windowbase.hpp" namespace MWGui { @@ -19,6 +16,10 @@ namespace MWGui void updateControlsBox(); + void updateLightSettings(); + + void updateWindowModeSettings(); + void onResChange(int, int) override { center(); } protected: @@ -27,13 +28,17 @@ namespace MWGui // graphics MyGUI::ListBox* mResolutionList; - MyGUI::Button* mFullscreenButton; + MyGUI::ComboBox* mWindowModeList; MyGUI::Button* mWindowBorderButton; MyGUI::ComboBox* mTextureFilteringButton; - MyGUI::Widget* mAnisotropyBox; MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::ComboBox* mWaterRainRippleDetail; + + MyGUI::ComboBox* mMaxLights; + MyGUI::ComboBox* mLightingMethodButton; + MyGUI::Button* mLightsResetButton; // controls MyGUI::ScrollView* mControlsBox; @@ -42,6 +47,13 @@ namespace MWGui MyGUI::Button* mControllerSwitch; bool mKeyboardMode; //if true, setting up the keyboard. Otherwise, it's controller + MyGUI::EditBox* mScriptFilter; + MyGUI::ListBox* mScriptList; + MyGUI::Widget* mScriptBox; + MyGUI::ScrollView* mScriptView; + LuaUi::LuaAdapter* mScriptAdapter; + int mCurrentPage; + void onTabChanged(MyGUI::TabControl* _sender, size_t index); void onOkButtonClicked(MyGUI::Widget* _sender); void onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos); @@ -54,6 +66,13 @@ namespace MWGui void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + void onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + + void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); + void onLightsResetButtonClicked(MyGUI::Widget* _sender); + void onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos); + + void onWindowModeChanged(MyGUI::ComboBox* _sender, size_t pos); void onRebindAction(MyGUI::Widget* _sender); void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); @@ -64,13 +83,19 @@ namespace MWGui void onWindowResize(MyGUI::Window* _sender); + void onScriptFilterChange(MyGUI::EditBox*); + void onScriptListSelection(MyGUI::ListBox*, size_t index); + void apply(); - void configureWidgets(MyGUI::Widget* widget); + void configureWidgets(MyGUI::Widget* widget, bool init); void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); void layoutControlsBox(); - + void renderScriptSettings(); + + void computeMinimumWindowSize(); + private: void resetScrollbars(); }; diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 28b13cdf0d..f1351218e5 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,20 +1,21 @@ #include "sortfilteritemmodel.hpp" #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 "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -27,27 +28,30 @@ namespace { - bool compareType(const std::string& type1, const std::string& type2) + unsigned int getTypeOrder(unsigned int type) { - // this defines the sorting order of types. types that are first in the vector appear before other types. - std::vector mapping; - mapping.emplace_back(typeid(ESM::Weapon).name() ); - mapping.emplace_back(typeid(ESM::Armor).name() ); - mapping.emplace_back(typeid(ESM::Clothing).name() ); - mapping.emplace_back(typeid(ESM::Potion).name() ); - mapping.emplace_back(typeid(ESM::Ingredient).name() ); - mapping.emplace_back(typeid(ESM::Apparatus).name() ); - mapping.emplace_back(typeid(ESM::Book).name() ); - mapping.emplace_back(typeid(ESM::Light).name() ); - mapping.emplace_back(typeid(ESM::Miscellaneous).name() ); - mapping.emplace_back(typeid(ESM::Lockpick).name() ); - mapping.emplace_back(typeid(ESM::Repair).name() ); - mapping.emplace_back(typeid(ESM::Probe).name() ); - - assert( std::find(mapping.begin(), mapping.end(), type1) != mapping.end() ); - assert( std::find(mapping.begin(), mapping.end(), type2) != mapping.end() ); - - return std::find(mapping.begin(), mapping.end(), type1) < std::find(mapping.begin(), mapping.end(), type2); + switch (type) + { + case ESM::Weapon::sRecordId: return 0; + case ESM::Armor::sRecordId: return 1; + case ESM::Clothing::sRecordId: return 2; + case ESM::Potion::sRecordId: return 3; + case ESM::Ingredient::sRecordId: return 4; + case ESM::Apparatus::sRecordId: return 5; + case ESM::Book::sRecordId: return 6; + case ESM::Light::sRecordId: return 7; + case ESM::Miscellaneous::sRecordId: return 8; + case ESM::Lockpick::sRecordId: return 9; + case ESM::Repair::sRecordId: return 10; + case ESM::Probe::sRecordId: return 11; + } + assert(false && "Invalid type value"); + return std::numeric_limits::max(); + } + + bool compareType(unsigned int type1, unsigned int type2) + { + return getTypeOrder(type1) < getTypeOrder(type2); } struct Compare @@ -62,15 +66,15 @@ namespace float result = 0; // compare items by type - std::string leftName = left.mBase.getTypeName(); - std::string rightName = right.mBase.getTypeName(); + auto leftType = left.mBase.getType(); + auto rightType = right.mBase.getType(); - if (leftName != rightName) - return compareType(leftName, rightName); + if (leftType != rightType) + return compareType(leftType, rightType); // compare items by name - leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); - rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); + std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) @@ -179,23 +183,29 @@ namespace MWGui MWWorld::Ptr base = item.mBase; int category = 0; - if (base.getTypeName() == typeid(ESM::Armor).name() - || base.getTypeName() == typeid(ESM::Clothing).name()) - category = Category_Apparel; - else if (base.getTypeName() == typeid(ESM::Weapon).name()) - category = Category_Weapon; - else if (base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Potion).name()) - category = Category_Magic; - else if (base.getTypeName() == typeid(ESM::Miscellaneous).name() - || base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Repair).name() - || base.getTypeName() == typeid(ESM::Lockpick).name() - || base.getTypeName() == typeid(ESM::Light).name() - || base.getTypeName() == typeid(ESM::Apparatus).name() - || base.getTypeName() == typeid(ESM::Book).name() - || base.getTypeName() == typeid(ESM::Probe).name()) - category = Category_Misc; + switch (base.getType()) + { + case ESM::Armor::sRecordId: + case ESM::Clothing::sRecordId: + category = Category_Apparel; + break; + case ESM::Weapon::sRecordId: + category = Category_Weapon; + break; + case ESM::Ingredient::sRecordId: + case ESM::Potion::sRecordId: + category = Category_Magic; + break; + case ESM::Miscellaneous::sRecordId: + case ESM::Repair::sRecordId: + case ESM::Lockpick::sRecordId: + case ESM::Light::sRecordId: + case ESM::Apparatus::sRecordId: + case ESM::Book::sRecordId: + case ESM::Probe::sRecordId: + category = Category_Misc; + break; + } if (item.mFlags & ItemStack::Flag_Enchanted) category |= Category_Magic; @@ -205,7 +215,7 @@ namespace MWGui if (mFilter & Filter_OnlyIngredients) { - if (base.getTypeName() != typeid(ESM::Ingredient).name()) + if (base.getType() != ESM::Ingredient::sRecordId) return false; if (!mNameFilter.empty() && !mEffectFilter.empty()) @@ -213,7 +223,7 @@ namespace MWGui if (!mNameFilter.empty()) { - const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); + const auto itemName = Utf8Stream::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } @@ -226,7 +236,7 @@ namespace MWGui for (const auto& effect : effects) { - const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); + const auto ciEffect = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; @@ -238,24 +248,24 @@ namespace MWGui if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; - if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getTypeName() != typeid(ESM::Miscellaneous).name() + if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getType() != ESM::Miscellaneous::sRecordId || base.getCellRef().getSoul() == "" || !MWBase::Environment::get().getWorld()->getStore().get().search(base.getCellRef().getSoul()))) return false; - if ((mFilter & Filter_OnlyRepairTools) && (base.getTypeName() != typeid(ESM::Repair).name())) + if ((mFilter & Filter_OnlyRepairTools) && (base.getType() != ESM::Repair::sRecordId)) return false; if ((mFilter & Filter_OnlyEnchantable) && (item.mFlags & ItemStack::Flag_Enchanted - || (base.getTypeName() != typeid(ESM::Armor).name() - && base.getTypeName() != typeid(ESM::Clothing).name() - && base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Book).name()))) + || (base.getType() != ESM::Armor::sRecordId + && base.getType() != ESM::Clothing::sRecordId + && base.getType() != ESM::Weapon::sRecordId + && base.getType() != ESM::Book::sRecordId))) return false; - if ((mFilter & Filter_OnlyEnchantable) && base.getTypeName() == typeid(ESM::Book).name() + if ((mFilter & Filter_OnlyEnchantable) && base.getType() == ESM::Book::sRecordId && !base.get()->mBase->mData.mIsScroll) return false; if ((mFilter & Filter_OnlyUsableItems) && base.getClass().getScript(base).empty()) { - std::shared_ptr actionOnUse = base.getClass().use(base); + std::unique_ptr actionOnUse = base.getClass().use(base); if (!actionOnUse || actionOnUse->isNullAction()) return false; } @@ -263,8 +273,8 @@ namespace MWGui if ((mFilter & Filter_OnlyRepairable) && ( !base.getClass().hasItemHealth(base) || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) - || (base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Armor).name()))) + || (base.getType() != ESM::Weapon::sRecordId + && base.getType() != ESM::Armor::sRecordId))) return false; if (mFilter & Filter_OnlyRechargable) @@ -285,7 +295,7 @@ namespace MWGui return false; } - std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if(compare.find(mNameFilter) == std::string::npos) return false; @@ -318,12 +328,12 @@ namespace MWGui void SortFilterItemModel::setNameFilter (const std::string& filter) { - mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mNameFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter (const std::string& filter) { - mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::update() diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index fa70a0edd7..64a01f71bd 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -35,20 +35,20 @@ namespace MWGui bool onDropItem(const MWWorld::Ptr &item, int count) override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; - static const int Category_Weapon = (1<<1); - static const int Category_Apparel = (1<<2); - static const int Category_Misc = (1<<3); - static const int Category_Magic = (1<<4); - static const int Category_All = 255; - - static const int Filter_OnlyIngredients = (1<<0); - static const int Filter_OnlyEnchanted = (1<<1); - static const int Filter_OnlyEnchantable = (1<<2); - static const int Filter_OnlyChargedSoulstones = (1<<3); - static const int Filter_OnlyUsableItems = (1<<4); // Only items with a Use action - static const int Filter_OnlyRepairable = (1<<5); - static const int Filter_OnlyRechargable = (1<<6); - static const int Filter_OnlyRepairTools = (1<<7); + static constexpr int Category_Weapon = (1<<1); + static constexpr int Category_Apparel = (1<<2); + static constexpr int Category_Misc = (1<<3); + static constexpr int Category_Magic = (1<<4); + static constexpr int Category_All = 255; + + static constexpr int Filter_OnlyIngredients = (1<<0); + static constexpr int Filter_OnlyEnchanted = (1<<1); + static constexpr int Filter_OnlyEnchantable = (1<<2); + static constexpr int Filter_OnlyChargedSoulstones = (1<<3); + static constexpr int Filter_OnlyUsableItems = (1<<4); // Only items with a Use action + static constexpr int Filter_OnlyRepairable = (1<<5); + static constexpr int Filter_OnlyRechargable = (1<<6); + static constexpr int Filter_OnlyRepairTools = (1<<7); private: diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index eb51f560be..9651513533 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,8 +15,9 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/spells.hpp" +#include "../mwmechanics/creaturestats.hpp" namespace MWGui { @@ -99,10 +102,8 @@ namespace MWGui std::vector spellsToSort; - for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) + for (const ESM::Spell* spell : merchantSpells) { - const ESM::Spell* spell = iter->first; - if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers @@ -115,10 +116,10 @@ namespace MWGui continue; } - if (playerHasSpell(iter->first->mId)) + if (playerHasSpell(spell->mId)) continue; - spellsToSort.push_back(iter->first); + spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index 622548c959..f46c437963 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.hpp +++ b/apps/openmw/mwgui/spellbuyingwindow.hpp @@ -15,11 +15,6 @@ namespace MyGUI class Widget; } -namespace MWGui -{ - class WindowManager; -} - namespace MWGui { class SpellBuyingWindow : public ReferenceInterface, public WindowBase diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 1f086507fd..6a8beb63fd 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -1,10 +1,14 @@ #include "spellcreationdialog.hpp" +#include #include #include +#include #include #include +#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -13,12 +17,12 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/store.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/spells.hpp" -#include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/spellutil.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "tooltips.hpp" #include "class.hpp" @@ -115,10 +119,6 @@ namespace MWGui { bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; - bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; - - if (!allowSelf && !allowTouch && !allowTarget) - return; // TODO: Show an error message popup? setMagicEffect(effect); mEditing = false; @@ -190,7 +190,8 @@ namespace MWGui void EditEffectDialog::setMagicEffect (const ESM::MagicEffect *effect) { - mEffectImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); + mEffectImage->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon, + MWBase::Environment::get().getResourceSystem()->getVFS())); mEffectName->setCaptionWithReplacing("#{"+ESM::MagicEffect::effectIdToString (effect->mIndex)+"}"); @@ -393,7 +394,8 @@ namespace MWGui MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - if (MyGUI::utility::parseInt(mPriceLabel->getCaption()) > playerGold) + int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); + if (price > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; @@ -401,8 +403,6 @@ namespace MWGui mSpell.mName = mNameEdit->getCaption(); - int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool @@ -457,7 +457,7 @@ namespace MWGui for (const ESM::ENAMstruct& effect : mEffects) { - y += std::max(1.f, MWMechanics::calcEffectCost(effect)); + y += std::max(1.f, MWMechanics::calcEffectCost(effect, nullptr, MWMechanics::EffectCostMethod::PlayerSpell)); if (effect.mRange == ESM::RT_Target) y *= 1.5; @@ -521,10 +521,8 @@ namespace MWGui std::vector knownEffects; - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; - // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; @@ -587,7 +585,7 @@ namespace MWGui mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); - mSelectAttributeDialog = 0; + mSelectAttributeDialog = nullptr; } void EffectEditorBase::onSelectSkill () @@ -598,7 +596,7 @@ namespace MWGui mAddEffectDialog.newEffect(effect); mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); - mSelectSkillDialog = 0; + mSelectSkillDialog = nullptr; } void EffectEditorBase::onAttributeOrSkillCancel () @@ -608,8 +606,8 @@ namespace MWGui if (mSelectAttributeDialog) MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); - mSelectSkillDialog = 0; - mSelectAttributeDialog = 0; + mSelectSkillDialog = nullptr; + mSelectAttributeDialog = nullptr; } void EffectEditorBase::onAvailableEffectClicked (MyGUI::Widget* sender) @@ -626,6 +624,13 @@ namespace MWGui const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; + bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; + + if (!allowSelf && !allowTouch && !allowTarget) + return; // TODO: Show an error message popup? + if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { delete mSelectSkillDialog; diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index 73352ac238..1dd16c33cd 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -1,8 +1,8 @@ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H -#include -#include +#include +#include #include "windowbase.hpp" #include "referenceinterface.hpp" diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index e6a10ee32b..0934feec1e 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -5,8 +5,10 @@ #include -#include +#include #include +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -24,50 +26,33 @@ namespace MWGui { - - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime, float totalTime) - { - MagicEffectInfo newEffectSource; - newEffectSource.mKey = key; - newEffectSource.mMagnitude = static_cast(magnitude); - newEffectSource.mPermanent = mIsPermanent; - newEffectSource.mRemainingTime = remainingTime; - newEffectSource.mSource = sourceName; - newEffectSource.mTotalTime = totalTime; - - mEffectSources[key.mId].push_back(newEffectSource); - } - - void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) { - // TODO: Tracking add/remove/expire would be better than force updating every frame - MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - - EffectSourceVisitor visitor; - - // permanent item enchantments & permanent spells - visitor.mIsPermanent = true; - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - store.visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - - // now add lasting effects - visitor.mIsPermanent = false; - stats.getActiveSpells().visitEffectSources(visitor); - - std::map >& effects = visitor.mEffectSources; + std::map> effects; + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + MagicEffectInfo newEffectSource; + newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg); + newEffectSource.mMagnitude = static_cast(effect.mMagnitude); + newEffectSource.mPermanent = effect.mDuration == -1.f; + newEffectSource.mRemainingTime = effect.mTimeLeft; + newEffectSource.mSource = params.getDisplayName(); + newEffectSource.mTotalTime = effect.mDuration; + effects[effect.mEffectId].push_back(newEffectSource); + } + } int w=2; - for (auto& effectInfoPair : effects) + for (const auto& [effectId, effectInfos] : effects) { - const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); @@ -78,7 +63,6 @@ namespace MWGui static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); - std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { @@ -152,7 +136,8 @@ namespace MWGui ("ImageBox", MyGUI::IntCoord(w,2,16,16), MyGUI::Align::Default); mWidgetMap[effectId] = image; - image->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); + image->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon, + MWBase::Environment::get().getResourceSystem()->getVFS())); std::string name = ESM::MagicEffect::effectIdToString (effectId); @@ -181,7 +166,9 @@ namespace MWGui } else if (mWidgetMap.find(effectId) != mWidgetMap.end()) { - mWidgetMap[effectId]->setVisible(false); + MyGUI::ImageBox* image = mWidgetMap[effectId]; + image->setVisible(false); + image->setAlpha(1.f); } } diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index b6aa49e69e..9825162a33 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -37,20 +37,6 @@ namespace MWGui bool mPermanent; // the effect is permanent }; - class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor - { - public: - bool mIsPermanent; - - std::map > mEffectSources; - - virtual ~EffectSourceVisitor() {} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - }; - class SpellIcons { public: diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 1dedfa10b1..455f167415 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -1,6 +1,7 @@ #include "spellmodel.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -42,6 +43,44 @@ namespace MWGui { } + bool SpellModel::matchingEffectExists(std::string filter, const ESM::EffectList &effects) + { + auto wm = MWBase::Environment::get().getWindowManager(); + const MWWorld::ESMStore &store = + MWBase::Environment::get().getWorld()->getStore(); + + for (const auto& effect : effects.mList) + { + short effectId = effect.mEffectID; + + if (effectId != -1) + { + const ESM::MagicEffect *magicEffect = + store.get().search(effectId); + std::string effectIDStr = ESM::MagicEffect::effectIdToString(effectId); + std::string fullEffectName = wm->getGameSettingString(effectIDStr, ""); + + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && effect.mSkill != -1) + { + fullEffectName += " " + wm->getGameSettingString(ESM::Skill::sSkillNameIds[effect.mSkill], ""); + } + + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && effect.mAttribute != -1) + { + fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); + } + + std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); + if (convert.find(filter) != std::string::npos) + { + return true; + } + } + } + + return false; + } + void SpellModel::update() { mSpells.clear(); @@ -52,17 +91,17 @@ namespace MWGui const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); - std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); - - if (name.find(filter) == std::string::npos) + std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); + + if (name.find(filter) == std::string::npos + && !matchingEffectExists(filter, spell->mEffects)) continue; Spell newSpell; @@ -70,7 +109,7 @@ namespace MWGui if (spell->mData.mType == ESM::Spell::ST_Spell) { newSpell.mType = Spell::Type_Spell; - std::string cost = std::to_string(spell->mData.mCost); + std::string cost = std::to_string(MWMechanics::calcSpellCost(*spell)); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); newSpell.mCostColumn = cost + "/" + chance; } @@ -101,9 +140,10 @@ namespace MWGui if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); - if (name.find(filter) == std::string::npos) + if (name.find(filter) == std::string::npos + && !matchingEffectExists(filter, enchant->mEffects)) continue; Spell newSpell; diff --git a/apps/openmw/mwgui/spellmodel.hpp b/apps/openmw/mwgui/spellmodel.hpp index d191cba0e0..af8000c278 100644 --- a/apps/openmw/mwgui/spellmodel.hpp +++ b/apps/openmw/mwgui/spellmodel.hpp @@ -2,6 +2,7 @@ #define OPENMW_GUI_SPELLMODEL_H #include "../mwworld/ptr.hpp" +#include namespace MWGui { @@ -57,6 +58,8 @@ namespace MWGui std::vector mSpells; std::string mFilter; + + bool matchingEffectExists(std::string filter, const ESM::EffectList &effects); }; } diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index d76a59820e..873037db10 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -1,9 +1,7 @@ #include "spellwindow.hpp" -#include #include #include -#include #include #include diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index cf5e88f8e0..786a7d877f 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -2,7 +2,6 @@ #define MWGUI_SPELLWINDOW_H #include "windowpinnablebase.hpp" -#include "../mwworld/ptr.hpp" #include "spellmodel.hpp" diff --git a/apps/openmw/mwgui/statswatcher.cpp b/apps/openmw/mwgui/statswatcher.cpp index ccb77de8f2..883fd265cc 100644 --- a/apps/openmw/mwgui/statswatcher.cpp +++ b/apps/openmw/mwgui/statswatcher.cpp @@ -11,6 +11,8 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include + namespace MWGui { // mWatchedTimeToStartDrowning = -1 for correct drowning state check, @@ -36,11 +38,8 @@ namespace MWGui { if (stats.getAttribute(i) != mWatchedAttributes[i] || mWatchedStatsEmpty) { - std::stringstream attrname; - attrname << "AttribVal"<<(i+1); - mWatchedAttributes[i] = stats.getAttribute(i); - setValue(attrname.str(), stats.getAttribute(i)); + setValue("AttribVal" + std::to_string(i + 1), stats.getAttribute(i)); } } diff --git a/apps/openmw/mwgui/statswatcher.hpp b/apps/openmw/mwgui/statswatcher.hpp index 41ab4fd252..6262a50565 100644 --- a/apps/openmw/mwgui/statswatcher.hpp +++ b/apps/openmw/mwgui/statswatcher.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "../mwmechanics/stat.hpp" @@ -63,6 +63,8 @@ namespace MWGui void watchActor(const MWWorld::Ptr& ptr); MWWorld::Ptr getWatchedActor() const { return mWatched; } + + void forceUpdate() { mWatchedStatsEmpty = true; } }; } diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 2a3e2cd85c..f3d98a4fcd 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -92,7 +94,7 @@ namespace MWGui int windowHeight = window->getSize().height; //initial values defined in openmw_stats_window.layout, if custom options are not present in .layout, a default is loaded - float leftPaneRatio = 0.44; + float leftPaneRatio = 0.44f; if (mLeftPane->isUserString("LeftPaneRatio")) leftPaneRatio = MyGUI::utility::parseFloat(mLeftPane->getUserString("LeftPaneRatio")); @@ -178,7 +180,7 @@ namespace MWGui void StatsWindow::setValue (const std::string& id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); - int modified = static_cast(value.getModified()); + int modified = static_cast(value.getModified(false)); // Fatigue can be negative if (id != "FBar") @@ -335,11 +337,23 @@ namespace MWGui { int max = MWBase::Environment::get().getWorld()->getStore().get().find("iLevelUpTotal")->mValue.getInteger(); getWidget(levelWidget, i==0 ? "Level_str" : "LevelText"); + levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max)); levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" + MyGUI::utility::toString(max)); } + std::stringstream detail; + for (int attribute = 0; attribute < ESM::Attribute::Length; ++attribute) + { + float mult = PCstats.getLevelupAttributeMultiplier(attribute); + mult = std::min(mult, 100 - PCstats.getAttribute(attribute).getBase()); + if (mult > 1) + detail << (detail.str().empty() ? "" : "\n") << "#{" + << MyGUI::TextIterator::toTagsString(ESM::Attribute::sGmstAttributeIds[attribute]) + << "} x" << MyGUI::utility::toString(mult); + } + levelWidget->setUserString("Caption_LevelDetailText", MyGUI::LanguageManager::getInstance().replaceTags(detail.str())); setFactions(PCstats.getFactionRanks()); setExpelled(PCstats.getExpelled ()); @@ -586,8 +600,7 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - int rank = factionPair.second; - rank = std::max(0, std::min(9, rank)); + const int rank = std::clamp(factionPair.second, 0, 9); text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index 24f302580e..bf78cde34a 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -6,8 +6,6 @@ namespace MWGui { - class WindowManager; - class StatsWindow : public WindowPinnableBase, public NoDrop, public StatsListener { public: diff --git a/apps/openmw/mwgui/textinput.hpp b/apps/openmw/mwgui/textinput.hpp index 84d9d032da..4d365eb44c 100644 --- a/apps/openmw/mwgui/textinput.hpp +++ b/apps/openmw/mwgui/textinput.hpp @@ -3,11 +3,6 @@ #include "windowbase.hpp" -namespace MWGui -{ - class WindowManager; -} - namespace MWGui { class TextInputDialog : public WindowModal diff --git a/apps/openmw/mwgui/timeadvancer.cpp b/apps/openmw/mwgui/timeadvancer.cpp index a07da16825..c38094ae45 100644 --- a/apps/openmw/mwgui/timeadvancer.cpp +++ b/apps/openmw/mwgui/timeadvancer.cpp @@ -58,12 +58,12 @@ namespace MWGui } } - int TimeAdvancer::getHours() + int TimeAdvancer::getHours() const { return mHours; } - bool TimeAdvancer::isRunning() + bool TimeAdvancer::isRunning() const { return mRunning; } diff --git a/apps/openmw/mwgui/timeadvancer.hpp b/apps/openmw/mwgui/timeadvancer.hpp index 8367b5a8b2..40fce978b9 100644 --- a/apps/openmw/mwgui/timeadvancer.hpp +++ b/apps/openmw/mwgui/timeadvancer.hpp @@ -1,7 +1,7 @@ #ifndef MWGUI_TIMEADVANCER_H #define MWGUI_TIMEADVANCER_H -#include +#include namespace MWGui { @@ -14,8 +14,8 @@ namespace MWGui void stop(); void onFrame(float dt); - int getHours(); - bool isRunning(); + int getHours() const; + bool isRunning() const; // signals typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index c0db57b1b1..0b37e97245 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -9,6 +9,7 @@ #include #include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -122,7 +123,7 @@ namespace MWGui info.caption = mFocusObject.getClass().getName(mFocusObject); if (info.caption.empty()) info.caption=mFocusObject.getCellRef().getRefId(); - info.icon=""; + info.icon.clear(); tooltipSize = createToolTip(info, checkOwned()); } else @@ -153,20 +154,18 @@ namespace MWGui return; MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); - if (focus == 0) + if (focus == nullptr) return; MyGUI::IntSize tooltipSize; // try to go 1 level up until there is a widget that has tooltip // this is necessary because some skin elements are actually separate widgets - int i=0; while (!focus->isUserString("ToolTipType")) { focus = focus->getParent(); if (!focus) return; - ++i; } std::string type = focus->getUserString("ToolTipType"); @@ -251,7 +250,7 @@ namespace MWGui } std::string cost = focus->getUserString("SpellCost"); if (cost != "" && cost != "0") - info.text += MWGui::ToolTips::getValueString(spell->mData.mCost, "#{sCastCost}"); + info.text += MWGui::ToolTips::getValueString(MWMechanics::calcSpellCost(*spell), "#{sCastCost}"); info.effects = effects; tooltipSize = createToolTip(info); } @@ -266,14 +265,14 @@ namespace MWGui std::map userStrings = focus->getUserStrings(); for (auto& userStringPair : userStrings) { - size_t underscorePos = userStringPair.first.find("_"); + size_t underscorePos = userStringPair.first.find('_'); if (underscorePos == std::string::npos) continue; std::string key = userStringPair.first.substr(0, underscorePos); std::string widgetName = userStringPair.first.substr(underscorePos+1, userStringPair.first.size()-(underscorePos+1)); type = "Property"; - size_t caretPos = key.find("^"); + size_t caretPos = key.find('^'); if (caretPos != std::string::npos) { type = key.substr(0, caretPos); @@ -373,7 +372,7 @@ namespace MWGui ToolTipInfo info = object.getToolTipInfo(mFocusObject, count); if (!image) - info.icon = ""; + info.icon.clear(); tooltipSize = createToolTip(info, isOwned); } @@ -410,7 +409,7 @@ namespace MWGui if (text.size() > 0 && text[0] == '\n') text.erase(0, 1); - const ESM::Enchantment* enchant = 0; + const ESM::Enchantment* enchant = nullptr; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (info.enchant != "") { @@ -438,7 +437,8 @@ namespace MWGui const int maximumWidth = MyGUI::RenderManager::getInstance().getViewSize().width - imageCaptionHPadding * 2; - std::string realImage = MWBase::Environment::get().getWindowManager()->correctIconPath(image); + const std::string realImage = Misc::ResourceHelpers::correctIconPath(image, + MWBase::Environment::get().getResourceSystem()->getVFS()); Gui::EditBox* captionWidget = mDynamicToolTipBox->createWidget("NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setEditStatic(true); @@ -493,6 +493,7 @@ namespace MWGui std::vector effectItems; int flag = info.isPotion ? Widgets::MWEffectList::EF_NoTarget : 0; flag |= info.isIngredient ? Widgets::MWEffectList::EF_NoMagnitude : 0; + flag |= info.isIngredient ? Widgets::MWEffectList::EF_Constant : 0; effectsWidget->createEffectWidgets(effectItems, effectArea, coord, true, flag); totalSize.height += coord.top-6; totalSize.width = std::max(totalSize.width, coord.width); @@ -649,7 +650,7 @@ namespace MWGui std::string ToolTips::getSoulString(const MWWorld::CellRef& cellref) { - std::string soul = cellref.getSoul(); + const std::string& soul = cellref.getSoul(); if (soul.empty()) return std::string(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -665,7 +666,7 @@ namespace MWGui { std::string ret; ret += getMiscString(cellref.getOwner(), "Owner"); - const std::string factionId = cellref.getFaction(); + const std::string& factionId = cellref.getFaction(); if (!factionId.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -844,10 +845,11 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore(); const ESM::BirthSign *sign = store.get().find(birthsignId); + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "BirthSignToolTip"); - widget->setUserString("ImageTexture_BirthSignImage", MWBase::Environment::get().getWindowManager()->correctTexturePath(sign->mTexture)); + widget->setUserString("ImageTexture_BirthSignImage", Misc::ResourceHelpers::correctTexturePath(sign->mTexture, vfs)); std::string text; text += sign->mName; @@ -939,7 +941,7 @@ namespace MWGui std::string icon = effect->mIcon; int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); + icon = Misc::ResourceHelpers::correctIconPath(icon, MWBase::Environment::get().getResourceSystem()->getVFS()); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "MagicEffectToolTip"); diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 81d6a8ab30..9222e1444d 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -26,7 +26,6 @@ #include "containeritemmodel.hpp" #include "tradeitemmodel.hpp" #include "countdialog.hpp" -#include "controllers.hpp" #include "tooltips.hpp" namespace @@ -266,6 +265,8 @@ namespace MWGui const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; + // were there any items traded at all? const std::vector& playerBought = playerItemModel->getItemsBorrowedToUs(); const std::vector& merchantBought = mTradeModel->getItemsBorrowedToUs(); @@ -406,10 +407,15 @@ namespace MWGui void TradeWindow::onBalanceValueChanged(int value) { + int previousBalance = mCurrentBalance; + // Entering a "-" sign inverts the buying/selling state mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; updateLabels(); + if (mCurrentBalance == 0) + mCurrentBalance = previousBalance; + if (value != std::abs(value)) mTotalBalance->setValue(std::abs(value)); } @@ -419,6 +425,7 @@ namespace MWGui // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined if (mCurrentBalance == std::numeric_limits::max() || mCurrentBalance == std::numeric_limits::min()+1) return; + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance -= 1; else mCurrentBalance += 1; updateLabels(); @@ -426,6 +433,7 @@ namespace MWGui void TradeWindow::onDecreaseButtonTriggered() { + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance += 1; else mCurrentBalance -= 1; updateLabels(); @@ -435,9 +443,17 @@ namespace MWGui { MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + MyGUI::utility::toString(playerGold)); + TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + const std::vector& playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); + const std::vector& merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); + + if (playerBorrowed.empty() && merchantBorrowed.empty()) + { + mCurrentBalance = 0; + } + if (mCurrentBalance < 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); @@ -524,4 +540,10 @@ namespace MWGui return; resetReference(); } + + void TradeWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + if(mTradeModel && mTradeModel->usesContainer(ptr)) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); + } } diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index f82d7b0f72..5ace09e8e2 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -42,6 +42,8 @@ namespace MWGui void resetReference() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 7fae33bae5..96267cc7b2 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -1,5 +1,6 @@ #include "trainingwindow.hpp" +#include #include #include "../mwbase/windowmanager.hpp" diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index ed7a74b95f..fcdc5775d1 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -5,20 +5,23 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/actorutil.hpp" - #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/actionteleport.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/cellutils.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/esmstore.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" namespace MWGui { @@ -44,7 +47,7 @@ namespace MWGui mSelect->getHeight()); } - void TravelWindow::addDestination(const std::string& name, ESM::Position pos, bool interior) + void TravelWindow::addDestination(const std::string& name, const ESM::Position &pos, bool interior) { int price; @@ -74,7 +77,7 @@ namespace MWGui // Add price for the travelling followers std::set followers; - MWWorld::ActionTeleport::getFollowers(player, followers); + MWWorld::ActionTeleport::getFollowers(player, followers, !interior); // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); @@ -115,19 +118,17 @@ namespace MWGui std::vector transport; if (mPtr.getClass().isNpc()) transport = mPtr.get()->mBase->getTransport(); - else if (mPtr.getTypeName() == typeid(ESM::Creature).name()) + else if (mPtr.getType() == ESM::Creature::sRecordId) transport = mPtr.get()->mBase->getTransport(); for(unsigned int i = 0;ipositionToIndex(transport[i].mPos.pos[0], - transport[i].mPos.pos[1],x,y); + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(transport[i].mPos.pos[0], transport[i].mPos.pos[1]); if (cellname == "") { - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(x,y); + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cellIndex.x(), cellIndex.y()); cellname = MWBase::Environment::get().getWorld()->getCellName(cell); interior = false; } diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 962d17161d..dd970ee10e 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -11,12 +11,6 @@ namespace MyGUI class Widget; } -namespace MWGui -{ - class WindowManager; -} - - namespace MWGui { class TravelWindow : public ReferenceInterface, public WindowBase @@ -37,7 +31,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onTravelButtonClick(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void addDestination(const std::string& name, ESM::Position pos, bool interior); + void addDestination(const std::string& name, const ESM::Position& pos, bool interior); void clearDestinations(); int mCurrentY; diff --git a/apps/openmw/mwgui/videowidget.cpp b/apps/openmw/mwgui/videowidget.cpp index 2aea0018d5..a973db3a61 100644 --- a/apps/openmw/mwgui/videowidget.cpp +++ b/apps/openmw/mwgui/videowidget.cpp @@ -18,7 +18,7 @@ namespace MWGui VideoWidget::VideoWidget() : mVFS(nullptr) { - mPlayer.reset(new Video::VideoPlayer()); + mPlayer = std::make_unique(); setNeedKeyFocus(true); } @@ -44,13 +44,13 @@ void VideoWidget::playVideo(const std::string &video) return; } - mPlayer->playVideo(videoStream, video); + mPlayer->playVideo(std::move(videoStream), video); osg::ref_ptr texture = mPlayer->getVideoTexture(); if (!texture) return; - mTexture.reset(new osgMyGUI::OSGTexture(texture)); + mTexture = std::make_unique(texture); setRenderItemTexture(mTexture.get()); getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 18cc187c15..53194e4f3f 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -23,8 +23,6 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" -#include "../mwstate/charactermanager.hpp" - namespace MWGui { @@ -100,12 +98,22 @@ namespace MWGui bool WaitDialog::exit() { - return (!mTimeAdvancer.isRunning()); //Only exit if not currently waiting + bool canExit = !mTimeAdvancer.isRunning(); // Only exit if not currently waiting + if (canExit) + { + clear(); + stopWaiting(); + } + return canExit; } void WaitDialog::clear() { mSleeping = false; + mHours = 1; + mManualHours = 1; + mFadeTimeRemaining = 0; + mInterruptAt = -1; mTimeAdvancer.stop(); } @@ -151,9 +159,9 @@ namespace MWGui if (hour == 0) hour = 12; ESM::EpochTimeStamp currentDate = MWBase::Environment::get().getWorld()->getEpochTimeStamp(); - int daysPassed = MWBase::Environment::get().getWorld()->getTimeStamp().getDay(); - std::string formattedHour = pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"; - std::string dateTimeText = Misc::StringUtils::format("%i %s (#{sDay} %i) %i %s", currentDate.mDay, month, daysPassed, hour, formattedHour); + std::string daysPassed = Misc::StringUtils::format("(#{sDay} %i)", MWBase::Environment::get().getWorld()->getTimeStamp().getDay()); + std::string formattedHour(pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"); + std::string dateTimeText = Misc::StringUtils::format("%i %s %s %i %s", currentDate.mDay, month, daysPassed, hour, formattedHour); mDateTimeText->setCaptionWithReplacing (dateTimeText); } @@ -193,7 +201,7 @@ namespace MWGui if (!region->mSleepList.empty()) { // figure out if player will be woken while sleeping - int x = Misc::Rng::rollDice(hoursToWait); + int x = Misc::Rng::rollDice(hoursToWait, world->getPrng()); float fSleepRandMod = world->getStore().get().find("fSleepRandMod")->mValue.getFloat(); if (x < fSleepRandMod * hoursToWait) { diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index 74076641a9..97d70240c0 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -1,12 +1,15 @@ #include "widgets.hpp" -#include #include +#include #include #include #include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" @@ -15,9 +18,7 @@ #include "controllers.hpp" -namespace MWGui -{ - namespace Widgets +namespace MWGui::Widgets { /* MWSkill */ @@ -101,7 +102,7 @@ namespace MWGui button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); } - button = 0; + button = nullptr; assignWidget(button, "StatValueButton"); if (button) { @@ -192,7 +193,7 @@ namespace MWGui button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); } - button = 0; + button = nullptr; assignWidget(button, "StatValueButton"); if (button) { @@ -472,7 +473,7 @@ namespace MWGui mTextWidget->setCaptionWithReplacing(spellLine); mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; - mImageWidget->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(magicEffect->mIcon)); + mImageWidget->setImageTexture(Misc::ResourceHelpers::correctIconPath(magicEffect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); } MWSpellEffect::~MWSpellEffect() @@ -535,4 +536,3 @@ namespace MWGui assignWidget(mBarTextWidget, "BarText"); } } -} diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index 731a41a354..61d1ced750 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -3,12 +3,12 @@ #include "../mwmechanics/stat.hpp" -#include -#include +#include +#include +#include -#include -#include -#include +#include +#include namespace MyGUI { @@ -181,8 +181,6 @@ namespace MWGui public: MWSpell(); - typedef MWMechanics::Stat SpellValue; - void setSpellId(const std::string &id); /** @@ -215,8 +213,6 @@ namespace MWGui public: MWEffectList(); - typedef MWMechanics::Stat EnchantmentValue; - enum EffectFlags { EF_NoTarget = 0x01, // potions have no target (target is always the player) @@ -268,7 +264,7 @@ namespace MWGui void initialiseOverride() override; private: - static const int sIconOffset = 24; + static constexpr int sIconOffset = 24; void updateWidgets(); diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 84e557fcdc..9e476e4dab 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -14,7 +14,7 @@ using namespace MWGui; -WindowBase::WindowBase(const std::string& parLayout) +WindowBase::WindowBase(std::string_view parLayout) : Layout(parLayout) { mMainWidget->setVisible(false); @@ -139,12 +139,12 @@ void NoDrop::setAlpha(float alpha) mWidget->setAlpha(alpha); } -BookWindowBase::BookWindowBase(const std::string& parLayout) +BookWindowBase::BookWindowBase(std::string_view parLayout) : WindowBase(parLayout) { } -float BookWindowBase::adjustButton (char const * name) +float BookWindowBase::adjustButton(std::string_view name) { Gui::ImageButton* button; WindowBase::getWidget (button, name); diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 8afb2321e8..dde9190666 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -3,11 +3,6 @@ #include "layout.hpp" -namespace MWBase -{ - class WindowManager; -} - namespace MWWorld { class Ptr; @@ -15,13 +10,12 @@ namespace MWWorld namespace MWGui { - class WindowManager; class DragAndDrop; class WindowBase: public Layout { public: - WindowBase(const std::string& parLayout); + WindowBase(std::string_view parLayout); virtual MyGUI::Widget* getDefaultKeyFocus() { return nullptr; } @@ -53,6 +47,8 @@ namespace MWGui /// Called when GUI viewport changes size virtual void onResChange(int width, int height) {} + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) {} + protected: virtual void onTitleDoubleClicked(); @@ -92,10 +88,10 @@ namespace MWGui class BookWindowBase : public WindowBase { public: - BookWindowBase(const std::string& parLayout); + BookWindowBase(std::string_view parLayout); protected: - float adjustButton (char const * name); + float adjustButton(std::string_view name); }; } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 840f0f9cfa..717e8375b0 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1,21 +1,20 @@ #include "windowmanagerimp.hpp" +#include #include #include +#include #include #include #include -#include -#include #include #include #include #include #include #include -#include // For BT_NO_PROFILE #include @@ -28,13 +27,14 @@ #include #include -#include -#include +#include +#include #include #include #include +#include #include @@ -50,8 +50,12 @@ #include #include +#include + +#include #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" @@ -63,11 +67,11 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/stat.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwrender/localmap.hpp" +#include "../mwrender/postprocessor.hpp" #include "console.hpp" #include "journalwindow.hpp" @@ -110,6 +114,7 @@ #include "itemwidget.hpp" #include "screenfader.hpp" #include "debugwindow.hpp" +#include "postprocessorhud.hpp" #include "spellview.hpp" #include "draganddrop.hpp" #include "container.hpp" @@ -123,8 +128,8 @@ namespace MWGui { WindowManager::WindowManager( SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath) + const std::string& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, + ToUTF8::FromType encoding, const std::string& versionDescription, bool useShaders) : mOldUpdateMask(0) , mOldCullMask(0) , mStore(nullptr) @@ -161,7 +166,9 @@ namespace MWGui , mHitFader(nullptr) , mScreenFader(nullptr) , mDebugWindow(nullptr) + , mPostProcessorHud(nullptr) , mJailScreen(nullptr) + , mContainerWindow(nullptr) , mTranslationDataStorage (translationDataStorage) , mCharGen(nullptr) , mInputBlocker(nullptr) @@ -171,7 +178,7 @@ namespace MWGui , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI")) , mHudEnabled(true) , mCursorVisible(true) - , mCursorActive(false) + , mCursorActive(true) , mPlayerBounty(-1) , mGui(nullptr) , mGuiModes() @@ -186,9 +193,9 @@ namespace MWGui , mVersionDescription(versionDescription) , mWindowVisible(true) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), uiScale); - mGuiPlatform->initialise(resourcePath, logpath); + mScalingFactor = std::clamp(Settings::Manager::getFloat("scaling factor", "GUI"), 0.5f, 8.f); + mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), resourceSystem->getVFS(), mScalingFactor); + mGuiPlatform->initialise("mygui", (std::filesystem::path(logpath) / "MyGUI.log").generic_string()); mGui = new MyGUI::Gui; mGui->initialise(""); @@ -198,8 +205,8 @@ namespace MWGui MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath)); - mFontLoader->loadBitmapFonts(exportFonts); + mFontLoader = std::make_unique(encoding, resourceSystem->getVFS(), mScalingFactor); + mFontLoader->loadBitmapFonts(); //Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); @@ -213,22 +220,24 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); - BookPage::registerMyGUIComponents (); + BookPage::registerMyGUIComponents(); + PostProcessorHud::registerMyGUIComponents(); ItemView::registerComponents(); ItemChargeView::registerComponents(); ItemWidget::registerComponents(); SpellView::registerComponents(); Gui::registerAllWidgets(); + LuaUi::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "AutoSizedResourceSkin"); MyGUI::ResourceManager::getInstance().load("core.xml"); - WindowManager::loadUserFonts(); + mFontLoader->loadTrueTypeFonts(); bool keyboardNav = Settings::Manager::getBool("keyboard navigation", "GUI"); - mKeyboardNavigation.reset(new KeyboardNavigation()); + mKeyboardNavigation = std::make_unique(); mKeyboardNavigation->setEnabled(keyboardNav); Gui::ImageButton::setDefaultNeedKeyFocus(keyboardNav); @@ -251,7 +260,7 @@ namespace MWGui MyGUI::PointerManager::getInstance().setVisible(false); mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Default, "InputBlocker"); + MyGUI::Align::Default, "Video"); mVideoBackground->setImageTexture("black"); mVideoBackground->setVisible(false); mVideoBackground->setNeedMouseFocus(true); @@ -275,12 +284,10 @@ namespace MWGui mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); - mStatsWatcher.reset(new StatsWatcher()); - } + if (useShaders) + mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); - void WindowManager::loadUserFonts() - { - mFontLoader->loadTrueTypeFonts(); + mStatsWatcher = std::make_unique(); } void WindowManager::initUI() @@ -353,10 +360,10 @@ namespace MWGui mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow); mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete); - ContainerWindow* containerWindow = new ContainerWindow(mDragAndDrop); - mWindows.push_back(containerWindow); - trackWindow(containerWindow, "container"); - mGuiModeStates[GM_Container] = GuiModeState({containerWindow, mInventoryWindow}); + mContainerWindow = new ContainerWindow(mDragAndDrop); + mWindows.push_back(mContainerWindow); + trackWindow(mContainerWindow, "container"); + mGuiModeStates[GM_Container] = GuiModeState({mContainerWindow, mInventoryWindow}); mHud = new HUD(mCustomMarkers, mDragAndDrop, mLocalMapRender); mWindows.push_back(mHud); @@ -380,6 +387,7 @@ namespace MWGui mSettingsWindow = new SettingsWindow(); mWindows.push_back(mSettingsWindow); + trackWindow(mSettingsWindow, "settings"); mGuiModeStates[GM_Settings] = GuiModeState(mSettingsWindow); mConfirmationDialog = new ConfirmationDialog(); @@ -460,6 +468,10 @@ namespace MWGui mDebugWindow = new DebugWindow(); mWindows.push_back(mDebugWindow); + mPostProcessorHud = new PostProcessorHud(); + mWindows.push_back(mPostProcessorHud); + trackWindow(mPostProcessorHud, "postprocessor"); + mInputBlocker = MyGUI::Gui::getInstance().createWidget("",0,0,w,h,MyGUI::Align::Stretch,"InputBlocker"); mHud->setVisible(true); @@ -494,15 +506,17 @@ namespace MWGui } else allow(GW_ALL); + + mStatsWatcher->forceUpdate(); } WindowManager::~WindowManager() { try { - mStatsWatcher.reset(); + LuaUi::clearUserInterface(); - mKeyboardNavigation.reset(); + mStatsWatcher.reset(); MyGUI::LanguageManager::getInstance().eventRequestTag.clear(); MyGUI::PointerManager::getInstance().eventChangeMousePointer.clear(); @@ -522,6 +536,8 @@ namespace MWGui delete mCursorManager; delete mToolTips; + mKeyboardNavigation.reset(); + cleanupGarbage(); mFontLoader.reset(); @@ -560,17 +576,17 @@ namespace MWGui void WindowManager::enableScene(bool enable) { unsigned int disablemask = MWRender::Mask_GUI|MWRender::Mask_PreCompile; - if (!enable && mViewer->getCamera()->getCullMask() != disablemask) + if (!enable && getCullMask() != disablemask) { mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask(); - mOldCullMask = mViewer->getCamera()->getCullMask(); + mOldCullMask = getCullMask(); mViewer->getUpdateVisitor()->setTraversalMask(disablemask); - mViewer->getCamera()->setCullMask(disablemask); + setCullMask(disablemask); } - else if (enable && mViewer->getCamera()->getCullMask() == disablemask) + else if (enable && getCullMask() == disablemask) { mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask); - mViewer->getCamera()->setCullMask(mOldCullMask); + setCullMask(mOldCullMask); } } @@ -629,6 +645,7 @@ namespace MWGui mMap->setVisible(false); mStatsWindow->setVisible(false); mSpellWindow->setVisible(false); + mHud->setDrowningBarVisible(false); mInventoryWindow->setVisible(getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); } @@ -710,12 +727,11 @@ namespace MWGui if (block) { - osg::Timer frameTimer; + Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mMessageBoxManager->readPressedButton(false) == -1 && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { - double dt = frameTimer.time_s(); - frameTimer.setStartTick(); + const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); mKeyboardNavigation->onFrame(); mMessageBoxManager->onFrame(dt); @@ -734,7 +750,7 @@ namespace MWGui // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); - MWBase::Environment::get().limitFrameRate(frameTimer.time_s()); + frameRateLimiter.limit(); } } } @@ -748,6 +764,11 @@ namespace MWGui } } + void WindowManager::scheduleMessageBox(std::string message, enum MWGui::ShowInDialogueMode showInDialogueMode) + { + mScheduledMessageBoxes.lock()->emplace_back(std::move(message), showInDialogueMode); + } + void WindowManager::staticMessageBox(const std::string& message) { mMessageBoxManager->createMessageBox(message, true); @@ -758,6 +779,11 @@ namespace MWGui mMessageBoxManager->removeStaticMessageBox(); } + const std::vector WindowManager::getActiveMessageBoxes() + { + return mMessageBoxManager->getActiveMessageBoxes(); + } + int WindowManager::readPressedButton () { return mMessageBoxManager->readPressedButton(); @@ -802,6 +828,8 @@ namespace MWGui void WindowManager::update (float frameDuration) { + handleScheduledMessageBoxes(); + bool gameRunning = MWBase::Environment::get().getStateManager()->getState()!= MWBase::StateManager::State_NoGame; @@ -851,6 +879,8 @@ namespace MWGui if (mLocalMapRender) mLocalMapRender->cleanupCameras(); + mDebugWindow->onFrame(frameDuration); + if (!gameRunning) return; @@ -870,7 +900,7 @@ namespace MWGui mHud->onFrame(frameDuration); - mDebugWindow->onFrame(frameDuration); + mPostProcessorHud->onFrame(frameDuration); if (mCharGen) mCharGen->onFrame(frameDuration); @@ -1014,8 +1044,9 @@ namespace MWGui if(tag.compare(0, MyGuiPrefixLength, MyGuiPrefix) == 0) { tag = tag.substr(MyGuiPrefixLength, tag.length()); - std::string settingSection = tag.substr(0, tag.find(",")); - std::string settingTag = tag.substr(tag.find(",")+1, tag.length()); + size_t comma_pos = tag.find(','); + std::string settingSection = tag.substr(0, comma_pos); + std::string settingTag = tag.substr(comma_pos+1, tag.length()); _result = Settings::Manager::getString(settingTag, settingSection); } @@ -1030,12 +1061,27 @@ namespace MWGui } else { + std::vector split; + Misc::StringUtils::split(tag, split, ":"); + + // TODO: LocalizationManager should not be a part of lua + const auto& luaManager = MWBase::Environment::get().getLuaManager(); + + // If a key has a "Context:KeyName" format, use YAML to translate data + if (split.size() == 2 && luaManager != nullptr) + { + _result = luaManager->translate(split[0], split[1]); + return; + } + + // If not, treat is as GMST name from legacy localization if (!mStore) { Log(Debug::Error) << "Error: WindowManager::onRetrieveTag: no Store set up yet, can not replace '" << tag << "'"; + _result = tag; return; } - const ESM::GameSetting *setting = mStore->get().find(tag); + const ESM::GameSetting *setting = mStore->get().search(tag); if (setting && setting->mValue.getType()==ESM::VT_String) _result = setting->mValue.getString(); @@ -1060,7 +1106,7 @@ namespace MWGui else if (setting.first == "Video" && ( setting.second == "resolution x" || setting.second == "resolution y" - || setting.second == "fullscreen" + || setting.second == "window mode" || setting.second == "window border")) changeRes = true; @@ -1075,19 +1121,28 @@ namespace MWGui { mVideoWrapper->setVideoMode(Settings::Manager::getInt("resolution x", "Video"), Settings::Manager::getInt("resolution y", "Video"), - Settings::Manager::getBool("fullscreen", "Video"), + static_cast(Settings::Manager::getInt("window mode", "Video")), Settings::Manager::getBool("window border", "Video")); } } void WindowManager::windowResized(int x, int y) { - // Note: this is a side effect of resolution change or window resize. - // There is no need to track these changes. Settings::Manager::setInt("resolution x", "Video", x); Settings::Manager::setInt("resolution y", "Video", y); - Settings::Manager::resetPendingChange("resolution x", "Video"); - Settings::Manager::resetPendingChange("resolution y", "Video"); + + // We only want to process changes to window-size related settings. + Settings::CategorySettingVector filter = {{"Video", "resolution x"}, + {"Video", "resolution y"}}; + + // If the HUD has not been initialised, the World singleton will not be available. + if (mHud) + { + MWBase::Environment::get().getWorld()->processChangedSettings( + Settings::Manager::getPendingChanges(filter)); + } + + Settings::Manager::resetPendingChanges(filter); mGuiPlatform->getRenderManagerPtr()->setViewSize(x, y); @@ -1119,7 +1174,7 @@ namespace MWGui window->onResChange(x, y); // We should reload TrueType fonts to fit new resolution - loadUserFonts(); + mFontLoader->loadTrueTypeFonts(); // TODO: check if any windows are now off-screen and move them back if so } @@ -1150,6 +1205,16 @@ namespace MWGui } void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) + { + pushGuiMode(mode, arg, false); + } + + void WindowManager::forceLootMode(const MWWorld::Ptr& ptr) + { + pushGuiMode(MWGui::GM_Container, ptr, true); + } + + void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force) { if (mode==GM_Inventory && mAllowed==GW_None) return; @@ -1172,6 +1237,8 @@ namespace MWGui mGuiModeStates[mode].update(true); playSound(mGuiModeStates[mode].mOpenSound); } + if(force) + mContainerWindow->treatNextOpenAsLoot(); for (WindowBase* window : mGuiModeStates[mode].mWindows) window->setPtr(arg); @@ -1180,6 +1247,21 @@ namespace MWGui updateVisible(); } + void WindowManager::setCullMask(uint32_t mask) + { + mViewer->getCamera()->setCullMask(mask); + + // We could check whether stereo is enabled here, but these methods are + // trivial and have no effect in mono or multiview so just call them regardless. + mViewer->getCamera()->setCullMaskLeft(mask); + mViewer->getCamera()->setCullMaskRight(mask); + } + + uint32_t WindowManager::getCullMask() + { + return mViewer->getCamera()->getCullMask(); + } + void WindowManager::popGuiMode(bool noSound) { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) @@ -1251,7 +1333,7 @@ namespace MWGui void WindowManager::setSelectedEnchantItem(const MWWorld::Ptr& item) { mSelectedEnchantItem = item; - mSelectedSpell = ""; + mSelectedSpell.clear(); const ESM::Enchantment* ench = mStore->get() .find(item.getClass().getEnchantment(item)); @@ -1284,13 +1366,13 @@ namespace MWGui void WindowManager::unsetSelectedSpell() { - mSelectedSpell = ""; + mSelectedSpell.clear(); mSelectedEnchantItem = MWWorld::Ptr(); mHud->unsetSelectedSpell(); MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - if (player->getDrawState() == MWMechanics::DrawState_Spell) - player->setDrawState(MWMechanics::DrawState_Nothing); + if (player->getDrawState() == MWMechanics::DrawState::Spell) + player->setDrawState(MWMechanics::DrawState::Nothing); mSpellWindow->setTitle("#{sNone}"); } @@ -1324,6 +1406,11 @@ namespace MWGui return mHud->getWorldMouseOver(); } + float WindowManager::getScalingFactor() const + { + return mScalingFactor; + } + void WindowManager::executeInConsole (const std::string& path) { mConsole->executeFile (path); @@ -1333,6 +1420,7 @@ namespace MWGui MWGui::CountDialog* WindowManager::getCountDialog() { return mCountDialog; } MWGui::ConfirmationDialog* WindowManager::getConfirmationDialog() { return mConfirmationDialog; } MWGui::TradeWindow* WindowManager::getTradeWindow() { return mTradeWindow; } + MWGui::PostProcessorHud* WindowManager::getPostProcessorHud() { return mPostProcessorHud; } void WindowManager::useItem(const MWWorld::Ptr &item, bool bypassBeastRestrictions) { @@ -1421,6 +1509,7 @@ namespace MWGui return !mGuiModes.empty() || isConsoleMode() || + (mPostProcessorHud && mPostProcessorHud->isVisible()) || (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); } @@ -1485,6 +1574,7 @@ namespace MWGui { mHudEnabled = !mHudEnabled; updateVisible(); + mMessageBoxManager->setVisible(mHudEnabled); return mHudEnabled; } @@ -1750,11 +1840,10 @@ namespace MWGui ~MWSound::Type::Movie & MWSound::Type::Mask ); - osg::Timer frameTimer; + Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { - double dt = frameTimer.time_s(); - frameTimer.setStartTick(); + const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); MWBase::Environment::get().getInputManager()->update(dt, true, false); @@ -1777,7 +1866,7 @@ namespace MWGui // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); - MWBase::Environment::get().limitFrameRate(frameTimer.time_s()); + frameRateLimiter.limit(); } mVideoWidget->stop(); @@ -1957,7 +2046,7 @@ namespace MWGui { if (_type != "Text") return; - char* text=0; + char* text=nullptr; text = SDL_GetClipboardText(); if (text) _data = MyGUI::TextIterator::toTagsString(text); @@ -1982,9 +2071,25 @@ namespace MWGui void WindowManager::toggleDebugWindow() { -#ifndef BT_NO_PROFILE mDebugWindow->setVisible(!mDebugWindow->isVisible()); -#endif + } + + void WindowManager::togglePostProcessorHud() + { + if (!MWBase::Environment::get().getWorld()->getPostProcessor()->isEnabled()) + return; + + bool visible = mPostProcessorHud->isVisible(); + + if (!visible && !mGuiModes.empty()) + mKeyboardNavigation->saveFocus(mGuiModes.back()); + + mPostProcessorHud->setVisible(!visible); + + if (visible && !mGuiModes.empty()) + mKeyboardNavigation->restoreFocus(mGuiModes.back()); + + updateVisible(); } void WindowManager::cycleSpell(bool next) @@ -2004,7 +2109,7 @@ namespace MWGui if (soundId.empty()) return; - MWBase::Environment::get().getSoundManager()->playSound(soundId, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); + MWBase::Environment::get().getSoundManager()->playSound(soundId, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnvNoScaling); } void WindowManager::updateSpellWindow() @@ -2018,28 +2123,14 @@ namespace MWGui mConsole->setSelectedObject(object); } - std::string WindowManager::correctIconPath(const std::string& path) - { - return Misc::ResourceHelpers::correctIconPath(path, mResourceSystem->getVFS()); - } - - std::string WindowManager::correctBookartPath(const std::string& path, int width, int height, bool* exists) + void WindowManager::printToConsole(const std::string& msg, std::string_view color) { - std::string corrected = Misc::ResourceHelpers::correctBookartPath(path, width, height, mResourceSystem->getVFS()); - if (exists) - *exists = mResourceSystem->getVFS()->exists(corrected); - return corrected; + mConsole->print(msg, color); } - std::string WindowManager::correctTexturePath(const std::string& path) + void WindowManager::setConsoleMode(const std::string& mode) { - return Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); - } - - bool WindowManager::textureExists(const std::string &path) - { - std::string corrected = Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); - return mResourceSystem->getVFS()->exists(corrected); + mConsole->setConsoleMode(mode); } void WindowManager::createCursors() @@ -2193,4 +2284,28 @@ namespace MWGui { return mStatsWatcher->getWatchedActor(); } + + const std::string& WindowManager::getVersionDescription() const + { + return mVersionDescription; + } + + void WindowManager::handleScheduledMessageBoxes() + { + const auto scheduledMessageBoxes = mScheduledMessageBoxes.lock(); + for (const ScheduledMessageBox& v : *scheduledMessageBoxes) + messageBox(v.mMessage, v.mShowInDialogueMode); + scheduledMessageBoxes->clear(); + } + + void WindowManager::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + for(auto* window : mWindows) + window->onDeleteCustomData(ptr); + } + + void WindowManager::asyncPrepareSaveMap() + { + mMap->asyncPrepareSaveMap(); + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index cc1a1b6944..6ff99665c2 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -8,6 +8,7 @@ **/ #include +#include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include "mapwindow.hpp" #include "statswatcher.hpp" @@ -114,7 +116,6 @@ namespace MWGui class TrainingWindow; class SpellIcons; class MerchantRepair; - class Repair; class SoulgemDialog; class Recharge; class CompanionWindow; @@ -122,6 +123,7 @@ namespace MWGui class WindowModal; class ScreenFader; class DebugWindow; + class PostProcessorHud; class JailScreen; class KeyboardNavigation; @@ -133,15 +135,14 @@ namespace MWGui typedef std::vector FactionList; WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& localPath); + const std::string& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, + ToUTF8::FromType encoding, const std::string& versionDescription, bool useShaders); virtual ~WindowManager(); /// Set the ESMStore to use for retrieving of GUI-related strings. void setStore (const MWWorld::ESMStore& store); void initUI(); - void loadUserFonts() override; Loading::Listener* getLoadingScreen() override; @@ -186,6 +187,8 @@ namespace MWGui MWGui::CountDialog* getCountDialog() override; MWGui::ConfirmationDialog* getConfirmationDialog() override; MWGui::TradeWindow* getTradeWindow() override; + const std::vector getActiveMessageBoxes() override; + MWGui::PostProcessorHud* getPostProcessorHud() override; /// Make the player use an item, while updating GUI state accordingly void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions=false) override; @@ -193,6 +196,8 @@ namespace MWGui void updateSpellWindow() override; void setConsoleSelectedObject(const MWWorld::Ptr& object) override; + void printToConsole(const std::string& msg, std::string_view color) override; + void setConsoleMode(const std::string& mode) override; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning @@ -209,6 +214,8 @@ namespace MWGui void setDragDrop(bool dragDrop) override; bool getWorldMouseOver() override; + float getScalingFactor() const override; + bool toggleFogOfWar() override; bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; @@ -263,6 +270,7 @@ namespace MWGui void exitCurrentGuiMode() override; void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; + void scheduleMessageBox (std::string message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; void staticMessageBox(const std::string& message) override; void removeStaticMessageBox() override; void interactiveMessageBox (const std::string& message, @@ -270,7 +278,7 @@ namespace MWGui int readPressedButton () override; ///< returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) - void update (float duration) override; + void update (float duration); /** * Fetches a GMST string from the store, if there is no setting with the given @@ -359,6 +367,7 @@ namespace MWGui void toggleConsole() override; void toggleDebugWindow() override; + void togglePostProcessorHud() override; /// Cycle to next or previous spell void cycleSpell(bool next) override; @@ -367,12 +376,6 @@ namespace MWGui void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) override; - // In WindowManager for now since there isn't a VFS singleton - std::string correctIconPath(const std::string& path) override; - std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) override; - std::string correctTexturePath(const std::string& path) override; - bool textureExists(const std::string& path) override; - void addCell(MWWorld::CellStore* cell) override; void removeCell(MWWorld::CellStore* cell) override; void writeFog(MWWorld::CellStore* cell) override; @@ -382,6 +385,13 @@ namespace MWGui bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat=false) override; bool injectKeyRelease(MyGUI::KeyCode key) override; + const std::string& getVersionDescription() const override; + + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + void forceLootMode(const MWWorld::Ptr& ptr) override; + + void asyncPrepareSaveMap() override; + private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; @@ -438,7 +448,9 @@ namespace MWGui ScreenFader* mHitFader; ScreenFader* mScreenFader; DebugWindow* mDebugWindow; + PostProcessorHud* mPostProcessorHud; JailScreen* mJailScreen; + ContainerWindow* mContainerWindow; std::vector mWindows; @@ -519,6 +531,19 @@ namespace MWGui SDLUtil::VideoWrapper* mVideoWrapper; + float mScalingFactor; + + struct ScheduledMessageBox + { + std::string mMessage; + MWGui::ShowInDialogueMode mShowInDialogueMode; + + ScheduledMessageBox(std::string&& message, MWGui::ShowInDialogueMode showInDialogueMode) + : mMessage(std::move(message)), mShowInDialogueMode(showInDialogueMode) {} + }; + + Misc::ScopeGuarded> mScheduledMessageBoxes; + /** * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. * Supported syntax: @@ -550,6 +575,13 @@ namespace MWGui void updatePinnedWindows(); void enableScene(bool enable); + + void handleScheduledMessageBoxes(); + + void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force); + + void setCullMask(uint32_t mask) override; + uint32_t getCullMask() override; }; } diff --git a/apps/openmw/mwgui/windowpinnablebase.hpp b/apps/openmw/mwgui/windowpinnablebase.hpp index a942128195..c91f0a1489 100644 --- a/apps/openmw/mwgui/windowpinnablebase.hpp +++ b/apps/openmw/mwgui/windowpinnablebase.hpp @@ -5,8 +5,6 @@ namespace MWGui { - class WindowManager; - class WindowPinnableBase: public WindowBase { public: diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index b29aa58a29..4674ead688 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -9,6 +9,7 @@ #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -21,12 +22,13 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwgui/messagebox.hpp" + #include "actions.hpp" #include "bindingsmanager.hpp" namespace MWInput { - const float ZOOM_SCALE = 10.f; /// Used for scrolling camera in and out ActionManager::ActionManager(BindingsManager* bindingsManager, osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, @@ -39,8 +41,6 @@ namespace MWInput , mAlwaysRunActive(Settings::Manager::getBool("always run", "Input")) , mSneaking(false) , mAttemptJump(false) - , mOverencumberedMessageDelay(0.f) - , mPreviewPOVDelay(0.f) , mTimeIdle(0.f) { } @@ -89,43 +89,26 @@ namespace MWInput { player.setUpDown(1); triedToMove = true; - mOverencumberedMessageDelay = 0.f; } // if player tried to start moving, but can't (due to being overencumbered), display a notification. if (triedToMove) { MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - mOverencumberedMessageDelay -= dt; if (playerPtr.getClass().getEncumbrance(playerPtr) > playerPtr.getClass().getCapacity(playerPtr)) { player.setAutoMove (false); - if (mOverencumberedMessageDelay <= 0) + std::vector msgboxs = MWBase::Environment::get().getWindowManager()->getActiveMessageBoxes(); + const std::vector::iterator it = std::find_if(msgboxs.begin(), msgboxs.end(), [](MWGui::MessageBox*& msgbox) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}"); - mOverencumberedMessageDelay = 1.0; - } - } - } + return (msgbox->getMessage() == "#{sNotifyMessage59}"); + }); - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - const float switchLimit = 0.25; - MWBase::World* world = MWBase::Environment::get().getWorld(); - if (mBindingsManager->actionIsActive(A_TogglePOV)) - { - if (world->isFirstPerson() ? mPreviewPOVDelay > switchLimit : mPreviewPOVDelay == 0) - world->togglePreviewMode(true); - mPreviewPOVDelay += dt; - } - else - { - //disable preview mode - if (mPreviewPOVDelay > 0) - world->togglePreviewMode(false); - if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= switchLimit) - world->togglePOV(); - mPreviewPOVDelay = 0.f; + // if an overencumbered messagebox is already present, reset its expiry timer, otherwise create new one. + if (it != msgboxs.end()) + (*it)->mCurrentTime = 0; + else + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}"); } } @@ -161,42 +144,21 @@ namespace MWInput resetIdleTime(); } else - { - updateIdleTime(dt); - } + mTimeIdle += dt; mAttemptJump = false; } - - bool ActionManager::isPreviewModeEnabled() - { - return MWBase::Environment::get().getWorld()->isPreviewModeEnabled(); - } void ActionManager::resetIdleTime() { - if (mTimeIdle < 0) - MWBase::Environment::get().getWorld()->toggleVanityMode(false); mTimeIdle = 0.f; } - void ActionManager::updateIdleTime(float dt) - { - static const float vanityDelay = MWBase::Environment::get().getWorld()->getStore().get() - .find("fVanityDelay")->mValue.getFloat(); - if (mTimeIdle >= 0.f) - mTimeIdle += dt; - if (mTimeIdle > vanityDelay) - { - MWBase::Environment::get().getWorld()->toggleVanityMode(true); - mTimeIdle = -1.f; - } - } - void ActionManager::executeAction(int action) { - auto* inputManager = MWBase::Environment::get().getInputManager(); - auto* windowManager = MWBase::Environment::get().getWindowManager(); + MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::Action, action}); + const auto inputManager = MWBase::Environment::get().getInputManager(); + const auto windowManager = MWBase::Environment::get().getWindowManager(); // trigger action activated switch (action) { @@ -279,13 +241,8 @@ namespace MWInput case A_ToggleDebug: windowManager->toggleDebugWindow(); break; - case A_ZoomIn: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-ZOOM_SCALE); - break; - case A_ZoomOut: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(ZOOM_SCALE); + case A_TogglePostProcessorHUD: + windowManager->togglePostProcessorHud(); break; case A_QuickSave: quickSave(); @@ -333,12 +290,8 @@ namespace MWInput void ActionManager::screenshot() { - bool regularScreenshot = true; - - std::string settingStr; - - settingStr = Settings::Manager::getString("screenshot type","Video"); - regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; + const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); + bool regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; if (regularScreenshot) { @@ -349,7 +302,7 @@ namespace MWInput { osg::ref_ptr screenshot (new osg::Image); - if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), settingStr)) + if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) { (*mScreenCaptureOperation) (*(screenshot.get()), 0); // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason @@ -402,11 +355,11 @@ namespace MWInput if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) return; - MWMechanics::DrawState_ state = player.getDrawState(); - if (state == MWMechanics::DrawState_Weapon || state == MWMechanics::DrawState_Nothing) - player.setDrawState(MWMechanics::DrawState_Spell); + MWMechanics::DrawState state = player.getDrawState(); + if (state == MWMechanics::DrawState::Weapon || state == MWMechanics::DrawState::Nothing) + player.setDrawState(MWMechanics::DrawState::Spell); else - player.setDrawState(MWMechanics::DrawState_Nothing); + player.setDrawState(MWMechanics::DrawState::Nothing); } void ActionManager::quickLoad() @@ -437,11 +390,11 @@ namespace MWInput else if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) return; - MWMechanics::DrawState_ state = player.getDrawState(); - if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - player.setDrawState(MWMechanics::DrawState_Weapon); + MWMechanics::DrawState state = player.getDrawState(); + if (state == MWMechanics::DrawState::Spell || state == MWMechanics::DrawState::Nothing) + player.setDrawState(MWMechanics::DrawState::Weapon); else - player.setDrawState(MWMechanics::DrawState_Nothing); + player.setDrawState(MWMechanics::DrawState::Nothing); } void ActionManager::rest() @@ -494,16 +447,17 @@ namespace MWInput if (MyGUI::InputManager::getInstance ().isModalAny()) return; - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_MainMenu - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings - && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + if (windowManager->getMode() != MWGui::GM_Journal + && windowManager->getMode() != MWGui::GM_MainMenu + && windowManager->getMode() != MWGui::GM_Settings + && windowManager->getJournalAllowed()) { - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); + windowManager->pushGuiMode(MWGui::GM_Journal); } - else if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) + else if (windowManager->containsMode(MWGui::GM_Journal)) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); + windowManager->removeGuiMode(MWGui::GM_Journal); } } diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp index eceac2e94f..4c51139d46 100644 --- a/apps/openmw/mwinput/actionmanager.hpp +++ b/apps/openmw/mwinput/actionmanager.hpp @@ -48,19 +48,16 @@ namespace MWInput void showQuickKeysMenu(); void resetIdleTime(); + float getIdleTime() const { return mTimeIdle; } bool isAlwaysRunActive() const { return mAlwaysRunActive; }; bool isSneaking() const { return mSneaking; }; void setAttemptJump(bool enabled) { mAttemptJump = enabled; } - bool isPreviewModeEnabled(); - private: void handleGuiArrowKey(int action); - void updateIdleTime(float dt); - BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; @@ -70,8 +67,6 @@ namespace MWInput bool mSneaking; bool mAttemptJump; - float mOverencumberedMessageDelay; - float mPreviewPOVDelay; float mTimeIdle; }; } diff --git a/apps/openmw/mwinput/actions.hpp b/apps/openmw/mwinput/actions.hpp index a1c1607126..c7bdbf28d3 100644 --- a/apps/openmw/mwinput/actions.hpp +++ b/apps/openmw/mwinput/actions.hpp @@ -73,6 +73,8 @@ namespace MWInput A_ZoomIn, A_ZoomOut, + A_TogglePostProcessorHUD, + A_Last // Marker for the last item }; } diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 18fac6ae29..b3e3590125 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,7 +15,6 @@ #include "../mwworld/player.hpp" #include "actions.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -171,16 +172,16 @@ namespace MWInput , mDragDrop(false) { std::string file = userFileExists ? userFile : ""; - mInputBinder = new InputControlSystem(file); - mListener = new BindingsListener(mInputBinder, this); - mInputBinder->setDetectingBindingListener(mListener); + mInputBinder = std::make_unique(file); + mListener = std::make_unique(mInputBinder.get(), this); + mInputBinder->setDetectingBindingListener(mListener.get()); loadKeyDefaults(); loadControllerDefaults(); for (int i = 0; i < A_Last; ++i) { - mInputBinder->getChannel(i)->addListener(mListener); + mInputBinder->getChannel(i)->addListener(mListener.get()); } } @@ -192,7 +193,6 @@ namespace MWInput BindingsManager::~BindingsManager() { mInputBinder->save(mUserFile); - delete mInputBinder; } void BindingsManager::update(float dt) @@ -286,6 +286,7 @@ namespace MWInput defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; defaultKeyBindings[A_QuickSave] = SDL_SCANCODE_F5; defaultKeyBindings[A_QuickLoad] = SDL_SCANCODE_F9; + defaultKeyBindings[A_TogglePostProcessorHUD] = SDL_SCANCODE_F2; std::map defaultMouseButtonBindings; defaultMouseButtonBindings[A_Inventory] = SDL_BUTTON_RIGHT; @@ -315,7 +316,7 @@ namespace MWInput && mInputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS && mInputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) == ICS::InputControlSystem::MouseWheelClick::UNASSIGNED)) { - clearAllKeyBindings(mInputBinder, control); + clearAllKeyBindings(mInputBinder.get(), control); if (defaultKeyBindings.find(i) != defaultKeyBindings.end() && (force || !mInputBinder->isKeyBound(defaultKeyBindings[i]))) @@ -402,7 +403,7 @@ namespace MWInput if (!controlExists || force || (mInputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS::InputControlSystem::UNASSIGNED && mInputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS)) { - clearAllControllerBindings(mInputBinder, control); + clearAllControllerBindings(mInputBinder.get(), control); if (defaultButtonBindings.find(i) != defaultButtonBindings.end() && (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i]))) @@ -425,13 +426,13 @@ namespace MWInput switch (action) { case A_Screenshot: - return "Screenshot"; + return "#{SettingsMenu:Screenshot}"; case A_ZoomIn: - return "Zoom In"; + return "#{SettingsMenu:CameraZoomIn}"; case A_ZoomOut: - return "Zoom Out"; + return "#{SettingsMenu:CameraZoomOut}"; case A_ToggleHUD: - return "Toggle HUD"; + return "#{SettingsMenu:ToggleHUD}"; case A_Use: return "#{sUse}"; case A_Activate: @@ -502,6 +503,8 @@ namespace MWInput return "#{sQuickSaveCmd}"; case A_QuickLoad: return "#{sQuickLoadCmd}"; + case A_TogglePostProcessorHUD: + return "#{SettingsMenu:TogglePostProcessorHUD}"; default: return std::string(); // not configurable } @@ -547,9 +550,9 @@ namespace MWInput ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; if (mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS::InputControlSystem::UNASSIGNED) - return sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + return SDLUtil::sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) - return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + return SDLUtil::sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else return "#{sNone}"; } @@ -563,7 +566,8 @@ namespace MWInput A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight, A_AutoMove, A_Jump, A_Inventory, A_Journal, A_Rest, A_Console, 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_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, + A_TogglePostProcessorHUD }; return actions; @@ -654,6 +658,15 @@ namespace MWInput return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE); } + SDL_GameController* BindingsManager::getControllerOrNull() const + { + const auto& controllers = mInputBinder->getJoystickInstanceMap(); + if (controllers.empty()) + return nullptr; + else + return controllers.begin()->second; + } + void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue) { MWBase::Environment::get().getInputManager()->resetIdleTime(); @@ -696,8 +709,8 @@ namespace MWInput else { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - MWMechanics::DrawState_ state = player.getDrawState(); - player.setAttackingOrSpell(currentValue != 0 && state != MWMechanics::DrawState_Nothing); + MWMechanics::DrawState state = player.getDrawState(); + player.setAttackingOrSpell(currentValue != 0 && state != MWMechanics::DrawState::Nothing); } } else if (action == A_Jump) diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index 7a44a1a335..668cccd4ca 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -1,6 +1,7 @@ #ifndef MWINPUT_MWBINDINGSMANAGER_H #define MWINPUT_MWBINDINGSMANAGER_H +#include #include #include @@ -41,7 +42,9 @@ namespace MWInput bool isLeftOrRightButton(int action, bool joystick) const; bool actionIsActive(int id) const; - float getActionValue(int id) const; + float getActionValue(int id) const; // returns value in range [0, 1] + + SDL_GameController* getControllerOrNull() const; void mousePressed(const SDL_MouseButtonEvent &evt, int deviceID); void mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID); @@ -64,8 +67,8 @@ namespace MWInput private: void setupSDLKeyMappings(); - InputControlSystem* mInputBinder; - BindingsListener* mListener; + std::unique_ptr mInputBinder; + std::unique_ptr mListener; std::string mUserFile; diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 48091541ca..adc62a80c5 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -2,12 +2,15 @@ #include #include -#include + +#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -18,7 +21,6 @@ #include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -31,15 +33,13 @@ namespace MWInput , mActionManager(actionManager) , mMouseManager(mouseManager) , mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input")) + , mGyroAvailable(false) , mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input")) - , mInvUiScalingFactor(1.f) , mSneakToggleShortcutTimer(0.f) - , mGamepadZoom(0) , mGamepadGuiCursorEnabled(true) , mGuiCursorEnabled(true) , mJoystickLastUsed(false) , mSneakGamepadShortcut(false) - , mGamepadPreviewMode(false) { if (!controllerBindingsFile.empty()) { @@ -60,7 +60,7 @@ namespace MWInput SDL_ControllerDeviceEvent evt; evt.which = i; static const int fakeDeviceID = 1; - controllerAdded(fakeDeviceID, evt); + ControllerManager::controllerAdded(fakeDeviceID, evt); Log(Debug::Info) << "Detected game controller: " << SDL_GameControllerNameForIndex(i); } else @@ -69,12 +69,8 @@ namespace MWInput } } - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale != 0.f) - mInvUiScalingFactor = 1.f / uiScale; - float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); - deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); + deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); } @@ -89,8 +85,6 @@ namespace MWInput bool ControllerManager::update(float dt) { - mGamepadPreviewMode = mActionManager->isPreviewModeEnabled(); - if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled)) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f; @@ -102,8 +96,10 @@ 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 xMove = xAxis * dt * 1500.0f * mInvUiScalingFactor * mGamepadCursorSpeed; - float yMove = yAxis * dt * 1500.0f * mInvUiScalingFactor * mGamepadCursorSpeed; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + float xMove = xAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; + float yMove = yAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; + float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) { @@ -117,7 +113,6 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode() || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) { - mGamepadZoom = 0; return false; } @@ -184,15 +179,6 @@ namespace MWInput } } - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - if (!mBindingsManager->actionIsActive(A_TogglePOV)) - mGamepadZoom = 0; - - if (mGamepadZoom) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom); - } - return triedToMove; } @@ -201,6 +187,9 @@ namespace MWInput if (!mJoystickEnabled || mBindingsManager->isDetectingBindingState()) return; + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::ControllerPressed, arg.button}); + mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { @@ -228,7 +217,7 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); if (!MWBase::Environment::get().getInputManager()->controlsDisabled()) @@ -243,6 +232,12 @@ namespace MWInput return; } + if (mJoystickEnabled) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::ControllerReleased, arg.button}); + } + if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; @@ -266,7 +261,7 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->controllerButtonReleased(deviceID, arg); @@ -282,21 +277,11 @@ namespace MWInput { gamepadToGuiControl(arg); } - else + else if (mBindingsManager->actionIsActive(A_TogglePOV) && + (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)) { - if (mGamepadPreviewMode) // Preview Mode Gamepad Zooming - { - if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) - { - mGamepadZoom = arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) - { - mGamepadZoom = -arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - } + // Preview Mode Gamepad Zooming; do not propagate to mBindingsManager + return; } mBindingsManager->controllerAxisMoved(deviceID, arg); } @@ -304,6 +289,7 @@ namespace MWInput void ControllerManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) { mBindingsManager->controllerAdded(deviceID, arg); + enableGyroSensor(); } void ControllerManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) @@ -396,4 +382,72 @@ namespace MWInput return true; } + + float ControllerManager::getAxisValue(SDL_GameControllerAxis axis) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768; + if (cntrl) + return SDL_GameControllerGetAxis(cntrl, axis) / static_cast(AXIS_MAX_ABSOLUTE_VALUE); + else + return 0; + } + + bool ControllerManager::isButtonPressed(SDL_GameControllerButton button) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (cntrl) + return SDL_GameControllerGetButton(cntrl, button) > 0; + else + return false; + } + + void ControllerManager::enableGyroSensor() + { + mGyroAvailable = false; + #if SDL_VERSION_ATLEAST(2, 0, 14) + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (!cntrl) + return; + if (!SDL_GameControllerHasSensor(cntrl, SDL_SENSOR_GYRO)) + return; + if (SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE) < 0) + return; + mGyroAvailable = true; + #endif + } + + bool ControllerManager::isGyroAvailable() const + { + return mGyroAvailable; + } + + std::array ControllerManager::getGyroValues() const + { + float gyro[3] = { 0.f }; + #if SDL_VERSION_ATLEAST(2, 0, 14) + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (cntrl && mGyroAvailable) + SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + #endif + return std::array({gyro[0], gyro[1], gyro[2]}); + } + + void ControllerManager::touchpadMoved(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::TouchMoved, arg }); + } + + void ControllerManager::touchpadPressed(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::TouchPressed, arg }); + } + + void ControllerManager::touchpadReleased(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::TouchReleased, arg }); + } } diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index 871f11102e..2472128c26 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -31,35 +31,45 @@ namespace MWInput void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) override; void controllerRemoved(const SDL_ControllerDeviceEvent &arg) override; + void touchpadMoved(int deviceId, const SDLUtil::TouchEvent& arg) override; + void touchpadPressed(int deviceId, const SDLUtil::TouchEvent& arg) override; + void touchpadReleased(int deviceId, const SDLUtil::TouchEvent& arg) override; + void processChangedSettings(const Settings::CategorySettingVector& changed); void setJoystickLastUsed(bool enabled) { mJoystickLastUsed = enabled; } - bool joystickLastUsed() { return mJoystickLastUsed; } + bool joystickLastUsed() const { return mJoystickLastUsed; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; } - bool gamepadGuiCursorEnabled() { return mGamepadGuiCursorEnabled; } + bool gamepadGuiCursorEnabled() const { return mGamepadGuiCursorEnabled; } + + float getAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1] + bool isButtonPressed(SDL_GameControllerButton button) const; + + bool isGyroAvailable() const; + std::array getGyroValues() const; private: // Return true if GUI consumes input. bool gamepadToGuiControl(const SDL_ControllerButtonEvent &arg); bool gamepadToGuiControl(const SDL_ControllerAxisEvent &arg); + void enableGyroSensor(); + BindingsManager* mBindingsManager; ActionManager* mActionManager; MouseManager* mMouseManager; bool mJoystickEnabled; + bool mGyroAvailable; float mGamepadCursorSpeed; - float mInvUiScalingFactor; float mSneakToggleShortcutTimer; - float mGamepadZoom; bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; bool mSneakGamepadShortcut; - bool mGamepadPreviewMode; }; } #endif diff --git a/apps/openmw/mwinput/controlswitch.cpp b/apps/openmw/mwinput/controlswitch.cpp index 33c4b75dcc..da8df3ac6b 100644 --- a/apps/openmw/mwinput/controlswitch.cpp +++ b/apps/openmw/mwinput/controlswitch.cpp @@ -1,8 +1,8 @@ #include "controlswitch.hpp" -#include -#include -#include +#include +#include +#include #include @@ -29,12 +29,15 @@ namespace MWInput mSwitches["vanitymode"] = true; } - bool ControlSwitch::get(const std::string& key) + bool ControlSwitch::get(std::string_view key) { - return mSwitches[key]; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + return it->second; } - void ControlSwitch::set(const std::string& key, bool value) + void ControlSwitch::set(std::string_view key, bool value) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); @@ -51,15 +54,14 @@ namespace MWInput /// \fixme maybe crouching at this time player.setUpDown(0); } - else if (key == "vanitymode") - { - MWBase::Environment::get().getWorld()->allowVanityMode(value); - } else if (key == "playerlooking" && !value) { - MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), 0.f, 0.f, 0.f); + MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), osg::Vec3f()); } - mSwitches[key] = value; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + it->second = value; } void ControlSwitch::write(ESM::ESMWriter& writer, Loading::Listener& /*progress*/) diff --git a/apps/openmw/mwinput/controlswitch.hpp b/apps/openmw/mwinput/controlswitch.hpp index 38d01066bd..b4353c31f5 100644 --- a/apps/openmw/mwinput/controlswitch.hpp +++ b/apps/openmw/mwinput/controlswitch.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace ESM { @@ -23,8 +24,8 @@ namespace MWInput public: ControlSwitch(); - bool get(const std::string& key); - void set(const std::string& key, bool value); + bool get(std::string_view key); + void set(std::string_view key, bool value); void clear(); void write(ESM::ESMWriter& writer, Loading::Listener& progress); @@ -32,7 +33,7 @@ namespace MWInput int countSavedGameRecords() const; private: - std::map mSwitches; + std::map> mSwitches; }; } #endif diff --git a/apps/openmw/mwinput/gyromanager.cpp b/apps/openmw/mwinput/gyromanager.cpp new file mode 100644 index 0000000000..b0c6f121c5 --- /dev/null +++ b/apps/openmw/mwinput/gyromanager.cpp @@ -0,0 +1,101 @@ +#include "gyromanager.hpp" + +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/player.hpp" + +namespace MWInput +{ + GyroManager::GyroscopeAxis GyroManager::gyroscopeAxisFromString(std::string_view s) + { + if (s == "x") + return GyroscopeAxis::X; + else if (s == "y") + return GyroscopeAxis::Y; + else if (s == "z") + return GyroscopeAxis::Z; + else if (s == "-x") + return GyroscopeAxis::Minus_X; + else if (s == "-y") + return GyroscopeAxis::Minus_Y; + else if (s == "-z") + return GyroscopeAxis::Minus_Z; + + return GyroscopeAxis::Unknown; + } + + GyroManager::GyroManager() + : mEnabled(Settings::Manager::getBool("enable gyroscope", "Input")) + , mGuiCursorEnabled(true) + , mSensitivityH(Settings::Manager::getFloat("gyro horizontal sensitivity", "Input")) + , mSensitivityV(Settings::Manager::getFloat("gyro vertical sensitivity", "Input")) + , mInputThreshold(Settings::Manager::getFloat("gyro input threshold", "Input")) + , mAxisH(gyroscopeAxisFromString(Settings::Manager::getString("gyro horizontal axis", "Input"))) + , mAxisV(gyroscopeAxisFromString(Settings::Manager::getString("gyro vertical axis", "Input"))) + {} + + void GyroManager::update(float dt, std::array values) const + { + if (!mGuiCursorEnabled) + { + float gyroH = getAxisValue(mAxisH, values); + float gyroV = getAxisValue(mAxisV, values); + + if (gyroH == 0.f && gyroV == 0.f) + return; + + float rot[3]; + rot[0] = -gyroV * dt * mSensitivityV; + rot[1] = 0.0f; + rot[2] = -gyroH * dt * mSensitivityH; + + // Only actually turn player when we're not in vanity mode + 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 (!playerLooking) + MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); + + MWBase::Environment::get().getInputManager()->resetIdleTime(); + } + } + + void GyroManager::processChangedSettings(const Settings::CategorySettingVector& changed) + { + for (const auto& setting : changed) + { + if (setting.first != "Input") + continue; + + if (setting.second == "enable gyroscope") + mEnabled = Settings::Manager::getBool("enable gyroscope", "Input"); + else if (setting.second == "gyro horizontal sensitivity") + mSensitivityH = Settings::Manager::getFloat("gyro horizontal sensitivity", "Input"); + else if (setting.second == "gyro vertical sensitivity") + mSensitivityV = Settings::Manager::getFloat("gyro vertical sensitivity", "Input"); + else if (setting.second == "gyro input threshold") + mInputThreshold = Settings::Manager::getFloat("gyro input threshold", "Input"); + else if (setting.second == "gyro horizontal axis") + mAxisH = gyroscopeAxisFromString(Settings::Manager::getString("gyro horizontal axis", "Input")); + else if (setting.second == "gyro vertical axis") + mAxisV = gyroscopeAxisFromString(Settings::Manager::getString("gyro vertical axis", "Input")); + } + } + + float GyroManager::getAxisValue(GyroscopeAxis axis, std::array values) const + { + if (axis == GyroscopeAxis::Unknown) + return 0; + float value = values[std::abs(axis) - 1]; + if (axis < 0) + value *= -1; + if (std::abs(value) <= mInputThreshold) + value = 0; + return value; + } +} diff --git a/apps/openmw/mwinput/gyromanager.hpp b/apps/openmw/mwinput/gyromanager.hpp new file mode 100644 index 0000000000..bcd3b88f49 --- /dev/null +++ b/apps/openmw/mwinput/gyromanager.hpp @@ -0,0 +1,47 @@ +#ifndef MWINPUT_GYROMANAGER +#define MWINPUT_GYROMANAGER + +#include + +namespace MWInput +{ + class GyroManager + { + public: + GyroManager(); + + bool isEnabled() const { return mEnabled; } + + void update(float dt, std::array values) const; + + void processChangedSettings(const Settings::CategorySettingVector& changed); + + void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + + private: + enum GyroscopeAxis + { + Unknown = 0, + X = 1, + Y = 2, + Z = 3, + Minus_X = -1, + Minus_Y = -2, + Minus_Z = -3 + }; + + static GyroscopeAxis gyroscopeAxisFromString(std::string_view s); + + bool mEnabled; + bool mGuiCursorEnabled; + float mSensitivityH; + float mSensitivityV; + float mInputThreshold; + GyroscopeAxis mAxisH; + GyroscopeAxis mAxisV; + + float getAxisValue(GyroscopeAxis axis, std::array values) const; + }; +} + +#endif // !MWINPUT_GYROMANAGER diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 690183c576..abc66de70d 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -3,9 +3,8 @@ #include #include -#include -#include -#include +#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" @@ -19,8 +18,8 @@ #include "controlswitch.hpp" #include "keyboardmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" #include "sensormanager.hpp" +#include "gyromanager.hpp" namespace MWInput { @@ -32,27 +31,21 @@ namespace MWInput const std::string& userFile, bool userFileExists, const std::string& userControllerBindingsFile, const std::string& 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)) + , mKeyboardManager(std::make_unique(mBindingsManager.get())) + , mMouseManager(std::make_unique(mBindingsManager.get(), mInputWrapper.get(), window)) + , mControllerManager(std::make_unique(mBindingsManager.get(), mActionManager.get(), mMouseManager.get(), userControllerBindingsFile, controllerBindingsFile)) + , mSensorManager(std::make_unique()) + , mGyroManager(std::make_unique()) { - mInputWrapper = new SDLUtil::InputWrapper(window, viewer, grab); mInputWrapper->setWindowEventCallback(MWBase::Environment::get().getWindowManager()); - - mBindingsManager = new BindingsManager(userFile, userFileExists); - - mControlSwitch = new ControlSwitch(); - - mActionManager = new ActionManager(mBindingsManager, screenCaptureOperation, viewer, screenCaptureHandler); - - mKeyboardManager = new KeyboardManager(mBindingsManager); - mInputWrapper->setKeyboardEventCallback(mKeyboardManager); - - mMouseManager = new MouseManager(mBindingsManager, mInputWrapper, window); - mInputWrapper->setMouseEventCallback(mMouseManager); - - mControllerManager = new ControllerManager(mBindingsManager, mActionManager, mMouseManager, userControllerBindingsFile, controllerBindingsFile); - mInputWrapper->setControllerEventCallback(mControllerManager); - - mSensorManager = new SensorManager(); - mInputWrapper->setSensorEventCallback(mSensorManager); + mInputWrapper->setKeyboardEventCallback(mKeyboardManager.get()); + mInputWrapper->setMouseEventCallback(mMouseManager.get()); + mInputWrapper->setControllerEventCallback(mControllerManager.get()); + mInputWrapper->setSensorEventCallback(mSensorManager.get()); } void InputManager::clear() @@ -61,20 +54,7 @@ namespace MWInput mControlSwitch->clear(); } - InputManager::~InputManager() - { - delete mActionManager; - delete mControllerManager; - delete mKeyboardManager; - delete mMouseManager; - delete mSensorManager; - - delete mControlSwitch; - - delete mBindingsManager; - - delete mInputWrapper; - } + InputManager::~InputManager() {} void InputManager::setAttemptJump(bool jumping) { @@ -103,7 +83,16 @@ namespace MWInput mSensorManager->update(dt); mActionManager->update(dt, controllerMove); - MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt); + if (mGyroManager->isEnabled()) + { + bool controllerAvailable = mControllerManager->isGyroAvailable(); + bool sensorAvailable = mSensorManager->isGyroAvailable(); + if (controllerAvailable || sensorAvailable) + { + mGyroManager->update(dt, + controllerAvailable ? mControllerManager->getGyroValues() : mSensorManager->getGyroValues()); + } + } } void InputManager::setDragDrop(bool dragDrop) @@ -120,7 +109,7 @@ namespace MWInput { mControllerManager->setGuiCursorEnabled(guiMode); mMouseManager->setGuiCursorEnabled(guiMode); - mSensorManager->setGuiCursorEnabled(guiMode); + mGyroManager->setGuiCursorEnabled(guiMode); mMouseManager->setMouseLookEnabled(!guiMode); if (guiMode) MWBase::Environment::get().getWindowManager()->showCrosshair(false); @@ -134,14 +123,15 @@ namespace MWInput { mMouseManager->processChangedSettings(changed); mSensorManager->processChangedSettings(changed); + mGyroManager->processChangedSettings(changed); } - bool InputManager::getControlSwitch(const std::string& sw) + bool InputManager::getControlSwitch(std::string_view sw) { return mControlSwitch->get(sw); } - void InputManager::toggleControlSwitch(const std::string& sw, bool value) + void InputManager::toggleControlSwitch(std::string_view sw, bool value) { mControlSwitch->set(sw, value); } @@ -151,21 +141,56 @@ namespace MWInput mActionManager->resetIdleTime(); } - std::string InputManager::getActionDescription(int action) + bool InputManager::isIdle() const + { + return mActionManager->getIdleTime() > 0.5; + } + + std::string InputManager::getActionDescription(int action) const { return mBindingsManager->getActionDescription(action); } - std::string InputManager::getActionKeyBindingName(int action) + std::string InputManager::getActionKeyBindingName(int action) const { return mBindingsManager->getActionKeyBindingName(action); } - std::string InputManager::getActionControllerBindingName(int action) + std::string InputManager::getActionControllerBindingName(int action) const { return mBindingsManager->getActionControllerBindingName(action); } + bool InputManager::actionIsActive(int action) const + { + return mBindingsManager->actionIsActive(action); + } + + float InputManager::getActionValue(int action) const + { + return mBindingsManager->getActionValue(action); + } + + bool InputManager::isControllerButtonPressed(SDL_GameControllerButton button) const + { + return mControllerManager->isButtonPressed(button); + } + + float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const + { + return mControllerManager->getAxisValue(axis); + } + + int InputManager::getMouseMoveX() const + { + return mMouseManager->getMouseMoveX(); + } + + int InputManager::getMouseMoveY() const + { + return mMouseManager->getMouseMoveY(); + } + std::vector InputManager::getActionKeySorting() { return mBindingsManager->getActionKeySorting(); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index f930836d1c..7b3029e11a 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -1,6 +1,8 @@ #ifndef MWINPUT_MWINPUTMANAGERIMP_H #define MWINPUT_MWINPUTMANAGERIMP_H +#include + #include #include @@ -39,11 +41,12 @@ namespace MWInput class KeyboardManager; class MouseManager; class SensorManager; + class GyroManager; /** * @brief Class that provides a high-level API for game input */ - class InputManager : public MWBase::InputManager + class InputManager final : public MWBase::InputManager { public: InputManager( @@ -55,12 +58,12 @@ namespace MWInput const std::string& userControllerBindingsFile, const std::string& controllerBindingsFile, bool grab); - virtual ~InputManager(); + ~InputManager() final; /// Clear all savegame-specific data void clear() override; - void update(float dt, bool disableControls=false, bool disableEvents=false) override; + void update(float dt, bool disableControls, bool disableEvents=false) override; void changeInputMode(bool guiMode) override; @@ -70,12 +73,20 @@ namespace MWInput void setGamepadGuiCursorEnabled(bool enabled) override; void setAttemptJump(bool jumping) override; - void toggleControlSwitch (const std::string& sw, bool value) override; - bool getControlSwitch (const std::string& sw) override; + void toggleControlSwitch(std::string_view sw, bool value) override; + bool getControlSwitch(std::string_view sw) override; + + std::string getActionDescription (int action) const override; + std::string getActionKeyBindingName (int action) const override; + std::string getActionControllerBindingName (int action) const override; + bool actionIsActive(int action) const override; + + float getActionValue(int action) const override; + bool isControllerButtonPressed(SDL_GameControllerButton button) const override; + float getControllerAxisValue(SDL_GameControllerAxis axis) const override; + int getMouseMoveX() const override; + int getMouseMoveY() const override; - std::string getActionDescription (int action) override; - std::string getActionKeyBindingName (int action) override; - std::string getActionControllerBindingName (int action) override; int getNumActions() override { return A_Last; } std::vector getActionKeySorting() override; std::vector getActionControllerSorting() override; @@ -91,6 +102,7 @@ namespace MWInput void readRecord(ESM::ESMReader& reader, uint32_t type) override; void resetIdleTime() override; + bool isIdle() const override; void executeAction(int action) override; @@ -107,18 +119,17 @@ namespace MWInput void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); - SDLUtil::InputWrapper* mInputWrapper; - bool mControlsDisabled; - ControlSwitch* mControlSwitch; - - ActionManager* mActionManager; - BindingsManager* mBindingsManager; - ControllerManager* mControllerManager; - KeyboardManager* mKeyboardManager; - MouseManager* mMouseManager; - SensorManager* mSensorManager; + std::unique_ptr mInputWrapper; + std::unique_ptr mBindingsManager; + std::unique_ptr mControlSwitch; + std::unique_ptr mActionManager; + std::unique_ptr mKeyboardManager; + std::unique_ptr mMouseManager; + std::unique_ptr mControllerManager; + std::unique_ptr mSensorManager; + std::unique_ptr mGyroManager; }; } #endif diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp index db047a342c..d8fc548f25 100644 --- a/apps/openmw/mwinput/keyboardmanager.cpp +++ b/apps/openmw/mwinput/keyboardmanager.cpp @@ -4,15 +4,17 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/player.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -34,17 +36,19 @@ namespace MWInput // HACK: to make default keybinding for the console work without printing an extra "^" upon closing // This assumes that SDL_TextInput events always come *after* the key event // (which is somewhat reasonable, and hopefully true for all SDL platforms) - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (mBindingsManager->getKeyBinding(A_Console) == arg.keysym.scancode && MWBase::Environment::get().getWindowManager()->isConsoleMode()) SDL_StopTextInput(); - bool consumed = false; + bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable + (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && + // Don't trust isprint for symbols outside the extended ASCII range + ((kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff) || + (arg.keysym.sym >= 0 && arg.keysym.sym <= 255 && std::isprint(arg.keysym.sym)))); if (kc != MyGUI::KeyCode::None && !mBindingsManager->isDetectingBindingState()) { - consumed = MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat); - if (SDL_IsTextInputActive() && // Little trick to check if key is printable - (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && std::isprint(arg.keysym.sym))) + if (MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat)) consumed = true; mBindingsManager->setPlayerControlsEnabled(!consumed); } @@ -56,16 +60,23 @@ namespace MWInput if (!input->controlsDisabled() && !consumed) mBindingsManager->keyPressed(arg); + if (!consumed) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::KeyPressed, arg.keysym}); + } + input->setJoystickLastUsed(false); } void KeyboardManager::keyReleased(const SDL_KeyboardEvent &arg) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (!mBindingsManager->isDetectingBindingState()) mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->keyReleased(arg); + MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::KeyReleased, arg.keysym}); } } diff --git a/apps/openmw/mwinput/keyboardmanager.hpp b/apps/openmw/mwinput/keyboardmanager.hpp index f97f6b9e60..ca58461a20 100644 --- a/apps/openmw/mwinput/keyboardmanager.hpp +++ b/apps/openmw/mwinput/keyboardmanager.hpp @@ -1,7 +1,6 @@ #ifndef MWINPUT_MWKEYBOARDMANAGER_H #define MWINPUT_MWKEYBOARDMANAGER_H -#include #include namespace MWInput diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index ac30d4487b..26bccc79c4 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -3,10 +3,10 @@ #include #include #include -#include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -17,7 +17,6 @@ #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -29,22 +28,20 @@ namespace MWInput , mCameraYMultiplier(Settings::Manager::getFloat("camera y multiplier", "Input")) , mBindingsManager(bindingsManager) , mInputWrapper(inputWrapper) - , mInvUiScalingFactor(1.f) , mGuiCursorX(0) , mGuiCursorY(0) , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) + , mMouseMoveX(0) + , mMouseMoveY(0) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale != 0.f) - mInvUiScalingFactor = 1.f / uiScale; - int w,h; SDL_GetWindowSize(window, &w, &h); - mGuiCursorX = mInvUiScalingFactor * w / 2.f; - mGuiCursorY = mInvUiScalingFactor * h / 2.f; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = w / (2.f * uiScale); + mGuiCursorY = h / (2.f * uiScale); } void MouseManager::processChangedSettings(const Settings::CategorySettingVector& changed) @@ -79,8 +76,9 @@ 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 - mGuiCursorX = static_cast(arg.x) * mInvUiScalingFactor; - mGuiCursorY = static_cast(arg.y) * mInvUiScalingFactor; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = static_cast(arg.x) / uiScale; + mGuiCursorY = static_cast(arg.y) / uiScale; mMouseWheel = static_cast(arg.z); @@ -126,7 +124,11 @@ namespace MWInput else { bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; + guiMode = MyGUI::InputManager::getInstance().injectMouseRelease( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(id) + ) && guiMode; if (mBindingsManager->isDetectingBindingState()) return; // don't allow same mouseup to bind as initiated bind @@ -147,14 +149,19 @@ namespace MWInput void MouseManager::mousePressed(const SDL_MouseButtonEvent &arg, Uint8 id) { - MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + input->setJoystickLastUsed(false); bool guiMode = false; if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events { guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; - if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != 0) + guiMode = MyGUI::InputManager::getInstance().injectMousePress( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(id) + ) && guiMode; + if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != nullptr) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled() && id == SDL_BUTTON_LEFT) @@ -168,7 +175,8 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(!guiMode); // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings) + // 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()) mBindingsManager->mousePressed(arg, id); } @@ -197,6 +205,8 @@ namespace MWInput void MouseManager::update(float dt) { + SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); + if (!mMouseLookEnabled) return; @@ -226,12 +236,18 @@ namespace MWInput bool MouseManager::injectMouseButtonPress(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMousePress( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(button)); } bool MouseManager::injectMouseButtonRelease(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMouseRelease( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(button)); } void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) @@ -241,14 +257,15 @@ namespace MWInput mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); - mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); + mGuiCursorX = std::clamp(mGuiCursorX, 0.f, viewSize.width - 1); + mGuiCursorY = std::clamp(mGuiCursorY, 0.f, viewSize.height - 1); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } void MouseManager::warpMouse() { - mInputWrapper->warpMouse(static_cast(mGuiCursorX / mInvUiScalingFactor), static_cast(mGuiCursorY / mInvUiScalingFactor)); + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mInputWrapper->warpMouse(static_cast(mGuiCursorX*uiScale), static_cast(mGuiCursorY*uiScale)); } } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 3bf692bcf8..16ea56d62b 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -38,6 +38,9 @@ namespace MWInput void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + int getMouseMoveX() const { return mMouseMoveX; } + int getMouseMoveY() const { return mMouseMoveY; } + private: bool mInvertX; bool mInvertY; @@ -47,13 +50,15 @@ namespace MWInput BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; - float mInvUiScalingFactor; float mGuiCursorX; float mGuiCursorY; int mMouseWheel; bool mMouseLookEnabled; bool mGuiCursorEnabled; + + int mMouseMoveX; + int mMouseMoveY; }; } #endif diff --git a/apps/openmw/mwinput/sdlmappings.cpp b/apps/openmw/mwinput/sdlmappings.cpp deleted file mode 100644 index 0c3f5c5d85..0000000000 --- a/apps/openmw/mwinput/sdlmappings.cpp +++ /dev/null @@ -1,218 +0,0 @@ -#include "sdlmappings.hpp" - -#include - -#include - -#include -#include - -namespace MWInput -{ - std::string sdlControllerButtonToString(int button) - { - switch(button) - { - case SDL_CONTROLLER_BUTTON_A: - return "A Button"; - case SDL_CONTROLLER_BUTTON_B: - return "B Button"; - case SDL_CONTROLLER_BUTTON_BACK: - return "Back Button"; - case SDL_CONTROLLER_BUTTON_DPAD_DOWN: - return "DPad Down"; - case SDL_CONTROLLER_BUTTON_DPAD_LEFT: - return "DPad Left"; - case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: - return "DPad Right"; - case SDL_CONTROLLER_BUTTON_DPAD_UP: - return "DPad Up"; - case SDL_CONTROLLER_BUTTON_GUIDE: - return "Guide Button"; - case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - return "Left Shoulder"; - case SDL_CONTROLLER_BUTTON_LEFTSTICK: - return "Left Stick Button"; - case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - return "Right Shoulder"; - case SDL_CONTROLLER_BUTTON_RIGHTSTICK: - return "Right Stick Button"; - case SDL_CONTROLLER_BUTTON_START: - return "Start Button"; - case SDL_CONTROLLER_BUTTON_X: - return "X Button"; - case SDL_CONTROLLER_BUTTON_Y: - return "Y Button"; - default: - return "Button " + std::to_string(button); - } - } - - std::string sdlControllerAxisToString(int axis) - { - switch(axis) - { - case SDL_CONTROLLER_AXIS_LEFTX: - return "Left Stick X"; - case SDL_CONTROLLER_AXIS_LEFTY: - return "Left Stick Y"; - case SDL_CONTROLLER_AXIS_RIGHTX: - return "Right Stick X"; - case SDL_CONTROLLER_AXIS_RIGHTY: - return "Right Stick Y"; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: - return "Left Trigger"; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: - return "Right Trigger"; - default: - return "Axis " + std::to_string(axis); - } - } - - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button) - { - //The right button is the second button, according to MyGUI - if(button == SDL_BUTTON_RIGHT) - button = SDL_BUTTON_MIDDLE; - else if(button == SDL_BUTTON_MIDDLE) - button = SDL_BUTTON_RIGHT; - - //MyGUI's buttons are 0 indexed - return MyGUI::MouseButton::Enum(button - 1); - } - - void initKeyMap(std::map& keyMap) - { - keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None; - keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape; - keyMap[SDLK_1] = MyGUI::KeyCode::One; - keyMap[SDLK_2] = MyGUI::KeyCode::Two; - keyMap[SDLK_3] = MyGUI::KeyCode::Three; - keyMap[SDLK_4] = MyGUI::KeyCode::Four; - keyMap[SDLK_5] = MyGUI::KeyCode::Five; - keyMap[SDLK_6] = MyGUI::KeyCode::Six; - keyMap[SDLK_7] = MyGUI::KeyCode::Seven; - keyMap[SDLK_8] = MyGUI::KeyCode::Eight; - keyMap[SDLK_9] = MyGUI::KeyCode::Nine; - keyMap[SDLK_0] = MyGUI::KeyCode::Zero; - keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus; - keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals; - keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace; - keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab; - keyMap[SDLK_q] = MyGUI::KeyCode::Q; - keyMap[SDLK_w] = MyGUI::KeyCode::W; - keyMap[SDLK_e] = MyGUI::KeyCode::E; - keyMap[SDLK_r] = MyGUI::KeyCode::R; - keyMap[SDLK_t] = MyGUI::KeyCode::T; - keyMap[SDLK_y] = MyGUI::KeyCode::Y; - keyMap[SDLK_u] = MyGUI::KeyCode::U; - keyMap[SDLK_i] = MyGUI::KeyCode::I; - keyMap[SDLK_o] = MyGUI::KeyCode::O; - keyMap[SDLK_p] = MyGUI::KeyCode::P; - keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return; - keyMap[SDLK_a] = MyGUI::KeyCode::A; - keyMap[SDLK_s] = MyGUI::KeyCode::S; - keyMap[SDLK_d] = MyGUI::KeyCode::D; - keyMap[SDLK_f] = MyGUI::KeyCode::F; - keyMap[SDLK_g] = MyGUI::KeyCode::G; - keyMap[SDLK_h] = MyGUI::KeyCode::H; - keyMap[SDLK_j] = MyGUI::KeyCode::J; - keyMap[SDLK_k] = MyGUI::KeyCode::K; - keyMap[SDLK_l] = MyGUI::KeyCode::L; - keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon; - keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe; - keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave; - keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift; - keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash; - keyMap[SDLK_z] = MyGUI::KeyCode::Z; - keyMap[SDLK_x] = MyGUI::KeyCode::X; - keyMap[SDLK_c] = MyGUI::KeyCode::C; - keyMap[SDLK_v] = MyGUI::KeyCode::V; - keyMap[SDLK_b] = MyGUI::KeyCode::B; - keyMap[SDLK_n] = MyGUI::KeyCode::N; - keyMap[SDLK_m] = MyGUI::KeyCode::M; - keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma; - keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period; - keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash; - keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift; - keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply; - keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt; - keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space; - keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital; - keyMap[SDLK_F1] = MyGUI::KeyCode::F1; - keyMap[SDLK_F2] = MyGUI::KeyCode::F2; - keyMap[SDLK_F3] = MyGUI::KeyCode::F3; - keyMap[SDLK_F4] = MyGUI::KeyCode::F4; - keyMap[SDLK_F5] = MyGUI::KeyCode::F5; - keyMap[SDLK_F6] = MyGUI::KeyCode::F6; - keyMap[SDLK_F7] = MyGUI::KeyCode::F7; - keyMap[SDLK_F8] = MyGUI::KeyCode::F8; - keyMap[SDLK_F9] = MyGUI::KeyCode::F9; - keyMap[SDLK_F10] = MyGUI::KeyCode::F10; - keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock; - keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock; - keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7; - keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8; - keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9; - keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract; - keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4; - keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5; - keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6; - keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add; - keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1; - keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2; - keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3; - keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0; - keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal; - keyMap[SDLK_F11] = MyGUI::KeyCode::F11; - keyMap[SDLK_F12] = MyGUI::KeyCode::F12; - keyMap[SDLK_F13] = MyGUI::KeyCode::F13; - keyMap[SDLK_F14] = MyGUI::KeyCode::F14; - keyMap[SDLK_F15] = MyGUI::KeyCode::F15; - keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals; - keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon; - keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter; - keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide; - keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq; - keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt; - keyMap[SDLK_HOME] = MyGUI::KeyCode::Home; - keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp; - keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp; - keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft; - keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight; - keyMap[SDLK_END] = MyGUI::KeyCode::End; - keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown; - keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown; - keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert; - keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete; - keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu; - -//The function of the Ctrl and Meta keys are switched on macOS compared to other platforms. -//For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard -#if defined(__APPLE__) - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows; -#else - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl; -#endif - } - - MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code) - { - static std::map keyMap; - if (keyMap.empty()) - initKeyMap(keyMap); - - MyGUI::KeyCode kc = MyGUI::KeyCode::None; - auto foundKey = keyMap.find(code); - if (foundKey != keyMap.end()) - kc = foundKey->second; - - return kc; - } -} diff --git a/apps/openmw/mwinput/sensormanager.cpp b/apps/openmw/mwinput/sensormanager.cpp index 3e8e70aefe..f3cee579e5 100644 --- a/apps/openmw/mwinput/sensormanager.cpp +++ b/apps/openmw/mwinput/sensormanager.cpp @@ -11,18 +11,10 @@ namespace MWInput { SensorManager::SensorManager() - : mInvertX(Settings::Manager::getBool("invert x axis", "Input")) - , mInvertY(Settings::Manager::getBool("invert y axis", "Input")) - , mGyroXSpeed(0.f) - , mGyroYSpeed(0.f) + : mRotation() + , mGyroValues() , mGyroUpdateTimer(0.f) - , mGyroHSensitivity(Settings::Manager::getFloat("gyro horizontal sensitivity", "Input")) - , mGyroVSensitivity(Settings::Manager::getFloat("gyro vertical sensitivity", "Input")) - , mGyroHAxis(GyroscopeAxis::Minus_X) - , mGyroVAxis(GyroscopeAxis::Y) - , mGyroInputThreshold(Settings::Manager::getFloat("gyro input threshold", "Input")) , mGyroscope(nullptr) - , mGuiCursorEnabled(true) { init(); } @@ -42,24 +34,6 @@ namespace MWInput } } - SensorManager::GyroscopeAxis SensorManager::mapGyroscopeAxis(const std::string& axis) - { - if (axis == "x") - return GyroscopeAxis::X; - else if (axis == "y") - return GyroscopeAxis::Y; - else if (axis == "z") - return GyroscopeAxis::Z; - else if (axis == "-x") - return GyroscopeAxis::Minus_X; - else if (axis == "-y") - return GyroscopeAxis::Minus_Y; - else if (axis == "-z") - return GyroscopeAxis::Minus_Z; - - return GyroscopeAxis::Unknown; - } - void SensorManager::correctGyroscopeAxes() { if (!Settings::Manager::getBool("enable gyroscope", "Input")) @@ -68,40 +42,36 @@ namespace MWInput // Treat setting from config as axes for landscape mode. // If the device does not support orientation change, do nothing. // Note: in is unclear how to correct axes for devices with non-standart Z axis direction. - mGyroHAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro horizontal axis", "Input")); - mGyroVAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro vertical axis", "Input")); + + mRotation = osg::Matrixf::identity(); + + float angle = 0; SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::Manager::getInt("screen", "Video")); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: - return; + break; case SDL_ORIENTATION_LANDSCAPE: break; case SDL_ORIENTATION_LANDSCAPE_FLIPPED: { - mGyroHAxis = GyroscopeAxis(-mGyroHAxis); - mGyroVAxis = GyroscopeAxis(-mGyroVAxis); - + angle = osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT: { - GyroscopeAxis oldVAxis = mGyroVAxis; - mGyroVAxis = mGyroHAxis; - mGyroHAxis = GyroscopeAxis(-oldVAxis); - + angle = -0.5 * osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT_FLIPPED: { - GyroscopeAxis oldVAxis = mGyroVAxis; - mGyroVAxis = GyroscopeAxis(-mGyroHAxis); - mGyroHAxis = oldVAxis; - + angle = 0.5 * osg::PIf; break; } } + + mRotation.makeRotate(angle, osg::Vec3f(0, 0, 1)); } void SensorManager::updateSensors() @@ -119,7 +89,6 @@ namespace MWInput { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; - mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } @@ -141,7 +110,6 @@ namespace MWInput { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; - mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } } @@ -151,46 +119,8 @@ namespace MWInput { for (const auto& setting : changed) { - if (setting.first == "Input" && setting.second == "invert x axis") - mInvertX = Settings::Manager::getBool("invert x axis", "Input"); - - if (setting.first == "Input" && setting.second == "invert y axis") - mInvertY = Settings::Manager::getBool("invert y axis", "Input"); - - if (setting.first == "Input" && setting.second == "gyro horizontal sensitivity") - mGyroHSensitivity = Settings::Manager::getFloat("gyro horizontal sensitivity", "Input"); - - if (setting.first == "Input" && setting.second == "gyro vertical sensitivity") - mGyroVSensitivity = Settings::Manager::getFloat("gyro vertical sensitivity", "Input"); - if (setting.first == "Input" && setting.second == "enable gyroscope") init(); - - if (setting.first == "Input" && setting.second == "gyro horizontal axis") - correctGyroscopeAxes(); - - if (setting.first == "Input" && setting.second == "gyro vertical axis") - correctGyroscopeAxes(); - - if (setting.first == "Input" && setting.second == "gyro input threshold") - mGyroInputThreshold = Settings::Manager::getFloat("gyro input threshold", "Input"); - } - } - - float SensorManager::getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const - { - switch (axis) - { - case GyroscopeAxis::X: - case GyroscopeAxis::Y: - case GyroscopeAxis::Z: - return std::abs(arg.data[0]) >= mGyroInputThreshold ? arg.data[axis-1] : 0.f; - case GyroscopeAxis::Minus_X: - case GyroscopeAxis::Minus_Y: - case GyroscopeAxis::Minus_Z: - return std::abs(arg.data[0]) >= mGyroInputThreshold ? -arg.data[std::abs(axis)-1] : 0.f; - default: - return 0.f; } } @@ -217,10 +147,9 @@ namespace MWInput break; case SDL_SENSOR_GYRO: { - mGyroXSpeed = getGyroAxisSpeed(mGyroHAxis, arg); - mGyroYSpeed = getGyroAxisSpeed(mGyroVAxis, arg); + osg::Vec3f gyro(arg.data[0], arg.data[1], arg.data[2]); + mGyroValues = mRotation * gyro; mGyroUpdateTimer = 0.f; - break; } default: @@ -230,41 +159,24 @@ namespace MWInput void SensorManager::update(float dt) { - if (mGyroXSpeed == 0.f && mGyroYSpeed == 0.f) - return; - + mGyroUpdateTimer += dt; if (mGyroUpdateTimer > 0.5f) { // More than half of second passed since the last gyroscope update. // A device more likely was disconnected or switched to the sleep mode. // Reset current rotation speed and wait for update. - mGyroXSpeed = 0.f; - mGyroYSpeed = 0.f; + mGyroValues = osg::Vec3f(); mGyroUpdateTimer = 0.f; - return; } + } - mGyroUpdateTimer += dt; - - if (!mGuiCursorEnabled) - { - float rot[3]; - rot[0] = -mGyroYSpeed * dt * mGyroVSensitivity * 4 * (mInvertY ? -1 : 1); - rot[1] = 0.0f; - rot[2] = -mGyroXSpeed * dt * mGyroHSensitivity * 4 * (mInvertX ? -1 : 1); - - // Only actually turn player when we're not in vanity mode - 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 (!playerLooking) - MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); + bool SensorManager::isGyroAvailable() const + { + return mGyroscope != nullptr; + } - MWBase::Environment::get().getInputManager()->resetIdleTime(); - } + std::array SensorManager::getGyroValues() const + { + return { mGyroValues.x(), mGyroValues.y(), mGyroValues.z() }; } } diff --git a/apps/openmw/mwinput/sensormanager.hpp b/apps/openmw/mwinput/sensormanager.hpp index 75472d43b4..8f72b99de9 100644 --- a/apps/openmw/mwinput/sensormanager.hpp +++ b/apps/openmw/mwinput/sensormanager.hpp @@ -3,6 +3,9 @@ #include +#include +#include + #include #include @@ -33,41 +36,19 @@ namespace MWInput void displayOrientationChanged() override; void processChangedSettings(const Settings::CategorySettingVector& changed); - void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + bool isGyroAvailable() const; + std::array getGyroValues() const; private: - enum GyroscopeAxis - { - Unknown = 0, - X = 1, - Y = 2, - Z = 3, - Minus_X = -1, - Minus_Y = -2, - Minus_Z = -3 - }; void updateSensors(); void correctGyroscopeAxes(); - GyroscopeAxis mapGyroscopeAxis(const std::string& axis); - float getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const; - - bool mInvertX; - bool mInvertY; - float mGyroXSpeed; - float mGyroYSpeed; + osg::Matrixf mRotation; + osg::Vec3f mGyroValues; float mGyroUpdateTimer; - float mGyroHSensitivity; - float mGyroVSensitivity; - GyroscopeAxis mGyroHAxis; - GyroscopeAxis mGyroVAxis; - float mGyroInputThreshold; - SDL_Sensor* mGyroscope; - - bool mGuiCursorEnabled; }; } #endif diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp new file mode 100644 index 0000000000..d70e9bfea5 --- /dev/null +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -0,0 +1,73 @@ +#include "luabindings.hpp" + +#include "luamanagerimp.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + struct TimerCallback + { + AsyncPackageId mAsyncId; + std::string mName; + }; + + sol::function getAsyncPackageInitializer(const Context& context) + { + using TimerType = LuaUtil::ScriptsContainer::TimerType; + sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); + api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) + { + asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); + return TimerCallback{asyncId, std::string(name)}; + }; + api["newSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) + { + callback.mAsyncId.mContainer->setupSerializableTimer( + TimerType::SIMULATION_TIME, world->getSimulationTime() + delay, + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); + }; + api["newGameTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) + { + callback.mAsyncId.mContainer->setupSerializableTimer( + TimerType::GAME_TIME, world->getGameTime() + delay, + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); + }; + api["newUnsavableSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + { + asyncId.mContainer->setupUnsavableTimer( + TimerType::SIMULATION_TIME, world->getSimulationTime() + delay, asyncId.mScriptId, std::move(callback)); + }; + api["newUnsavableGameTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + { + asyncId.mContainer->setupUnsavableTimer( + TimerType::GAME_TIME, world->getGameTime() + delay, asyncId.mScriptId, std::move(callback)); + }; + api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) -> LuaUtil::Callback + { + return LuaUtil::Callback{std::move(fn), asyncId.mHiddenData}; + }; + + sol::usertype callbackType = context.mLua->sol().new_usertype("Callback"); + callbackType[sol::meta_function::call] = + [](const LuaUtil::Callback& callback, sol::variadic_args va) { return callback.call(sol::as_args(va)); }; + + auto initializer = [](sol::table hiddenData) + { + LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; + return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; + }; + return sol::make_object(context.mLua->sol(), initializer); + } + +} diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp new file mode 100644 index 0000000000..b45c4e30c6 --- /dev/null +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -0,0 +1,122 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwrender/camera.hpp" +#include "../mwrender/renderingmanager.hpp" + +namespace MWLua +{ + + using CameraMode = MWRender::Camera::Mode; + + sol::table initCameraPackage(const Context& context) + { + MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera(); + MWRender::RenderingManager* renderingManager = MWBase::Environment::get().getWorld()->getRenderingManager(); + + sol::table api(context.mLua->sol(), sol::create); + api["MODE"] = LuaUtil::makeStrictReadOnly(context.mLua->sol().create_table_with( + "Static", CameraMode::Static, + "FirstPerson", CameraMode::FirstPerson, + "ThirdPerson", CameraMode::ThirdPerson, + "Vanity", CameraMode::Vanity, + "Preview", CameraMode::Preview + )); + + api["getMode"] = [camera]() -> int { return static_cast(camera->getMode()); }; + api["getQueuedMode"] = [camera]() -> sol::optional + { + std::optional mode = camera->getQueuedMode(); + if (mode) + return static_cast(*mode); + else + return sol::nullopt; + }; + api["setMode"] = [camera](int mode, sol::optional force) + { + camera->setMode(static_cast(mode), force ? *force : false); + }; + + api["allowCharacterDeferredRotation"] = [camera](bool v) { camera->allowCharacterDeferredRotation(v); }; + api["showCrosshair"] = [camera](bool v) { camera->showCrosshair(v); }; + + api["getTrackedPosition"] = [camera]() -> osg::Vec3f { return camera->getTrackedPosition(); }; + api["getPosition"] = [camera]() -> osg::Vec3f { return camera->getPosition(); }; + + // All angles are negated in order to make camera rotation consistent with objects rotation. + // TODO: Fix the inconsistency of rotation direction in camera.cpp. + api["getPitch"] = [camera]() { return -camera->getPitch(); }; + api["getYaw"] = [camera]() { return -camera->getYaw(); }; + api["getRoll"] = [camera]() { return -camera->getRoll(); }; + + api["setStaticPosition"] = [camera](const osg::Vec3f& pos) { camera->setStaticPosition(pos); }; + api["setPitch"] = [camera](float v) + { + camera->setPitch(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setYaw"] = [camera](float v) + { + camera->setYaw(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setRoll"] = [camera](float v) { camera->setRoll(-v); }; + api["setExtraPitch"] = [camera](float v) { camera->setExtraPitch(-v); }; + api["setExtraYaw"] = [camera](float v) { camera->setExtraYaw(-v); }; + api["setExtraRoll"] = [camera](float v) { camera->setExtraRoll(-v); }; + api["getExtraPitch"] = [camera]() { return -camera->getExtraPitch(); }; + api["getExtraYaw"] = [camera]() { return -camera->getExtraYaw(); }; + api["getExtraRoll"] = [camera]() { return -camera->getExtraRoll(); }; + + api["getThirdPersonDistance"] = [camera]() { return camera->getCameraDistance(); }; + api["setPreferredThirdPersonDistance"] = [camera](float v) { camera->setPreferredCameraDistance(v); }; + + api["getFirstPersonOffset"] = [camera]() { return camera->getFirstPersonOffset(); }; + api["setFirstPersonOffset"] = [camera](const osg::Vec3f& v) { camera->setFirstPersonOffset(v); }; + + api["getFocalPreferredOffset"] = [camera]() -> osg::Vec2f { return camera->getFocalPointTargetOffset(); }; + api["setFocalPreferredOffset"] = [camera](const osg::Vec2f& v) { camera->setFocalPointTargetOffset(v); }; + api["getFocalTransitionSpeed"] = [camera]() { return camera->getFocalPointTransitionSpeed(); }; + api["setFocalTransitionSpeed"] = [camera](float v) { camera->setFocalPointTransitionSpeed(v); }; + api["instantTransition"] = [camera]() { camera->instantTransition(); }; + + api["getCollisionType"] = [camera]() { return camera->getCollisionType(); }; + api["setCollisionType"] = [camera](int collisionType) { camera->setCollisionType(collisionType); }; + + api["getBaseFieldOfView"] = []() + { + return osg::DegreesToRadians(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f)); + }; + api["getFieldOfView"] = [renderingManager]() { return osg::DegreesToRadians(renderingManager->getFieldOfView()); }; + api["setFieldOfView"] = [renderingManager](float v) { renderingManager->setFieldOfView(osg::RadiansToDegrees(v)); }; + + api["getBaseViewDistance"] = []() + { + return std::max(0.f, Settings::Manager::getFloat("viewing distance", "Camera")); + }; + api["getViewDistance"] = [renderingManager]() { return renderingManager->getViewDistance(); }; + api["setViewDistance"] = [renderingManager](float d) { renderingManager->setViewDistance(d, true); }; + + 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"); + double aspect = (height == 0.0) ? 1.0 : width / height; + double fovTan = std::tan(osg::DegreesToRadians(renderingManager->getFieldOfView()) / 2); + osg::Matrixf invertedViewMatrix; + invertedViewMatrix.invert(camera->getViewMatrix()); + float x = (pos.x() * 2 - 1) * aspect * fovTan; + float y = (1 - pos.y() * 2) * fovTan; + return invertedViewMatrix.preMult(osg::Vec3f(x, y, -1)) - camera->getPosition(); + }; + + return LuaUtil::makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp new file mode 100644 index 0000000000..5048e68b7e --- /dev/null +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -0,0 +1,130 @@ +#include "luabindings.hpp" + +#include + +#include "../mwworld/cellstore.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + template + static void initCellBindings(const std::string& prefix, const Context& context) + { + sol::usertype cellT = context.mLua->sol().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) + { + const ESM::Cell* cell = c.mStore->getCell(); + std::stringstream res; + if (cell->isExterior()) + res << "exterior(" << cell->getGridX() << ", " << cell->getGridY() << ")"; + else + res << "interior(" << cell->mName << ")"; + return res.str(); + }; + + cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mName; }); + cellT["region"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mRegion; }); + cellT["gridX"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridX(); }); + cellT["gridY"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridY(); }); + cellT["hasWater"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->hasWater(); }); + cellT["isExterior"] = sol::readonly_property([](const CellT& c) { return c.mStore->isExterior(); }); + cellT["isQuasiExterior"] = sol::readonly_property([](const CellT& c) + { + return (c.mStore->getCell()->mData.mFlags & ESM::Cell::QuasiEx) != 0; + }); + + cellT["isInSameSpace"] = [](const CellT& c, const ObjectT& obj) + { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.isInCell()) + return false; + MWWorld::CellStore* cell = ptr.getCell(); + return cell == c.mStore || (cell->isExterior() && c.mStore->isExterior()); + }; + + if constexpr (std::is_same_v) + { // only for global scripts + cellT["getAll"] = [worldView=context.mWorldView, ids=getPackageToTypeTable(context.mLua->sol())]( + const CellT& cell, sol::optional type) + { + ObjectIdList res = std::make_shared>(); + auto visitor = [&](const MWWorld::Ptr& ptr) + { + worldView->getObjectRegistry()->registerPtr(ptr); + if (getLiveCellRefType(ptr.mRef) == ptr.getType()) + res->push_back(getId(ptr)); + return true; + }; + + bool ok = false; + sol::optional typeId = sol::nullopt; + if (type.has_value()) + typeId = ids[*type]; + else + { + ok = true; + cell.mStore->forEach(std::move(visitor)); + } + if (typeId.has_value()) + { + ok = true; + switch (*typeId) + { + case ESM::REC_INTERNAL_PLAYER: + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getCell() == cell.mStore) + res->push_back(getId(player)); + } + break; + + case ESM::REC_CREA: cell.mStore->template forEachType(visitor); break; + case ESM::REC_NPC_: cell.mStore->template forEachType(visitor); break; + case ESM::REC_ACTI: cell.mStore->template forEachType(visitor); break; + case ESM::REC_DOOR: cell.mStore->template forEachType(visitor); break; + case ESM::REC_CONT: cell.mStore->template forEachType(visitor); break; + + case ESM::REC_ALCH: cell.mStore->template forEachType(visitor); break; + case ESM::REC_ARMO: cell.mStore->template forEachType(visitor); break; + case ESM::REC_BOOK: cell.mStore->template forEachType(visitor); break; + case ESM::REC_CLOT: cell.mStore->template forEachType(visitor); break; + case ESM::REC_INGR: cell.mStore->template forEachType(visitor); break; + case ESM::REC_LIGH: cell.mStore->template forEachType(visitor); break; + case ESM::REC_MISC: cell.mStore->template forEachType(visitor); break; + case ESM::REC_WEAP: cell.mStore->template forEachType(visitor); break; + case ESM::REC_APPA: cell.mStore->template forEachType(visitor); break; + case ESM::REC_LOCK: cell.mStore->template forEachType(visitor); break; + case ESM::REC_PROB: cell.mStore->template forEachType(visitor); break; + case ESM::REC_REPA: cell.mStore->template forEachType(visitor); break; + default: ok = false; + } + } + if (!ok) + throw std::runtime_error(std::string("Incorrect type argument in cell:getAll: " + LuaUtil::toString(*type))); + return GObjectList{res}; + }; + } + } + + void initCellBindingsForLocalScripts(const Context& context) + { + initCellBindings("L", context); + } + + void initCellBindingsForGlobalScripts(const Context& context) + { + initCellBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp new file mode 100644 index 0000000000..124e7f06be --- /dev/null +++ b/apps/openmw/mwlua/context.hpp @@ -0,0 +1,32 @@ +#ifndef MWLUA_CONTEXT_H +#define MWLUA_CONTEXT_H + +#include "eventqueue.hpp" + +namespace LuaUtil +{ + class LuaState; + class UserdataSerializer; + class L10nManager; +} + +namespace MWLua +{ + class LuaManager; + class WorldView; + + struct Context + { + bool mIsGlobal; + LuaManager* mLuaManager; + LuaUtil::LuaState* mLua; + LuaUtil::UserdataSerializer* mSerializer; + LuaUtil::L10nManager* mL10n; + WorldView* mWorldView; + LocalEventQueue* mLocalEventQueue; + GlobalEventQueue* mGlobalEventQueue; + }; + +} + +#endif // MWLUA_CONTEXT_H diff --git a/apps/openmw/mwlua/debugbindings.cpp b/apps/openmw/mwlua/debugbindings.cpp new file mode 100644 index 0000000000..fe00fa72b2 --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.cpp @@ -0,0 +1,51 @@ +#include "debugbindings.hpp" +#include "context.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwrender/renderingmanager.hpp" + +#include + +namespace MWLua +{ + sol::table initDebugPackage(const Context& context) + { + sol::table api = context.mLua->newTable(); + + 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}, + })); + + api["toggleRenderMode"] = [context] (MWRender::RenderMode value) + { + context.mLuaManager->addAction([value] + { + MWBase::Environment::get().getWorld()->toggleRenderMode(value); + }); + }; + + api["NAV_MESH_RENDER_MODE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + {"AreaType", MWRender::NavMeshMode::AreaType}, + {"UpdateFrequency", MWRender::NavMeshMode::UpdateFrequency}, + })); + + api["setNavMeshRenderMode"] = [context] (MWRender::NavMeshMode value) + { + context.mLuaManager->addAction([value] + { + MWBase::Environment::get().getWorld()->getRenderingManager()->setNavMeshMode(value); + }); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/debugbindings.hpp b/apps/openmw/mwlua/debugbindings.hpp new file mode 100644 index 0000000000..c508b54496 --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_MWLUA_DEBUGBINDINGS_H +#define OPENMW_MWLUA_DEBUGBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initDebugPackage(const Context& context); +} + +#endif // OPENMW_MWLUA_DEBUGBINDINGS_H diff --git a/apps/openmw/mwlua/eventqueue.cpp b/apps/openmw/mwlua/eventqueue.cpp new file mode 100644 index 0000000000..86086f29db --- /dev/null +++ b/apps/openmw/mwlua/eventqueue.cpp @@ -0,0 +1,63 @@ +#include "eventqueue.hpp" + +#include + +#include +#include +#include + +#include + +namespace MWLua +{ + + template + void saveEvent(ESM::ESMWriter& esm, const ObjectId& dest, const Event& event) + { + esm.writeHNString("LUAE", event.mEventName); + dest.save(esm, true); + if (!event.mEventData.empty()) + saveLuaBinaryData(esm, event.mEventData); + } + + void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents, LocalEventQueue& localEvents, + const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer) + { + while (esm.isNextSub("LUAE")) + { + std::string name = esm.getHString(); + ObjectId dest; + dest.load(esm, true); + std::string data = loadLuaBinaryData(esm); + try + { + data = LuaUtil::serialize(LuaUtil::deserialize(lua, data, serializer), serializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << "loadEvent: invalid event data: " << e.what(); + } + if (dest.isSet()) + { + auto it = contentFileMapping.find(dest.mContentFile); + if (it != contentFileMapping.end()) + dest.mContentFile = it->second; + localEvents.push_back({dest, std::move(name), std::move(data)}); + } + else + globalEvents.push_back({std::move(name), std::move(data)}); + } + } + + void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue& globalEvents, const LocalEventQueue& localEvents) + { + ObjectId globalId; + globalId.unset(); // Used as a marker of a global event. + + for (const GlobalEvent& e : globalEvents) + saveEvent(esm, globalId, e); + for (const LocalEvent& e : localEvents) + saveEvent(esm, e.mDest, e); + } + +} diff --git a/apps/openmw/mwlua/eventqueue.hpp b/apps/openmw/mwlua/eventqueue.hpp new file mode 100644 index 0000000000..0e5f2dfcb4 --- /dev/null +++ b/apps/openmw/mwlua/eventqueue.hpp @@ -0,0 +1,43 @@ +#ifndef MWLUA_EVENTQUEUE_H +#define MWLUA_EVENTQUEUE_H + +#include "object.hpp" + +namespace ESM +{ + class ESMReader; + class ESMWriter; +} + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace sol +{ + class state; +} + +namespace MWLua +{ + struct GlobalEvent + { + std::string mEventName; + std::string mEventData; + }; + struct LocalEvent + { + ObjectId mDest; + std::string mEventName; + std::string mEventData; + }; + using GlobalEventQueue = std::vector; + using LocalEventQueue = std::vector; + + void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&, + const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer); + void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&); +} + +#endif // MWLUA_EVENTQUEUE_H diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp new file mode 100644 index 0000000000..5f8dc1ecf0 --- /dev/null +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -0,0 +1,46 @@ +#ifndef MWLUA_GLOBALSCRIPTS_H +#define MWLUA_GLOBALSCRIPTS_H + +#include +#include +#include + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class GlobalScripts : public LuaUtil::ScriptsContainer + { + public: + GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + { + registerEngineHandlers({ + &mObjectActiveHandlers, + &mActorActiveHandlers, + &mItemActiveHandlers, + &mNewGameHandlers, + &mPlayerAddedHandlers + }); + } + + void newGameStarted() { callEngineHandlers(mNewGameHandlers); } + void objectActive(const GObject& obj) { callEngineHandlers(mObjectActiveHandlers, obj); } + void actorActive(const GObject& obj) { callEngineHandlers(mActorActiveHandlers, obj); } + void itemActive(const GObject& obj) { callEngineHandlers(mItemActiveHandlers, obj); } + void playerAdded(const GObject& obj) { callEngineHandlers(mPlayerAddedHandlers, obj); } + + private: + EngineHandlerList mObjectActiveHandlers{"onObjectActive"}; + EngineHandlerList mActorActiveHandlers{"onActorActive"}; + EngineHandlerList mItemActiveHandlers{"onItemActive"}; + EngineHandlerList mNewGameHandlers{"onNewGame"}; + EngineHandlerList mPlayerAddedHandlers{"onPlayerAdded"}; + }; + +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp new file mode 100644 index 0000000000..bde2a52f8e --- /dev/null +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -0,0 +1,301 @@ +#include "luabindings.hpp" + +#include +#include +#include + +#include + +#include "../mwbase/inputmanager.hpp" +#include "../mwinput/actions.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + sol::table initInputPackage(const Context& context) + { + sol::usertype keyEvent = context.mLua->sol().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)); + else + return std::string(); + }); + keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; }); + keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); + keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); + 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"); + 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 { return {e.mX, e.mY};}); + touchpadEvent["pressure"] = sol::readonly_property( + [](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); + + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + sol::table api(context.mLua->sol(), sol::create); + + api["isIdle"] = [input]() { return input->isIdle(); }; + api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; + api["isKeyPressed"] = [](SDL_Scancode code) -> bool + { + int maxCode; + const auto* state = SDL_GetKeyboardState(&maxCode); + if (code >= 0 && code < maxCode) + return state[code] != 0; + else + return false; + }; + api["isShiftPressed"] = []() -> bool { return SDL_GetModState() & KMOD_SHIFT; }; + api["isCtrlPressed"] = []() -> bool { return SDL_GetModState() & KMOD_CTRL; }; + api["isAltPressed"] = []() -> bool { return SDL_GetModState() & KMOD_ALT; }; + api["isSuperPressed"] = []() -> bool { return SDL_GetModState() & KMOD_GUI; }; + api["isControllerButtonPressed"] = [input](int button) + { + return input->isControllerButtonPressed(static_cast(button)); + }; + api["isMouseButtonPressed"] = [](int button) -> bool + { + return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button); + }; + api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); }; + api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); }; + api["getAxisValue"] = [input](int axis) + { + if (axis < SDL_CONTROLLER_AXIS_MAX) + return input->getControllerAxisValue(static_cast(axis)); + else + return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1; + }; + + 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["ACTION"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + {"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}, + + {"Activate", MWInput::A_Activate}, + {"Use", MWInput::A_Use}, + {"Jump", MWInput::A_Jump}, + {"AutoMove", MWInput::A_AutoMove}, + {"Rest", MWInput::A_Rest}, + {"Journal", MWInput::A_Journal}, + {"Weapon", MWInput::A_Weapon}, + {"Spell", MWInput::A_Spell}, + {"Run", MWInput::A_Run}, + {"CycleSpellLeft", MWInput::A_CycleSpellLeft}, + {"CycleSpellRight", MWInput::A_CycleSpellRight}, + {"CycleWeaponLeft", MWInput::A_CycleWeaponLeft}, + {"CycleWeaponRight", MWInput::A_CycleWeaponRight}, + {"ToggleSneak", MWInput::A_ToggleSneak}, + {"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}, + + {"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}, + + {"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"} + })); + + 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} + })); + + 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}, + + {"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["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}, + + {"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}, + + {"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}, + + {"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}, + + {"LeftArrow", SDL_SCANCODE_LEFT}, + {"RightArrow", SDL_SCANCODE_RIGHT}, + {"UpArrow", SDL_SCANCODE_UP}, + {"DownArrow", SDL_SCANCODE_DOWN}, + + {"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); + } + +} diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp new file mode 100644 index 0000000000..4fd9920cb5 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.cpp @@ -0,0 +1,193 @@ +#include "localscripts.hpp" + +#include + +#include "../mwworld/ptr.hpp" +#include "../mwworld/class.hpp" +#include "../mwmechanics/aisequence.hpp" +#include "../mwmechanics/aicombat.hpp" +#include "../mwmechanics/aiescort.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/aipursue.hpp" +#include "../mwmechanics/aitravel.hpp" +#include "../mwmechanics/aiwander.hpp" +#include "../mwmechanics/aipackage.hpp" +#include "../mwmechanics/creaturestats.hpp" + +#include "luamanagerimp.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + void LocalScripts::initializeSelfPackage(const Context& context) + { + using ActorControls = MWBase::LuaManager::ActorControls; + sol::usertype controls = context.mLua->sol().new_usertype("ActorControls"); + +#define CONTROL(TYPE, FIELD) sol::property([](const ActorControls& c) { return c.FIELD; },\ + [](ActorControls& c, const TYPE& v) { c.FIELD = v; c.mChanged = true; }) + controls["movement"] = CONTROL(float, mMovement); + controls["sideMovement"] = CONTROL(float, mSideMovement); + controls["pitchChange"] = CONTROL(float, mPitchChange); + controls["yawChange"] = CONTROL(float, mYawChange); + controls["run"] = CONTROL(bool, mRun); + controls["jump"] = CONTROL(bool, mJump); + controls["use"] = CONTROL(int, mUse); +#undef CONTROL + + sol::usertype selfAPI = + context.mLua->sol().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; }; + + using AiPackage = MWMechanics::AiPackage; + sol::usertype aiPackage = context.mLua->sol().new_usertype("AiPackage"); + aiPackage["type"] = sol::readonly_property([](const AiPackage& p) -> std::string_view + { + switch (p.getTypeId()) + { + case MWMechanics::AiPackageTypeId::Wander: return "Wander"; + case MWMechanics::AiPackageTypeId::Travel: return "Travel"; + case MWMechanics::AiPackageTypeId::Escort: return "Escort"; + case MWMechanics::AiPackageTypeId::Follow: return "Follow"; + case MWMechanics::AiPackageTypeId::Activate: return "Activate"; + case MWMechanics::AiPackageTypeId::Combat: return "Combat"; + case MWMechanics::AiPackageTypeId::Pursue: return "Pursue"; + case MWMechanics::AiPackageTypeId::AvoidDoor: return "AvoidDoor"; + case MWMechanics::AiPackageTypeId::Face: return "Face"; + case MWMechanics::AiPackageTypeId::Breathe: return "Breathe"; + case MWMechanics::AiPackageTypeId::Cast: return "Cast"; + default: return "Unknown"; + } + }); + aiPackage["target"] = sol::readonly_property([worldView=context.mWorldView](const AiPackage& p) -> sol::optional + { + MWWorld::Ptr target = p.getTarget(); + if (target.isEmpty()) + return sol::nullopt; + else + return LObject(getId(target), worldView->getObjectRegistry()); + }); + aiPackage["sideWithTarget"] = sol::readonly_property([](const AiPackage& p) { return p.sideWithTarget(); }); + aiPackage["destPosition"] = sol::readonly_property([](const AiPackage& p) { return p.getDestination(); }); + + selfAPI["_getActiveAiPackage"] = [](SelfObject& self) -> sol::optional> + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (ai.isEmpty()) + return sol::nullopt; + else + return *ai.begin(); + }; + selfAPI["_iterateAndFilterAiSequence"] = [](SelfObject& self, sol::function callback) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + + ai.erasePackagesIf([&](auto& entry) + { + bool keep = LuaUtil::call(callback, entry).template get(); + return !keep; + }); + }; + selfAPI["_startAiCombat"] = [](SelfObject& self, const LObject& target) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiCombat(target.ptr()), ptr); + }; + selfAPI["_startAiPursue"] = [](SelfObject& self, const LObject& target) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiPursue(target.ptr()), ptr); + }; + selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiFollow(target.ptr()), ptr); + }; + selfAPI["_startAiEscort"] = [](SelfObject& self, const LObject& target, LCell cell, + float duration, const osg::Vec3f& dest) + { + 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. + const std::string& refId = target.ptr().getCellRef().getRefId(); + int gameHoursDuration = static_cast(std::ceil(duration / 3600.0)); + const ESM::Cell* esmCell = cell.mStore->getCell(); + if (esmCell->isExterior()) + ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr); + else + ai.stack(MWMechanics::AiEscort(refId, esmCell->mName, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr); + }; + selfAPI["_startAiWander"] = [](SelfObject& self, int distance, float duration) + { + 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); + }; + selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target) + { + 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); + }; + } + + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + { + this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); + registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers}); + } + + void LocalScripts::receiveEngineEvent(const EngineEvent& event) + { + std::visit([this](auto&& arg) + { + using EventT = std::decay_t; + if constexpr (std::is_same_v) + { + mData.mIsActive = true; + callEngineHandlers(mOnActiveHandlers); + } + else if constexpr (std::is_same_v) + { + mData.mIsActive = false; + callEngineHandlers(mOnInactiveHandlers); + } + else if constexpr (std::is_same_v) + { + callEngineHandlers(mOnActivatedHandlers, arg.mActivatingActor); + } + else + { + static_assert(std::is_same_v); + callEngineHandlers(mOnConsumeHandlers, arg.mConsumable); + } + }, event); + } + + void LocalScripts::applyStatsCache() + { + const auto& ptr = mData.ptr(); + for (auto& [stat, value] : mData.mStatsCache) + stat(ptr, value); + mData.mStatsCache.clear(); + } +} diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp new file mode 100644 index 0000000000..ff2e2bff25 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.hpp @@ -0,0 +1,84 @@ +#ifndef MWLUA_LOCALSCRIPTS_H +#define MWLUA_LOCALSCRIPTS_H + +#include +#include +#include + +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "object.hpp" +#include "luabindings.hpp" + +namespace MWLua +{ + + class LocalScripts : public LuaUtil::ScriptsContainer + { + public: + static void initializeSelfPackage(const Context&); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + + MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } + + struct SelfObject : public LObject + { + class CachedStat + { + public: + using Setter = void(*)(int, std::string_view, const MWWorld::Ptr&, const sol::object&); + private: + Setter mSetter; // Function that updates a stat's property + int mIndex; // Optional index to disambiguate the stat + std::string_view mProp; // Name of the stat's property + public: + CachedStat(Setter setter, int index, std::string_view prop) : mSetter(setter), mIndex(index), mProp(std::move(prop)) {} + + void operator()(const MWWorld::Ptr& ptr, const sol::object& object) const + { + mSetter(mIndex, mProp, ptr, object); + } + + bool operator<(const CachedStat& other) const + { + return std::tie(mSetter, mIndex, mProp) < std::tie(other.mSetter, other.mIndex, other.mProp); + } + }; + + SelfObject(const LObject& obj) : LObject(obj), mIsActive(false) {} + MWBase::LuaManager::ActorControls mControls; + std::map mStatsCache; + bool mIsActive; + }; + + struct OnActive {}; + struct OnInactive {}; + struct OnActivated + { + LObject mActivatingActor; + }; + struct OnConsume + { + LObject mConsumable; + }; + using EngineEvent = std::variant; + + void receiveEngineEvent(const EngineEvent&); + + void applyStatsCache(); + protected: + SelfObject mData; + + private: + EngineHandlerList mOnActiveHandlers{"onActive"}; + EngineHandlerList mOnInactiveHandlers{"onInactive"}; + EngineHandlerList mOnConsumeHandlers{"onConsume"}; + EngineHandlerList mOnActivatedHandlers{"onActivated"}; + }; + +} + +#endif // MWLUA_LOCALSCRIPTS_H diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp new file mode 100644 index 0000000000..705209a603 --- /dev/null +++ b/apps/openmw/mwlua/luabindings.cpp @@ -0,0 +1,139 @@ +#include "luabindings.hpp" + +#include + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/store.hpp" + +#include "eventqueue.hpp" +#include "worldview.hpp" +#include "luamanagerimp.hpp" +#include "types/types.hpp" + +namespace MWLua +{ + + static void addTimeBindings(sol::table& api, const Context& context, bool global) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + + api["getSimulationTime"] = [world=context.mWorldView]() { return world->getSimulationTime(); }; + api["getSimulationTimeScale"] = [world]() { return world->getSimulationTimeScale(); }; + api["getGameTime"] = [world=context.mWorldView]() { return world->getGameTime(); }; + api["getGameTimeScale"] = [world=context.mWorldView]() { return world->getGameTimeScale(); }; + api["isWorldPaused"] = [world=context.mWorldView]() { return world->isPaused(); }; + api["getRealTime"] = []() + { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + + if (!global) + return; + + api["setGameTimeScale"] = [world=context.mWorldView](double scale) { world->setGameTimeScale(scale); }; + + api["setSimulationTimeScale"] = [context, world](float scale) + { + context.mLuaManager->addAction([scale, world] { + world->setSimulationTimeScale(scale); + }); + }; + + // TODO: Ability to pause/resume world from Lua (needed for UI dehardcoding) + // api["pause"] = []() {}; + // api["resume"] = []() {}; + } + + sol::table initCorePackage(const Context& context) + { + auto* lua = context.mLua; + sol::table api(lua->sol(), sol::create); + api["API_REVISION"] = 27; + 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.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); + }; + addTimeBindings(api, context, false); + api["l10n"] = [l10n=context.mL10n](const std::string& context, const sol::object &fallbackLocale) { + if (fallbackLocale == sol::nil) + return l10n->getContext(context); + else + return l10n->getContext(context, fallbackLocale.as()); + }; + const MWWorld::Store* gmst = &MWBase::Environment::get().getWorld()->getStore().get(); + api["getGMST"] = [lua=context.mLua, gmst](const std::string& setting) -> sol::object + { + const ESM::Variant& value = gmst->find(setting)->mValue; + if (value.getType() == ESM::VT_String) + return sol::make_object(lua->sol(), value.getString()); + else if (value.getType() == ESM::VT_Int) + return sol::make_object(lua->sol(), value.getInteger()); + else + return sol::make_object(lua->sol(), value.getFloat()); + }; + return LuaUtil::makeReadOnly(api); + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + addTimeBindings(api, context, true); + api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findNamedCell(name); + if (cell) + return GCell{cell}; + else + return sol::nullopt; + }; + api["getExteriorCell"] = [worldView=context.mWorldView](int x, int y) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findExteriorCell(x, y); + if (cell) + return GCell{cell}; + else + return sol::nullopt; + }; + api["activeActors"] = GObjectList{worldView->getActorsInScene()}; + // TODO: add world.placeNewObject(recordId, cell, pos, [rot]) + return LuaUtil::makeReadOnly(api); + } + + sol::table initGlobalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getMutableSection(section); }; + res["allGlobalSections"] = [globalStorage]() { return globalStorage->getAllSections(); }; + return LuaUtil::makeReadOnly(res); + } + + sol::table initLocalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); }; + return LuaUtil::makeReadOnly(res); + } + + sol::table initPlayerStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); }; + res["playerSection"] = [playerStorage](std::string_view section) { return playerStorage->getMutableSection(section); }; + res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); }; + return LuaUtil::makeReadOnly(res); + } + +} + diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp new file mode 100644 index 0000000000..5760a383ec --- /dev/null +++ b/apps/openmw/mwlua/luabindings.hpp @@ -0,0 +1,62 @@ +#ifndef MWLUA_LUABINDINGS_H +#define MWLUA_LUABINDINGS_H + +#include +#include +#include +#include + +#include "context.hpp" +#include "eventqueue.hpp" +#include "object.hpp" +#include "worldview.hpp" + +namespace MWWorld +{ + class CellStore; +} + +namespace MWLua +{ + + sol::table initCorePackage(const Context&); + sol::table initWorldPackage(const Context&); + sol::table initPostprocessingPackage(const Context&); + + sol::table initGlobalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage); + sol::table initLocalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage); + sol::table initPlayerStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage); + + // Implemented in nearbybindings.cpp + sol::table initNearbyPackage(const Context&); + + // Implemented in objectbindings.cpp + void initObjectBindingsForLocalScripts(const Context&); + void initObjectBindingsForGlobalScripts(const Context&); + + // Implemented in cellbindings.cpp + void initCellBindingsForLocalScripts(const Context&); + void initCellBindingsForGlobalScripts(const Context&); + + // Implemented in asyncbindings.cpp + struct AsyncPackageId + { + LuaUtil::ScriptsContainer* mContainer; + int mScriptId; + sol::table mHiddenData; + }; + sol::function getAsyncPackageInitializer(const Context&); + + // Implemented in camerabindings.cpp + sol::table initCameraPackage(const Context&); + + // Implemented in uibindings.cpp + sol::table initUserInterfacePackage(const Context&); + + // Implemented in inputbindings.cpp + sol::table initInputPackage(const Context&); + + // openmw.self package is implemented in localscripts.cpp +} + +#endif // MWLUA_LUABINDINGS_H diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp new file mode 100644 index 0000000000..1678743257 --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -0,0 +1,605 @@ +#include "luamanagerimp.hpp" + +#include + +#include + +#include +#include +#include + +#include + +#include + +#include + +#include "../mwbase/windowmanager.hpp" + +#include "../mwrender/postprocessor.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/ptr.hpp" + +#include "luabindings.hpp" +#include "userdataserializer.hpp" +#include "types/types.hpp" +#include "debugbindings.hpp" + +namespace MWLua +{ + + LuaManager::LuaManager(const VFS::Manager* vfs, const std::string& libsDir) + : mLua(vfs, &mConfiguration) + , mUiResourceManager(vfs) + , mL10n(vfs, &mLua) + { + Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); + mLua.addInternalLibSearchPath(libsDir); + + mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); + mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); + mGlobalLoader = createUserdataSerializer(false, mWorldView.getObjectRegistry(), &mContentFileMapping); + mLocalLoader = createUserdataSerializer(true, mWorldView.getObjectRegistry(), &mContentFileMapping); + + mGlobalScripts.setSerializer(mGlobalSerializer.get()); + } + + void LuaManager::initConfiguration() + { + mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().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]); + mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf()); + } + + void LuaManager::initL10n() + { + mL10n.init(); + std::vector preferredLocales; + Misc::StringUtils::split(Settings::Manager::getString("preferred locales", "General"), preferredLocales, ", "); + mL10n.setPreferredLocales(preferredLocales); + } + + void LuaManager::init() + { + Context context; + context.mIsGlobal = true; + context.mLuaManager = this; + context.mLua = &mLua; + context.mL10n = &mL10n; + context.mWorldView = &mWorldView; + context.mLocalEventQueue = &mLocalEvents; + context.mGlobalEventQueue = &mGlobalEvents; + context.mSerializer = mGlobalSerializer.get(); + + Context localContext = context; + localContext.mIsGlobal = false; + localContext.mSerializer = mLocalSerializer.get(); + + initObjectBindingsForGlobalScripts(context); + initCellBindingsForGlobalScripts(context); + initObjectBindingsForLocalScripts(localContext); + initCellBindingsForLocalScripts(localContext); + LocalScripts::initializeSelfPackage(localContext); + LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); + + mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context)); + mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); + mLua.addCommonPackage("openmw.core", initCorePackage(context)); + mLua.addCommonPackage("openmw.types", initTypesPackage(context)); + mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); + mGlobalScripts.addPackage("openmw.storage", initGlobalStoragePackage(context, &mGlobalStorage)); + + mCameraPackage = initCameraPackage(localContext); + mUserInterfacePackage = initUserInterfacePackage(localContext); + mInputPackage = initInputPackage(localContext); + mNearbyPackage = initNearbyPackage(localContext); + mLocalStoragePackage = initLocalStoragePackage(localContext, &mGlobalStorage); + mPlayerStoragePackage = initPlayerStoragePackage(localContext, &mGlobalStorage, &mPlayerStorage); + mPostprocessingPackage = initPostprocessingPackage(localContext); + mDebugPackage = initDebugPackage(localContext); + + initConfiguration(); + mInitialized = true; + } + + std::string LuaManager::translate(const std::string& contextName, const std::string& key) + { + return mL10n.translate(contextName, key); + } + + void LuaManager::loadPermanentStorage(const std::string& userConfigPath) + { + auto globalPath = std::filesystem::path(userConfigPath) / "global_storage.bin"; + auto playerPath = std::filesystem::path(userConfigPath) / "player_storage.bin"; + if (std::filesystem::exists(globalPath)) + mGlobalStorage.load(globalPath.string()); + if (std::filesystem::exists(playerPath)) + mPlayerStorage.load(playerPath.string()); + } + + void LuaManager::savePermanentStorage(const std::string& userConfigPath) + { + std::filesystem::path confDir(userConfigPath); + mGlobalStorage.save((confDir / "global_storage.bin").string()); + mPlayerStorage.save((confDir / "player_storage.bin").string()); + } + + void LuaManager::update() + { + static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); + if (mPlayer.isEmpty()) + return; // The game is not started yet. + + float frameDuration = MWBase::Environment::get().getFrameDuration(); + ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); + + MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (!(getId(mPlayer) == getId(newPlayerPtr))) + throw std::logic_error("Player Refnum was changed unexpectedly"); + if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) + { + mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry + objectRegistry->registerPtr(mPlayer); + } + + mWorldView.update(); + + std::vector globalEvents = std::move(mGlobalEvents); + std::vector localEvents = std::move(mLocalEvents); + mGlobalEvents = std::vector(); + mLocalEvents = std::vector(); + + if (!mWorldView.isPaused()) + { // Update time and process timers + double simulationTime = mWorldView.getSimulationTime() + frameDuration; + mWorldView.setSimulationTime(simulationTime); + double gameTime = mWorldView.getGameTime(); + + mGlobalScripts.processTimers(simulationTime, gameTime); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->processTimers(simulationTime, gameTime); + } + + // Receive events + for (GlobalEvent& e : globalEvents) + mGlobalScripts.receiveEvent(e.mEventName, e.mEventData); + for (LocalEvent& e : localEvents) + { + LObject obj(e.mDest, objectRegistry); + LocalScripts* scripts = obj.isValid() ? obj.ptr().getRefData().getLuaScripts() : nullptr; + if (scripts) + scripts->receiveEvent(e.mEventName, e.mEventData); + else + Log(Debug::Debug) << "Ignored event " << e.mEventName << " to L" << idToString(e.mDest) + << ". Object not found or has no attached scripts"; + } + + // Run queued callbacks + for (CallbackWithData& c : mQueuedCallbacks) + c.mCallback.call(c.mArg); + mQueuedCallbacks.clear(); + + // Engine handlers in local scripts + for (const LocalEngineEvent& e : mLocalEngineEvents) + { + LObject obj(e.mDest, objectRegistry); + if (!obj.isValid()) + { + if (luaDebug) + Log(Debug::Verbose) << "Can not call engine handlers: object" << idToString(e.mDest) << " is not found"; + continue; + } + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->receiveEngineEvent(e.mEvent); + } + mLocalEngineEvents.clear(); + + if (!mWorldView.isPaused()) + { + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(frameDuration); + } + + // Engine handlers in global scripts + if (mPlayerChanged) + { + mPlayerChanged = false; + mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); + } + if (mNewGameStarted) + { + mNewGameStarted = false; + mGlobalScripts.newGameStarted(); + } + + for (ObjectId id : mObjectAddedEvents) + { + GObject obj(id, objectRegistry); + if (obj.isValid()) + { + mGlobalScripts.objectActive(obj); + const MWWorld::Class& objClass = obj.ptr().getClass(); + if (objClass.isActor()) + mGlobalScripts.actorActive(obj); + if (mWorldView.isItem(obj.ptr())) + mGlobalScripts.itemActive(obj); + } + else if (luaDebug) + Log(Debug::Verbose) << "Could not resolve a Lua object added event: object" << idToString(id) + << " is already removed"; + } + mObjectAddedEvents.clear(); + + if (!mWorldView.isPaused()) + mGlobalScripts.update(frameDuration); + } + + void LuaManager::synchronizedUpdate() + { + if (mPlayer.isEmpty()) + return; // The game is not started yet. + + // 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()); + if (playerScripts && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu)) + { + for (const auto& event : mInputEvents) + playerScripts->processInputEvent(event); + } + mInputEvents.clear(); + if (playerScripts) + playerScripts->onFrame(mWorldView.isPaused() ? 0.0 : MWBase::Environment::get().getFrameDuration()); + mProcessingInputEvents = false; + + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + for (const std::string& message : mUIMessages) + windowManager->messageBox(message); + mUIMessages.clear(); + for (auto& [msg, color] : mInGameConsoleMessages) + windowManager->printToConsole(msg, "#" + color.toHex()); + mInGameConsoleMessages.clear(); + + for (std::unique_ptr& action : mActionQueue) + action->safeApply(mWorldView); + mActionQueue.clear(); + + if (mTeleportPlayerAction) + mTeleportPlayerAction->safeApply(mWorldView); + mTeleportPlayerAction.reset(); + } + + void LuaManager::clear() + { + LuaUi::clearUserInterface(); + mUiResourceManager.clear(); + MWBase::Environment::get().getWindowManager()->setConsoleMode(""); + MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders(); + mActiveLocalScripts.clear(); + mLocalEvents.clear(); + mGlobalEvents.clear(); + mInputEvents.clear(); + mObjectAddedEvents.clear(); + mLocalEngineEvents.clear(); + mNewGameStarted = false; + mPlayerChanged = false; + mWorldView.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; + if (!mPlayer.isEmpty()) + { + mPlayer.getCellRef().unsetRefNum(); + mPlayer.getRefData().setLuaScripts(nullptr); + mPlayer = MWWorld::Ptr(); + } + mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + } + + void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) + { + if (!mInitialized) + return; + if (!mPlayer.isEmpty()) + throw std::logic_error("Player is initialized twice"); + mWorldView.objectAddedToScene(ptr); + mPlayer = ptr; + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + localScripts = createLocalScripts(ptr); + localScripts->addAutoStartedScripts(); + } + mActiveLocalScripts.insert(localScripts); + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); + mPlayerChanged = true; + } + + void LuaManager::newGameStarted() + { + mNewGameStarted = true; + mInputEvents.clear(); + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + + void LuaManager::gameLoaded() + { + if (!mGlobalScriptsStarted) + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) + { + mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. + + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + LuaUtil::ScriptIdsWithInitializationData autoStartConf = + mConfiguration.getLocalConf(getLiveCellRefType(ptr.mRef), ptr.getCellRef().getRefId(), getId(ptr)); + if (!autoStartConf.empty()) + { + localScripts = createLocalScripts(ptr, std::move(autoStartConf)); + localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()` + } + } + if (localScripts) + { + mActiveLocalScripts.insert(localScripts); + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); + } + + if (ptr != mPlayer) + mObjectAddedEvents.push_back(getId(ptr)); + } + + void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + mWorldView.objectRemovedFromScene(ptr); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + { + mActiveLocalScripts.erase(localScripts); + if (!mWorldView.getObjectRegistry()->getPtr(getId(ptr), true).isEmpty()) + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnInactive{}}); + } + } + + void LuaManager::registerObject(const MWWorld::Ptr& ptr) + { + mWorldView.getObjectRegistry()->registerPtr(ptr); + } + + void LuaManager::deregisterObject(const MWWorld::Ptr& ptr) + { + mWorldView.getObjectRegistry()->deregisterPtr(ptr); + } + + void LuaManager::itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) + { + mWorldView.getObjectRegistry()->registerPtr(consumable); + mLocalEngineEvents.push_back({getId(actor), LocalScripts::OnConsume{LObject(getId(consumable), mWorldView.getObjectRegistry())}}); + } + + void LuaManager::objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) + { + mLocalEngineEvents.push_back({getId(object), LocalScripts::OnActivated{LObject(getId(actor), mWorldView.getObjectRegistry())}}); + } + + MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + return nullptr; + return localScripts->getActorControls(); + } + + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + localScripts = createLocalScripts(ptr); + localScripts->addAutoStartedScripts(); + if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) + mActiveLocalScripts.insert(localScripts); + } + localScripts->addCustomScript(scriptId); + } + + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, + std::optional autoStartConf) + { + assert(mInitialized); + std::shared_ptr scripts; + const uint32_t type = getLiveCellRefType(ptr.mRef); + if (type == ESM::REC_STAT) + throw std::runtime_error("Lua scripts on static objects are not allowed"); + else if (type == ESM::REC_INTERNAL_PLAYER) + { + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts->setAutoStartConf(mConfiguration.getPlayerConf()); + scripts->addPackage("openmw.ui", mUserInterfacePackage); + scripts->addPackage("openmw.camera", mCameraPackage); + scripts->addPackage("openmw.input", mInputPackage); + scripts->addPackage("openmw.storage", mPlayerStoragePackage); + scripts->addPackage("openmw.postprocessing", mPostprocessingPackage); + scripts->addPackage("openmw.debug", mDebugPackage); + } + else + { + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + if (!autoStartConf.has_value()) + autoStartConf = mConfiguration.getLocalConf(type, ptr.getCellRef().getRefId(), getId(ptr)); + scripts->setAutoStartConf(std::move(*autoStartConf)); + scripts->addPackage("openmw.storage", mLocalStoragePackage); + } + scripts->addPackage("openmw.nearby", mNearbyPackage); + scripts->setSerializer(mLocalSerializer.get()); + + MWWorld::RefData& refData = ptr.getRefData(); + refData.setLuaScripts(std::move(scripts)); + return refData.getLuaScripts(); + } + + void LuaManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) + { + writer.startRecord(ESM::REC_LUAM); + + mWorldView.save(writer); + ESM::LuaScripts globalScripts; + mGlobalScripts.save(globalScripts); + globalScripts.save(writer); + saveEvents(writer, mGlobalEvents, mLocalEvents); + + writer.endRecord(ESM::REC_LUAM); + } + + void LuaManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if (type != ESM::REC_LUAM) + throw std::runtime_error("ESM::REC_LUAM is expected"); + + mWorldView.load(reader); + ESM::LuaScripts globalScripts; + globalScripts.load(reader); + loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); + + mGlobalScripts.setSavedDataDeserializer(mGlobalLoader.get()); + mGlobalScripts.load(globalScripts); + mGlobalScriptsStarted = true; + } + + void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().getLuaScripts()->save(data); + else + data.mScripts.clear(); + } + + void LuaManager::loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) + { + if (data.mScripts.empty()) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().setLuaScripts(nullptr); + return; + } + + mWorldView.getObjectRegistry()->registerPtr(ptr); + LocalScripts* scripts = createLocalScripts(ptr); + + 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. + mWorldView.getObjectRegistry()->deregisterPtr(ptr); + } + + void LuaManager::reloadAllScripts() + { + Log(Debug::Info) << "Reload Lua"; + + LuaUi::clearUserInterface(); + MWBase::Environment::get().getWindowManager()->setConsoleMode(""); + mUiResourceManager.clear(); + mLua.dropScriptCache(); + mL10n.clear(); + initConfiguration(); + + { // Reload global scripts + mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get()); + ESM::LuaScripts data; + mGlobalScripts.save(data); + mGlobalScripts.load(data); + } + + for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) + { // 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); + } + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->receiveEngineEvent(LocalScripts::OnActive()); + } + + void LuaManager::handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) + { + PlayerScripts* playerScripts = nullptr; + if (!mPlayer.isEmpty()) + playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + 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()) + selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr), mWorldView.getObjectRegistry())); + if (!playerScripts->consoleCommand(consoleMode, command, selected)) + MWBase::Environment::get().getWindowManager()->printToConsole("No Lua handlers for console\n", + MWBase::WindowManager::sConsoleColor_Error); + } + + LuaManager::Action::Action(LuaUtil::LuaState* state) + { + static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); + if (luaDebug) + mCallerTraceback = state->debugTraceback(); + } + + void LuaManager::Action::safeApply(WorldView& w) const + { + try + { + apply(w); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in " << this->toString() << ": " << e.what(); + + if (mCallerTraceback.empty()) + Log(Debug::Error) << "Set 'lua_debug=true' in settings.cfg to enable action tracebacks"; + else + Log(Debug::Error) << "Caller " << mCallerTraceback; + } + } + + namespace + { + class FunctionAction final : public LuaManager::Action + { + public: + FunctionAction(LuaUtil::LuaState* state, std::function fn, std::string_view name) + : Action(state), mFn(std::move(fn)), mName(name) {} + + void apply(WorldView&) const override { mFn(); } + std::string toString() const override { return "FunctionAction " + mName; } + + private: + std::function mFn; + std::string mName; + }; + } + + void LuaManager::addAction(std::function action, std::string_view name) + { + mActionQueue.push_back(std::make_unique(&mLua, std::move(action), name)); + } + +} diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp new file mode 100644 index 0000000000..3f3d8566a7 --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -0,0 +1,197 @@ +#ifndef MWLUA_LUAMANAGERIMP_H +#define MWLUA_LUAMANAGERIMP_H + +#include +#include + +#include +#include +#include + +#include + +#include + +#include "../mwbase/luamanager.hpp" + +#include "object.hpp" +#include "eventqueue.hpp" +#include "globalscripts.hpp" +#include "localscripts.hpp" +#include "playerscripts.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + + class LuaManager : public MWBase::LuaManager + { + public: + LuaManager(const VFS::Manager* vfs, const std::string& libsDir); + + // Called by engine.cpp before UI setup. + void initL10n(); + + // Called by engine.cpp when the environment is fully initialized. + void init(); + + void loadPermanentStorage(const std::string& userConfigPath); + void savePermanentStorage(const std::string& userConfigPath); + + // Called by engine.cpp every frame. For performance reasons it works in a separate + // thread (in parallel with osg Cull). Can not use scene graph. + void update(); + + std::string translate(const std::string& contextName, const std::string& key) override; + + // Called by engine.cpp from the main thread. Can use scene graph. + void synchronizedUpdate(); + + // Available everywhere through the MWBase::LuaManager interface. + // LuaManager queues these events and propagates to scripts on the next `update` call. + void newGameStarted() override; + void gameLoaded() override; + void objectAddedToScene(const MWWorld::Ptr& ptr) override; + void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; + void registerObject(const MWWorld::Ptr& ptr) override; + void deregisterObject(const MWWorld::Ptr& ptr) override; + void inputEvent(const InputEvent& event) override { mInputEvents.push_back(event); } + void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) override; + void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; + + MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; + + void clear() override; // should be called before loading game or starting a new game to reset internal state. + void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". + + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); + void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } + void addInGameConsoleMessage(const std::string& msg, const Misc::Color& color) + { + mInGameConsoleMessages.push_back({msg, color}); + } + + // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with OSG Cull), + // so we need to queue it and apply from the main thread. All such changes should be implemented as classes inherited + // from MWLua::Action. + class Action + { + public: + Action(LuaUtil::LuaState* state); + virtual ~Action() {} + + void safeApply(WorldView&) const; + virtual void apply(WorldView&) const = 0; + virtual std::string toString() const = 0; + + private: + std::string mCallerTraceback; + }; + + void addAction(std::function action, std::string_view name = ""); + void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } + void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } + + // Saving + void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; + void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) override; + + // Loading from a save + void readRecord(ESM::ESMReader& reader, uint32_t type) override; + 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; + + void handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) override; + + // Used to call Lua callbacks from C++ + void queueCallback(LuaUtil::Callback callback, sol::object arg) + { + mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); + } + + // Wraps Lua callback into an std::function. + // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or + // any other Lua-related function is running. + template + std::function wrapLuaCallback(const LuaUtil::Callback& c) + { + return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; + } + + LuaUi::ResourceManager* uiResourceManager() { return &mUiResourceManager; } + + bool isProcessingInputEvents() const { return mProcessingInputEvents; } + + private: + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, + std::optional autoStartConf = std::nullopt); + + bool mInitialized = false; + bool mGlobalScriptsStarted = false; + bool mProcessingInputEvents = false; + LuaUtil::ScriptsConfiguration mConfiguration; + LuaUtil::LuaState mLua; + LuaUi::ResourceManager mUiResourceManager; + LuaUtil::L10nManager mL10n; + sol::table mNearbyPackage; + sol::table mUserInterfacePackage; + sol::table mCameraPackage; + sol::table mInputPackage; + sol::table mLocalStoragePackage; + sol::table mPlayerStoragePackage; + sol::table mPostprocessingPackage; + sol::table mDebugPackage; + + GlobalScripts mGlobalScripts{&mLua}; + std::set mActiveLocalScripts; + WorldView mWorldView; + + bool mPlayerChanged = false; + bool mNewGameStarted = false; + MWWorld::Ptr mPlayer; + + GlobalEventQueue mGlobalEvents; + LocalEventQueue mLocalEvents; + + std::unique_ptr mGlobalSerializer; + std::unique_ptr mLocalSerializer; + + std::map mContentFileMapping; + std::unique_ptr mGlobalLoader; + std::unique_ptr mLocalLoader; + + std::vector mInputEvents; + std::vector mObjectAddedEvents; + + struct CallbackWithData + { + LuaUtil::Callback mCallback; + sol::object mArg; + }; + std::vector mQueuedCallbacks; + + struct LocalEngineEvent + { + ObjectId mDest; + LocalScripts::EngineEvent mEvent; + }; + std::vector mLocalEngineEvents; + + // Queued actions that should be done in main thread. Processed by applyQueuedChanges(). + std::vector> mActionQueue; + std::unique_ptr mTeleportPlayerAction; + std::vector mUIMessages; + std::vector> mInGameConsoleMessages; + + LuaUtil::LuaStorage mGlobalStorage{mLua.sol()}; + LuaUtil::LuaStorage mPlayerStorage{mLua.sol()}; + }; + +} + +#endif // MWLUA_LUAMANAGERIMP_H diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp new file mode 100644 index 0000000000..3ad9baac61 --- /dev/null +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -0,0 +1,127 @@ +#include "luabindings.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwphysics/raycasting.hpp" + +#include "luamanagerimp.hpp" +#include "worldview.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + sol::table initNearbyPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + + sol::usertype rayResult = + context.mLua->sol().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 + { + if (r.mHit) + return r.mHitPos; + else + return sol::nullopt; + }); + rayResult["hitNormal"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHit) + return r.mHitNormal; + else + return sol::nullopt; + }); + rayResult["hitObject"] = sol::readonly_property([worldView](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHitObject.isEmpty()) + return sol::nullopt; + else + return LObject(getId(r.mHitObject), worldView->getObjectRegistry()); + }); + + 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}, + })); + + api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + MWWorld::Ptr ignore; + int collisionType = MWPhysics::CollisionType_Default; + float radius = 0; + if (options) + { + sol::optional ignoreObj = options->get>("ignore"); + if (ignoreObj) ignore = ignoreObj->ptr(); + 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); + else + { + if (!ignore.isEmpty()) throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + return rayCasting->castSphere(from, to, radius, collisionType); + } + }; + // TODO: async raycasting + /*api["asyncCastRay"] = [luaManager = context.mLuaManager]( + const Callback& luaCallback, const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + std::function callback = + luaManager->wrapLuaCallback(luaCallback); + MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + + // Handle options the same way as in `castRay`. + + // NOTE: `callback` is not thread safe. If MWPhysics works in separate thread, it must put results to a queue + // 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) + { + if (!manager->isProcessingInputEvents()) + { + throw std::logic_error("castRenderingRay can be used only in player scripts during processing of input events; " + "use asyncCastRenderingRay instead."); + } + MWPhysics::RayCastingResult res; + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false); + return res; + }; + api["asyncCastRenderingRay"] = + [manager=context.mLuaManager](const LuaUtil::Callback& callback, const osg::Vec3f& from, const osg::Vec3f& to) + { + manager->addAction([manager, callback, from, to] + { + MWPhysics::RayCastingResult res; + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false); + manager->queueCallback(callback, sol::make_object(callback.mFunc.lua_state(), res)); + }); + }; + + api["activators"] = LObjectList{worldView->getActivatorsInScene()}; + api["actors"] = LObjectList{worldView->getActorsInScene()}; + api["containers"] = LObjectList{worldView->getContainersInScene()}; + api["doors"] = LObjectList{worldView->getDoorsInScene()}; + api["items"] = LObjectList{worldView->getItemsInScene()}; + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp new file mode 100644 index 0000000000..9fa1e22072 --- /dev/null +++ b/apps/openmw/mwlua/object.cpp @@ -0,0 +1,108 @@ +#include "object.hpp" + +#include "types/types.hpp" + +#include + +namespace MWLua +{ + + std::string idToString(const ObjectId& id) + { + return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); + } + + bool isMarker(const MWWorld::Ptr& ptr) + { + return Misc::ResourceHelpers::isHiddenMarker(ptr.getCellRef().getRefId()); + } + + std::string ptrToString(const MWWorld::Ptr& ptr) + { + std::string res = "object"; + res.append(idToString(getId(ptr))); + res.append(" ("); + res.append(getLuaObjectTypeName(ptr)); + res.append(", "); + res.append(ptr.getCellRef().getRefId()); + res.append(")"); + return res; + } + + std::string Object::toString() const + { + if (isValid()) + return ptrToString(ptr()); + else + return "object" + idToString(mId) + " (not found)"; + } + + bool Object::isValid() const + { + if (mLastUpdate < mObjectRegistry->mUpdateCounter) + { + updatePtr(); + mLastUpdate = mObjectRegistry->mUpdateCounter; + } + return !mPtr.isEmpty(); + } + + const MWWorld::Ptr& Object::ptr() const + { + if (!isValid()) + throw std::runtime_error("Object is not available: " + idToString(mId)); + return mPtr; + } + + void ObjectRegistry::update() + { + if (mChanged) + { + mUpdateCounter++; + mChanged = false; + } + } + + void ObjectRegistry::clear() + { + mObjectMapping.clear(); + mChanged = false; + mUpdateCounter = 0; + mLastAssignedId.unset(); + } + + MWWorld::Ptr ObjectRegistry::getPtr(ObjectId id, bool local) + { + MWWorld::Ptr ptr; + auto it = mObjectMapping.find(id); + if (it != mObjectMapping.end()) + ptr = it->second; + if (local) + { + // TODO: Return ptr only if it is active or was active in the previous frame, otherwise return empty. + // Needed because in multiplayer inactive objects will not be synchronized, so an be out of date. + } + else + { + // TODO: If Ptr is empty then try to load the object from esp/esm3. + } + return ptr; + } + + ObjectId ObjectRegistry::registerPtr(const MWWorld::Ptr& ptr) + { + ObjectId id = ptr.getCellRef().getOrAssignRefNum(mLastAssignedId); + mChanged = true; + mObjectMapping[id] = ptr; + return id; + } + + ObjectId ObjectRegistry::deregisterPtr(const MWWorld::Ptr& ptr) + { + ObjectId id = getId(ptr); + mChanged = true; + mObjectMapping.erase(id); + return id; + } + +} diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp new file mode 100644 index 0000000000..4c4fbd7908 --- /dev/null +++ b/apps/openmw/mwlua/object.hpp @@ -0,0 +1,122 @@ +#ifndef MWLUA_OBJECT_H +#define MWLUA_OBJECT_H + +#include +#include + +#include + +#include + +#include "../mwworld/ptr.hpp" + +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) { return ptr.getCellRef().getRefNum(); } + std::string idToString(const ObjectId& id); + std::string ptrToString(const MWWorld::Ptr& ptr); + bool isMarker(const MWWorld::Ptr& ptr); + + // Holds a mapping ObjectId -> MWWord::Ptr. + class ObjectRegistry + { + public: + ObjectRegistry() { mLastAssignedId.unset(); } + + void update(); // Should be called every frame. + void clear(); // Should be called before starting or loading a new game. + + ObjectId registerPtr(const MWWorld::Ptr& ptr); + ObjectId deregisterPtr(const MWWorld::Ptr& ptr); + + // Returns Ptr by id. If object is not found, returns empty Ptr. + // If local = true, returns non-empty ptr only if it can be used in local scripts + // (i.e. is active or was active in the previous frame). + MWWorld::Ptr getPtr(ObjectId id, bool local); + + // Needed only for saving/loading. + const ObjectId& getLastAssignedId() const { return mLastAssignedId; } + void setLastAssignedId(ObjectId id) { mLastAssignedId = id; } + + private: + friend class Object; + friend class LuaManager; + + bool mChanged = false; + int64_t mUpdateCounter = 0; + std::map mObjectMapping; + ObjectId mLastAssignedId; + }; + + // Lua scripts can't use MWWorld::Ptr directly, because lifetime of a script can be longer than lifetime of Ptr. + // `GObject` and `LObject` are intended to be passed to Lua as a userdata. + // It automatically updates the underlying Ptr when needed. + class Object + { + public: + Object(ObjectId id, ObjectRegistry* reg) : mId(id), mObjectRegistry(reg) {} + virtual ~Object() {} + ObjectId id() const { return mId; } + + std::string toString() const; + + // Updates and returns the underlying Ptr. Throws an exception if object is not available. + const MWWorld::Ptr& ptr() const; + + // Returns `true` if calling `ptr()` is safe. + bool isValid() const; + + virtual sol::object getObject(lua_State* lua, ObjectId id) const = 0; // returns LObject or GOBject + virtual sol::object getCell(lua_State* lua, MWWorld::CellStore* store) const = 0; // returns LCell or GCell + + protected: + virtual void updatePtr() const = 0; + + const ObjectId mId; + ObjectRegistry* mObjectRegistry; + + mutable MWWorld::Ptr mPtr; + mutable int64_t mLastUpdate = -1; + }; + + // Used only in local scripts + struct LCell + { + MWWorld::CellStore* mStore; + }; + class LObject : public Object + { + using Object::Object; + void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, true); } + sol::object getObject(lua_State* lua, ObjectId id) const final { return sol::make_object(lua, id, mObjectRegistry); } + sol::object getCell(lua_State* lua, MWWorld::CellStore* store) const final { return sol::make_object(lua, LCell{store}); } + }; + + // Used only in global scripts + struct GCell + { + MWWorld::CellStore* mStore; + }; + class GObject : public Object + { + using Object::Object; + void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, false); } + sol::object getObject(lua_State* lua, ObjectId id) const final { return sol::make_object(lua, id, mObjectRegistry); } + sol::object getCell(lua_State* lua, MWWorld::CellStore* store) const final { return sol::make_object(lua, GCell{store}); } + }; + + using ObjectIdList = std::shared_ptr>; + template + struct ObjectList { ObjectIdList mIds; }; + using GObjectList = ObjectList; + using LObjectList = ObjectList; + + template + struct Inventory { Obj mObj; }; + +} + +#endif // MWLUA_OBJECT_H diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp new file mode 100644 index 0000000000..14da8b098c --- /dev/null +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -0,0 +1,344 @@ +#include "luabindings.hpp" + +#include + +#include "../mwworld/action.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +#include "../mwmechanics/creaturestats.hpp" + +#include "eventqueue.hpp" +#include "luamanagerimp.hpp" +#include "types/types.hpp" + +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 {}; + template <> + struct is_automagical> : std::false_type {}; + template <> + struct is_automagical> : std::false_type {}; +} + +namespace MWLua +{ + + namespace { + + class TeleportAction final : public LuaManager::Action + { + public: + TeleportAction(LuaUtil::LuaState* state, ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot) + : Action(state), mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {} + + void apply(WorldView& worldView) const override + { + MWWorld::CellStore* cell = worldView.findCell(mCell, mPos); + if (!cell) + throw std::runtime_error(std::string("cell not found: '") + mCell + "'"); + + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false); + const MWWorld::Class& cls = obj.getClass(); + bool isPlayer = obj == world->getPlayerPtr(); + if (cls.isActor()) + cls.getCreatureStats(obj).land(isPlayer); + if (isPlayer) + { + ESM::Position esmPos; + static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); + std::memcpy(esmPos.pos, &mPos, sizeof(osg::Vec3f)); + std::memcpy(esmPos.rot, &mRot, sizeof(osg::Vec3f)); + world->getPlayer().setTeleported(true); + if (cell->isExterior()) + world->changeToExteriorCell(esmPos, true); + else + world->changeToInteriorCell(mCell, esmPos, true); + } + else + { + MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos); + world->rotateObject(newObj, mRot); + } + } + + std::string toString() const override { return "TeleportAction"; } + + private: + ObjectId mObject; + std::string mCell; + osg::Vec3f mPos; + osg::Vec3f mRot; + }; + + class ActivateAction final : public LuaManager::Action + { + public: + ActivateAction(LuaUtil::LuaState* state, ObjectId object, ObjectId actor) + : Action(state), mObject(object), mActor(actor) {} + + void apply(WorldView& worldView) const override + { + MWWorld::Ptr object = worldView.getObjectRegistry()->getPtr(mObject, true); + if (object.isEmpty()) + throw std::runtime_error(std::string("Object not found: " + idToString(mObject))); + MWWorld::Ptr actor = worldView.getObjectRegistry()->getPtr(mActor, true); + if (actor.isEmpty()) + throw std::runtime_error(std::string("Actor not found: " + idToString(mActor))); + + if (object.getRefData().activate()) + { + MWBase::Environment::get().getLuaManager()->objectActivated(object, actor); + std::unique_ptr action = object.getClass().activate(object, actor); + action->execute(actor); + } + } + + std::string toString() const override + { + return std::string("ActivateAction object=") + idToString(mObject) + + std::string(" actor=") + idToString(mActor); + } + + private: + ObjectId mObject; + ObjectId mActor; + }; + + template + using Cell = std::conditional_t, LCell, GCell>; + + template + void registerObjectList(const std::string& prefix, const Context& context) + { + using ListT = ObjectList; + sol::state& lua = context.mLua->sol(); + ObjectRegistry* registry = context.mWorldView->getObjectRegistry(); + 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] = [registry](const ListT& list, size_t index) + { + if (index > 0 && index <= list.mIds->size()) + return ObjectT((*list.mIds)[index - 1], registry); + else + throw std::runtime_error("Index out of range"); + }; + listT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + } + + template + void addBasicBindings(sol::usertype& objectT, const Context& context) + { + objectT["isValid"] = [](const ObjectT& o) { return o.isValid(); }; + objectT["recordId"] = sol::readonly_property([](const ObjectT& o) -> std::string + { + return o.ptr().getCellRef().getRefId(); + }); + objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> + { + const MWWorld::Ptr& ptr = o.ptr(); + if (ptr.isInCell()) + return Cell{ptr.getCell()}; + else + return sol::nullopt; + }); + objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f + { + return o.ptr().getRefData().getPosition().asVec3(); + }); + objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f + { + return o.ptr().getRefData().getPosition().asRotationVec3(); + }); + + objectT["type"] = sol::readonly_property([types=getTypeToPackageTable(context.mLua->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[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.mLocalEventQueue->push_back({dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); + }; + + objectT["activateBy"] = [context](const ObjectT& o, const ObjectT& actor) + { + uint32_t esmRecordType = actor.ptr().getType(); + if (esmRecordType != ESM::REC_CREA && esmRecordType != ESM::REC_NPC_) + throw std::runtime_error("The argument of `activateBy` must be an actor who activates the object. Got: " + + ptrToString(actor.ptr())); + context.mLuaManager->addAction(std::make_unique(context.mLua, o.id(), actor.id())); + }; + + if constexpr (std::is_same_v) + { // Only for global scripts + objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + if (object.ptr().getType() == ESM::REC_STAT) + throw std::runtime_error("Attaching scripts to Static is not allowed: " + std::string(path)); + luaManager->addCustomLocalScript(object.ptr(), *scriptId); + }; + objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); + if (localScripts->getAutoStartConf().count(*scriptId) > 0) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); + }; + + objectT["teleport"] = [context](const GObject& object, std::string_view cell, + const osg::Vec3f& pos, const sol::optional& optRot) + { + MWWorld::Ptr ptr = object.ptr(); + osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3(); + auto action = std::make_unique(context.mLua, object.id(), std::string(cell), pos, rot); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + context.mLuaManager->addTeleportPlayerAction(std::move(action)); + else + context.mLuaManager->addAction(std::move(action)); + }; + } + } + + template + 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"); + + inventoryT[sol::meta_function::to_string] = + [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; }; + + inventoryT["getAll"] = [worldView=context.mWorldView, ids=getPackageToTypeTable(context.mLua->sol())]( + const InventoryT& inventory, sol::optional type) + { + int mask = -1; + sol::optional typeId = sol::nullopt; + if (type.has_value()) + typeId = ids[*type]; + else + mask = MWWorld::ContainerStore::Type_All; + + if (typeId.has_value()) + { + switch (*typeId) + { + case ESM::REC_ALCH: mask = MWWorld::ContainerStore::Type_Potion; break; + case ESM::REC_ARMO: mask = MWWorld::ContainerStore::Type_Armor; break; + case ESM::REC_BOOK: mask = MWWorld::ContainerStore::Type_Book; break; + case ESM::REC_CLOT: mask = MWWorld::ContainerStore::Type_Clothing; break; + case ESM::REC_INGR: mask = MWWorld::ContainerStore::Type_Ingredient; break; + case ESM::REC_LIGH: mask = MWWorld::ContainerStore::Type_Light; break; + case ESM::REC_MISC: mask = MWWorld::ContainerStore::Type_Miscellaneous; break; + case ESM::REC_WEAP: mask = MWWorld::ContainerStore::Type_Weapon; break; + case ESM::REC_APPA: mask = MWWorld::ContainerStore::Type_Apparatus; break; + case ESM::REC_LOCK: mask = MWWorld::ContainerStore::Type_Lockpick; break; + case ESM::REC_PROB: mask = MWWorld::ContainerStore::Type_Probe; break; + case ESM::REC_REPA: mask = MWWorld::ContainerStore::Type_Repair; break; + default:; + } + } + + if (mask == -1) + throw std::runtime_error(std::string("Incorrect type argument in inventory:getAll: " + LuaUtil::toString(*type))); + + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + ObjectIdList list = std::make_shared>(); + auto it = store.begin(mask); + while (it.getType() != -1) + { + const MWWorld::Ptr& item = *(it++); + worldView->getObjectRegistry()->registerPtr(item); + list->push_back(getId(item)); + } + return ObjectList{list}; + }; + + inventoryT["countOf"] = [](const InventoryT& inventory, const std::string& recordId) + { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + return store.count(recordId); + }; + + if constexpr (std::is_same_v) + { // Only for global scripts + // TODO + // obj.inventory:drop(obj2, [count]) + // obj.inventory:drop(recordId, [count]) + // obj.inventory:addNew(recordId, [count]) + // obj.inventory:remove(obj/recordId, [count]) + /*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {}; + inventoryT["drop"] = [](const InventoryT& inventory) {}; + inventoryT["addNew"] = [](const InventoryT& inventory) {}; + inventoryT["remove"] = [](const InventoryT& inventory) {};*/ + } + } + + template + void initObjectBindings(const std::string& prefix, const Context& context) + { + sol::usertype objectT = context.mLua->sol().new_usertype( + prefix + "Object", sol::base_classes, sol::bases()); + addBasicBindings(objectT, context); + addInventoryBindings(objectT, prefix, context); + + registerObjectList(prefix, context); + } + } // namespace + + void initObjectBindingsForLocalScripts(const Context& context) + { + initObjectBindings("L", context); + } + + void initObjectBindingsForGlobalScripts(const Context& context) + { + initObjectBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp new file mode 100644 index 0000000000..b8173f6b7b --- /dev/null +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -0,0 +1,83 @@ +#ifndef MWLUA_PLAYERSCRIPTS_H +#define MWLUA_PLAYERSCRIPTS_H + +#include + +#include + +#include "../mwbase/luamanager.hpp" + +#include "localscripts.hpp" + +namespace MWLua +{ + + class PlayerScripts : public LocalScripts + { + public: + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + { + registerEngineHandlers({ + &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, + &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, + &mActionHandlers, &mOnFrameHandlers, + &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved + }); + } + + 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; + } + } + + void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + + bool consoleCommand(const std::string& consoleMode, const std::string& command, const sol::object& selectedObject) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command, selectedObject); + return !mConsoleCommandHandlers.mList.empty(); + } + + private: + 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" }; + }; + +} + +#endif // MWLUA_PLAYERSCRIPTS_H diff --git a/apps/openmw/mwlua/postprocessingbindings.cpp b/apps/openmw/mwlua/postprocessingbindings.cpp new file mode 100644 index 0000000000..8ae9cd10a8 --- /dev/null +++ b/apps/openmw/mwlua/postprocessingbindings.cpp @@ -0,0 +1,178 @@ +#include "luabindings.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwrender/postprocessor.hpp" + +#include "luamanagerimp.hpp" + +namespace +{ + template + class SetUniformShaderAction final : public MWLua::LuaManager::Action + { + public: + SetUniformShaderAction(LuaUtil::LuaState* state, std::shared_ptr shader, const std::string& name, const T& value) + : MWLua::LuaManager::Action(state), mShader(std::move(shader)), mName(name), mValue(value) {} + + void apply(MWLua::WorldView&) const override + { + MWBase::Environment::get().getWorld()->getPostProcessor()->setUniform(mShader, mName, mValue); + } + + std::string toString() const override + { + return std::string("SetUniformShaderAction shader=") + (mShader ? mShader->getName() : "nil") + + std::string("uniform=") + (mShader ? mName : "nil"); + } + + private: + std::shared_ptr mShader; + std::string mName; + T mValue; + }; +} + +namespace MWLua +{ + struct Shader; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + struct Shader + { + std::shared_ptr mShader; + + Shader(std::shared_ptr shader) : mShader(std::move(shader)) {} + + std::string toString() const + { + if (!mShader) + return "Shader(nil)"; + + return Misc::StringUtils::format("Shader(%s, %s)", mShader->getName(), mShader->getFileName()); + } + + enum { Action_None, Action_Enable, Action_Disable } mQueuedAction = Action_None; + }; + + template + auto getSetter(const Context& context) + { + return [context](const Shader& shader, const std::string& name, const T& value) { + context.mLuaManager->addAction(std::make_unique>(context.mLua, shader.mShader, name, value)); + }; + } + + template + auto getArraySetter(const Context& context) + { + return [context](const Shader& shader, const std::string& name, const sol::table& table) { + auto targetSize = MWBase::Environment::get().getWorld()->getPostProcessor()->getUniformSize(shader.mShader, name); + + if (!targetSize.has_value()) + throw std::runtime_error(Misc::StringUtils::format("Failed setting uniform array '%s'", name)); + + if (*targetSize != table.size()) + throw std::runtime_error(Misc::StringUtils::format("Mismatching uniform array size, got %zu expected %zu", table.size(), *targetSize)); + + std::vector values; + values.reserve(*targetSize); + + for (size_t i = 0; i < *targetSize; ++i) + { + sol::object obj = table[i+1]; + if (!obj.is()) + throw std::runtime_error("Invalid type for uniform array"); + values.push_back(obj.as()); + } + + context.mLuaManager->addAction(std::make_unique>>(context.mLua, shader.mShader, name, values)); + }; + } + + sol::table initPostprocessingPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + + sol::usertype shader = context.mLua->sol().new_usertype("Shader"); + shader[sol::meta_function::to_string] = [](const Shader& shader) { return shader.toString(); }; + + shader["enable"] = [context](Shader& shader, sol::optional optPos) + { + std::optional pos = std::nullopt; + if (optPos) + pos = optPos.value(); + + if (shader.mShader && shader.mShader->isValid()) + shader.mQueuedAction = Shader::Action_Enable; + + context.mLuaManager->addAction( + [=, &shader] { + shader.mQueuedAction = Shader::Action_None; + + if (MWBase::Environment::get().getWorld()->getPostProcessor()->enableTechnique(shader.mShader, pos) == MWRender::PostProcessor::Status_Error) + throw std::runtime_error("Failed enabling shader '" + shader.mShader->getName() + "'"); + } + ); + }; + + shader["disable"] = [context](Shader& shader) + { + shader.mQueuedAction = Shader::Action_Disable; + + context.mLuaManager->addAction( + [&] { + shader.mQueuedAction = Shader::Action_None; + + if (MWBase::Environment::get().getWorld()->getPostProcessor()->disableTechnique(shader.mShader) == MWRender::PostProcessor::Status_Error) + throw std::runtime_error("Failed disabling shader '" + shader.mShader->getName() + "'"); + } + ); + }; + + shader["isEnabled"] = [](const Shader& shader) + { + if (shader.mQueuedAction == Shader::Action_Enable) + return true; + else if (shader.mQueuedAction == Shader::Action_Disable) + return false; + return MWBase::Environment::get().getWorld()->getPostProcessor()->isTechniqueEnabled(shader.mShader); + }; + + shader["setBool"] = getSetter(context); + shader["setFloat"] = getSetter(context); + shader["setInt"] = getSetter(context); + shader["setVector2"] = getSetter(context); + shader["setVector3"] = getSetter(context); + shader["setVector4"] = getSetter(context); + + shader["setFloatArray"] = getArraySetter(context); + shader["setIntArray"] = getArraySetter(context); + shader["setVector2Array"] = getArraySetter(context); + shader["setVector3Array"] = getArraySetter(context); + shader["setVector4Array"] = getArraySetter(context); + + api["load"] = [](const std::string& name) + { + Shader shader{MWBase::Environment::get().getWorld()->getPostProcessor()->loadTechnique(name, false)}; + + if (!shader.mShader || !shader.mShader->isValid()) + throw std::runtime_error(Misc::StringUtils::format("Failed loading shader '%s'", name)); + + if (!shader.mShader->getDynamic()) + throw std::runtime_error(Misc::StringUtils::format("Shader '%s' is not marked as dynamic", name)); + + return shader; + }; + + return LuaUtil::makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp new file mode 100644 index 0000000000..040d96e288 --- /dev/null +++ b/apps/openmw/mwlua/stats.cpp @@ -0,0 +1,403 @@ +#include "stats.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "localscripts.hpp" +#include "luamanagerimp.hpp" + +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +namespace +{ + template + auto addIndexedAccessor(int index) + { + return sol::overload( + [index](MWLua::LocalScripts::SelfObject& o) { return T::create(&o, index); }, + [index](const MWLua::LObject& o) { return T::create(o, index); }, + [index](const MWLua::GObject& o) { return T::create(o, index); } + ); + } + + template + void addProp(const MWLua::Context& context, sol::usertype& type, std::string_view prop, G getter) + { + type[prop] = sol::property( + [=](const T& stat) { return stat.get(context, prop, getter); }, + [=](const T& stat, const sol::object& value) { stat.cache(context, prop, value); }); + } + + using SelfObject = MWLua::LocalScripts::SelfObject; + using StatObject = std::variant; + + const MWLua::Object* getObject(const StatObject& obj) + { + return std::visit([] (auto&& variant) -> const MWLua::Object* + { + using T = std::decay_t; + if constexpr(std::is_same_v) + return variant; + else if constexpr(std::is_same_v) + return &variant; + else if constexpr(std::is_same_v) + return &variant; + }, obj); + } + + template + sol::object getValue(const MWLua::Context& context, const StatObject& obj, SelfObject::CachedStat::Setter setter, int index, std::string_view prop, G getter) + { + return std::visit([&] (auto&& variant) + { + using T = std::decay_t; + if constexpr(std::is_same_v) + { + auto it = variant->mStatsCache.find({ setter, index, prop }); + if(it != variant->mStatsCache.end()) + return it->second; + return sol::make_object(context.mLua->sol(), getter(variant)); + } + else if constexpr(std::is_same_v) + return sol::make_object(context.mLua->sol(), getter(&variant)); + else if constexpr(std::is_same_v) + return sol::make_object(context.mLua->sol(), getter(&variant)); + }, obj); + } +} + +namespace MWLua +{ + namespace + { + class StatUpdateAction final : public LuaManager::Action + { + ObjectId mId; + public: + StatUpdateAction(LuaUtil::LuaState* state, ObjectId id) : Action(state), mId(id) {} + + void apply(WorldView& worldView) const override + { + LObject obj(mId, worldView.getObjectRegistry()); + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->applyStatsCache(); + } + + std::string toString() const override { return "StatUpdateAction"; } + }; + } + + class LevelStat + { + StatObject mObject; + + LevelStat(StatObject object) : mObject(std::move(object)) {} + public: + sol::object getCurrent(const Context& context) const + { + return getValue(context, mObject, &LevelStat::setValue, 0, "current", [](const MWLua::Object* obj) + { + const auto& ptr = obj->ptr(); + return ptr.getClass().getCreatureStats(ptr).getLevel(); + }); + } + + void setCurrent(const Context& context, const sol::object& value) const + { + SelfObject* obj = std::get(mObject); + if(obj->mStatsCache.empty()) + context.mLuaManager->addAction(std::make_unique(context.mLua, obj->id())); + obj->mStatsCache[SelfObject::CachedStat{&LevelStat::setValue, 0, "current"}] = value; + } + + sol::object getProgress(const Context& context) const + { + const auto& ptr = getObject(mObject)->ptr(); + if(!ptr.getClass().isNpc()) + return sol::nil; + return sol::make_object(context.mLua->sol(), ptr.getClass().getNpcStats(ptr).getLevelProgress()); + } + + static std::optional create(StatObject object, int index) + { + if(!getObject(object)->ptr().getClass().isActor()) + return {}; + return LevelStat{std::move(object)}; + } + + static void setValue(int, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + if(prop == "current") + stats.setLevel(value.as()); + } + }; + + class DynamicStat + { + StatObject mObject; + int mIndex; + + DynamicStat(StatObject object, int 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, &DynamicStat::setValue, mIndex, prop, [this, getter](const MWLua::Object* obj) + { + const auto& ptr = obj->ptr(); + return (ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).*getter)(); + }); + } + + static std::optional create(StatObject object, int index) + { + if(!getObject(object)->ptr().getClass().isActor()) + return {}; + return DynamicStat{std::move(object), index}; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = std::get(mObject); + if(obj->mStatsCache.empty()) + context.mLuaManager->addAction(std::make_unique(context.mLua, obj->id())); + obj->mStatsCache[SelfObject::CachedStat{&DynamicStat::setValue, mIndex, prop}] = value; + } + + static void setValue(int index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getDynamic(index); + float floatValue = value.as(); + if(prop == "base") + stat.setBase(floatValue); + else if(prop == "current") + stat.setCurrent(floatValue, true, true); + else if(prop == "modifier") + stat.setModifier(floatValue); + stats.setDynamic(index, stat); + } + }; + + class AttributeStat + { + StatObject mObject; + int mIndex; + + AttributeStat(StatObject object, int 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, &AttributeStat::setValue, mIndex, prop, [this, getter](const MWLua::Object* obj) + { + const auto& ptr = obj->ptr(); + return (ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).*getter)(); + }); + } + + float getModified(const Context& context) const + { + auto base = get(context, "base", &MWMechanics::AttributeValue::getBase).as(); + auto damage = get(context, "damage", &MWMechanics::AttributeValue::getDamage).as(); + auto modifier = get(context, "modifier", &MWMechanics::AttributeValue::getModifier).as(); + return std::max(0.f, base - damage + modifier); // Should match AttributeValue::getModified + } + + static std::optional create(StatObject object, int index) + { + if(!getObject(object)->ptr().getClass().isActor()) + return {}; + return AttributeStat{std::move(object), index}; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = std::get(mObject); + if(obj->mStatsCache.empty()) + context.mLuaManager->addAction(std::make_unique(context.mLua, obj->id())); + obj->mStatsCache[SelfObject::CachedStat{&AttributeStat::setValue, mIndex, prop}] = value; + } + + static void setValue(int index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getAttribute(index); + float floatValue = value.as(); + if(prop == "base") + stat.setBase(floatValue); + else if(prop == "damage") + { + stat.restore(stat.getDamage()); + stat.damage(floatValue); + } + else if(prop == "modifier") + stat.setModifier(floatValue); + stats.setAttribute(index, stat); + } + }; + + class SkillStat + { + StatObject mObject; + int mIndex; + + SkillStat(StatObject object, int index) : mObject(std::move(object)), mIndex(index) {} + + static float getProgress(const MWWorld::Ptr& ptr, int index, const MWMechanics::SkillValue& stat) + { + float progress = stat.getProgress(); + if(progress != 0.f) + progress /= getMaxProgress(ptr, index, stat); + return progress; + } + + static float getMaxProgress(const MWWorld::Ptr& ptr, int index, const MWMechanics::SkillValue& stat) { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + const auto cl = store.get().find(ptr.get()->mBase->mClass); + return ptr.getClass().getNpcStats(ptr).getSkillProgressRequirement(index, *cl); + } + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue(context, mObject, &SkillStat::setValue, mIndex, prop, [this, getter](const MWLua::Object* obj) + { + const auto& ptr = obj->ptr(); + return (ptr.getClass().getNpcStats(ptr).getSkill(mIndex).*getter)(); + }); + } + + float getModified(const Context& context) const + { + auto base = get(context, "base", &MWMechanics::SkillValue::getBase).as(); + auto damage = get(context, "damage", &MWMechanics::SkillValue::getDamage).as(); + auto modifier = get(context, "modifier", &MWMechanics::SkillValue::getModifier).as(); + return std::max(0.f, base - damage + modifier); // Should match SkillValue::getModified + } + + sol::object getProgress(const Context& context) const + { + return getValue(context, mObject, &SkillStat::setValue, mIndex, "progress", [this](const MWLua::Object* obj) + { + const auto& ptr = obj->ptr(); + return getProgress(ptr, mIndex, ptr.getClass().getNpcStats(ptr).getSkill(mIndex)); + }); + } + + static std::optional create(StatObject object, int index) + { + if(!getObject(object)->ptr().getClass().isNpc()) + return {}; + return SkillStat{std::move(object), index}; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = std::get(mObject); + if(obj->mStatsCache.empty()) + context.mLuaManager->addAction(std::make_unique(context.mLua, obj->id())); + obj->mStatsCache[SelfObject::CachedStat{&SkillStat::setValue, mIndex, prop}] = value; + } + + static void setValue(int index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getNpcStats(ptr); + auto stat = stats.getSkill(index); + float floatValue = value.as(); + if(prop == "base") + stat.setBase(floatValue); + else if(prop == "damage") + { + stat.restore(stat.getDamage()); + stat.damage(floatValue); + } + else if(prop == "modifier") + stat.setModifier(floatValue); + else if(prop == "progress") + stat.setProgress(floatValue * getMaxProgress(ptr, index, stat)); + stats.setSkill(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 {}; + 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); + actor["stats"] = LuaUtil::makeReadOnly(stats); + + auto levelStatT = context.mLua->sol().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); }); + stats["level"] = addIndexedAccessor(0); + + auto dynamicStatT = context.mLua->sol().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); + 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"); + 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); }); + addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); + sol::table attributes(context.mLua->sol(), sol::create); + stats["attributes"] = LuaUtil::makeReadOnly(attributes); + for(int id = ESM::Attribute::Strength; id < ESM::Attribute::Length; ++id) + attributes[Misc::StringUtils::lowerCase(ESM::Attribute::sAttributeNames[id])] = addIndexedAccessor(id); + } + + void addNpcStatsBindings(sol::table& npc, const Context& context) + { + sol::table npcStats(context.mLua->sol(), sol::create); + sol::table baseMeta(context.mLua->sol(), 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"); + 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); }); + 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); + npcStats["skills"] = LuaUtil::makeReadOnly(skills); + for(int id = ESM::Skill::Block; id < ESM::Skill::Length; ++id) + skills[Misc::StringUtils::lowerCase(ESM::Skill::sSkillNames[id])] = addIndexedAccessor(id); + } +} diff --git a/apps/openmw/mwlua/stats.hpp b/apps/openmw/mwlua/stats.hpp new file mode 100644 index 0000000000..5108b710bf --- /dev/null +++ b/apps/openmw/mwlua/stats.hpp @@ -0,0 +1,12 @@ +#ifndef MWLUA_STATS_H +#define MWLUA_STATS_H + +#include "context.hpp" + +namespace MWLua +{ + void addActorStatsBindings(sol::table& actor, const Context& context); + void addNpcStatsBindings(sol::table& npc, const Context& context); +} + +#endif diff --git a/apps/openmw/mwlua/types/activator.cpp b/apps/openmw/mwlua/types/activator.cpp new file mode 100644 index 0000000000..6d5d86722d --- /dev/null +++ b/apps/openmw/mwlua/types/activator.cpp @@ -0,0 +1,37 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addActivatorBindings(sol::table activator, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + activator["record"] = sol::overload( + [](const Object& obj) -> const ESM::Activator* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Activator* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Activator"); + record[sol::meta_function::to_string] = [](const ESM::Activator& rec) { return "ESM3_Activator[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mId; }); + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mScript; }); + } +} diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp new file mode 100644 index 0000000000..464d85c76c --- /dev/null +++ b/apps/openmw/mwlua/types/actor.cpp @@ -0,0 +1,250 @@ +#include "types.hpp" + +#include + +#include +#include +#include +#include + +#include "../luabindings.hpp" +#include "../localscripts.hpp" +#include "../luamanagerimp.hpp" +#include "../stats.hpp" + +namespace MWLua +{ + namespace + { + class SetEquipmentAction final : public LuaManager::Action + { + public: + using Item = std::variant; // recordId or ObjectId + using Equipment = std::map; // slot to item + + SetEquipmentAction(LuaUtil::LuaState* state, ObjectId actor, Equipment equipment) + : Action(state), mActor(actor), mEquipment(std::move(equipment)) {} + + void apply(WorldView& worldView) const override + { + MWWorld::Ptr actor = worldView.getObjectRegistry()->getPtr(mActor, false); + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + std::array usedSlots; + std::fill(usedSlots.begin(), usedSlots.end(), false); + + static constexpr int anySlot = -1; + auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView](int slot, const Item& item) -> bool + { + auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); + MWWorld::Ptr itemPtr; + if (std::holds_alternative(item)) + { + itemPtr = worldView.getObjectRegistry()->getPtr(std::get(item), false); + if (old_it != store.end() && *old_it == itemPtr) + return true; // already equipped + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 || + itemPtr.getContainerStore() != static_cast(&store)) + { + Log(Debug::Warning) << "Object" << idToString(std::get(item)) << " is not in inventory"; + return false; + } + } + else + { + const std::string& recordId = std::get(item); + if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) + return true; // already equipped + itemPtr = store.search(recordId); + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) + { + Log(Debug::Warning) << "There is no object with recordId='" << recordId << "' in inventory"; + return false; + } + } + + auto [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + bool requestedSlotIsAllowed = std::find(allowedSlots.begin(), allowedSlots.end(), slot) != allowedSlots.end(); + if (!requestedSlotIsAllowed) + { + auto firstAllowed = std::find_if(allowedSlots.begin(), allowedSlots.end(), [&](int s) { return !usedSlots[s]; }); + if (firstAllowed == allowedSlots.end()) + { + Log(Debug::Warning) << "No suitable slot for " << ptrToString(itemPtr); + return false; + } + slot = *firstAllowed; + } + + // TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search. + MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr); + if (it == store.end()) // should never happen + throw std::logic_error("Item not found in container"); + + store.equip(slot, it, actor); + return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed + }; + + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto old_it = store.getSlot(slot); + auto new_it = mEquipment.find(slot); + if (new_it == mEquipment.end()) + { + if (old_it != store.end()) + store.unequipSlot(slot, actor); + continue; + } + if (tryEquipToSlot(slot, new_it->second)) + usedSlots[slot] = true; + } + for (const auto& [slot, item] : mEquipment) + if (slot >= MWWorld::InventoryStore::Slots) + tryEquipToSlot(anySlot, item); + } + + std::string toString() const override { return "SetEquipmentAction"; } + + private: + ObjectId mActor; + Equipment mEquipment; + }; + } + + using SelfObject = LocalScripts::SelfObject; + + void addActorBindings(sol::table actor, const Context& context) + { + 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({ + {"Helmet", MWWorld::InventoryStore::Slot_Helmet}, + {"Cuirass", MWWorld::InventoryStore::Slot_Cuirass}, + {"Greaves", MWWorld::InventoryStore::Slot_Greaves}, + {"LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron}, + {"RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron}, + {"LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet}, + {"RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet}, + {"Boots", MWWorld::InventoryStore::Slot_Boots}, + {"Shirt", MWWorld::InventoryStore::Slot_Shirt}, + {"Pants", MWWorld::InventoryStore::Slot_Pants}, + {"Skirt", MWWorld::InventoryStore::Slot_Skirt}, + {"Robe", MWWorld::InventoryStore::Slot_Robe}, + {"LeftRing", MWWorld::InventoryStore::Slot_LeftRing}, + {"RightRing", MWWorld::InventoryStore::Slot_RightRing}, + {"Amulet", MWWorld::InventoryStore::Slot_Amulet}, + {"Belt", MWWorld::InventoryStore::Slot_Belt}, + {"CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight}, + {"CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft}, + {"Ammunition", MWWorld::InventoryStore::Slot_Ammunition} + })); + + actor["stance"] = [](const Object& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + if (cls.isActor()) + return cls.getCreatureStats(o.ptr()).getDrawState(); + else + throw std::runtime_error("Actor expected"); + }; + + actor["canMove"] = [](const Object& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getMaxSpeed(o.ptr()) > 0; + }; + actor["runSpeed"] = [](const Object& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getRunSpeed(o.ptr()); + }; + actor["walkSpeed"] = [](const Object& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getWalkSpeed(o.ptr()); + }; + actor["currentSpeed"] = [](const Object& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getCurrentSpeed(o.ptr()); + }; + + actor["isOnGround"] = [](const LObject& o) + { + return MWBase::Environment::get().getWorld()->isOnGround(o.ptr()); + }; + actor["isSwimming"] = [](const LObject& o) + { + return MWBase::Environment::get().getWorld()->isSwimming(o.ptr()); + }; + + actor["inventory"] = sol::overload( + [](const LObject& o) { return Inventory{o}; }, + [](const GObject& o) { return Inventory{o}; } + ); + auto getAllEquipment = [context](const Object& o) + { + const MWWorld::Ptr& ptr = o.ptr(); + sol::table equipment(context.mLua->sol(), sol::create); + if (!ptr.getClass().hasInventoryStore(ptr)) + return equipment; + + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto it = store.getSlot(slot); + if (it == store.end()) + continue; + context.mWorldView->getObjectRegistry()->registerPtr(*it); + equipment[slot] = o.getObject(context.mLua->sol(), getId(*it)); + } + return equipment; + }; + auto getEquipmentFromSlot = [context](const Object& o, int slot) -> sol::object + { + const MWWorld::Ptr& ptr = o.ptr(); + sol::table equipment(context.mLua->sol(), sol::create); + if (!ptr.getClass().hasInventoryStore(ptr)) + return sol::nil; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + auto it = store.getSlot(slot); + if (it == store.end()) + return sol::nil; + context.mWorldView->getObjectRegistry()->registerPtr(*it); + return o.getObject(context.mLua->sol(), getId(*it)); + }; + actor["equipment"] = sol::overload(getAllEquipment, getEquipmentFromSlot); + actor["hasEquipped"] = [](const Object& o, const Object& item) + { + const MWWorld::Ptr& ptr = o.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return false; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + return store.isEquipped(item.ptr()); + }; + actor["setEquipment"] = [context](const SelfObject& obj, const sol::table& equipment) + { + if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) + { + if (!equipment.empty()) + throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); + return; + } + SetEquipmentAction::Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = key.as(); + if (value.is()) + eqp[slot] = value.as().id(); + else + eqp[slot] = value.as(); + } + context.mLuaManager->addAction(std::make_unique(context.mLua, obj.id(), std::move(eqp))); + }; + + addActorStatsBindings(actor, context); + } + +} diff --git a/apps/openmw/mwlua/types/apparatus.cpp b/apps/openmw/mwlua/types/apparatus.cpp new file mode 100644 index 0000000000..5957feab60 --- /dev/null +++ b/apps/openmw/mwlua/types/apparatus.cpp @@ -0,0 +1,52 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +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}, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + apparatus["record"] = sol::overload( + [](const Object& obj) -> const ESM::Apparatus* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Apparatus* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Apparatus"); + record[sol::meta_function::to_string] = [](const ESM::Apparatus& rec) { return "ESM3_Apparatus[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mId; }); + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mScript; }); + record["icon"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string + { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["type"] = sol::readonly_property([](const ESM::Apparatus& rec) -> int { return rec.mData.mType; }); + record["value"] = sol::readonly_property([](const ESM::Apparatus& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Apparatus& rec) -> float { return rec.mData.mWeight; }); + record["quality"] = sol::readonly_property([](const ESM::Apparatus& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/book.cpp b/apps/openmw/mwlua/types/book.cpp new file mode 100644 index 0000000000..5129f94049 --- /dev/null +++ b/apps/openmw/mwlua/types/book.cpp @@ -0,0 +1,62 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addBookBindings(sol::table book, const Context& context) + { + sol::table skill(context.mLua->sol(), sol::create); + book["SKILL"] = LuaUtil::makeStrictReadOnly(skill); + for (int id = ESM::Skill::Block; id < ESM::Skill::Length; ++id) + { + std::string skillName = Misc::StringUtils::lowerCase(ESM::Skill::sSkillNames[id]); + skill[skillName] = skillName; + } + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + book["record"] = sol::overload( + [](const Object& obj) -> const ESM::Book* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Book* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Book"); + record[sol::meta_function::to_string] = [](const ESM::Book& rec) { return "ESM3_Book[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mId; }); + 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; }); + 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; }); + 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; }); + record["enchantCapacity"] = 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 + { + if (rec.mData.mSkillId >= 0) + return Misc::StringUtils::lowerCase(ESM::Skill::sSkillNames[rec.mData.mSkillId]); + else + return sol::nullopt; + }); + } +} diff --git a/apps/openmw/mwlua/types/container.cpp b/apps/openmw/mwlua/types/container.cpp new file mode 100644 index 0000000000..e1ce587e2c --- /dev/null +++ b/apps/openmw/mwlua/types/container.cpp @@ -0,0 +1,55 @@ +#include "types.hpp" + +#include +#include +#include + +#include +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + static const MWWorld::Ptr& containerPtr(const Object& o) { return verifyType(ESM::REC_CONT, o.ptr()); } + + void addContainerBindings(sol::table container, const Context& context) + { + container["content"] = sol::overload( + [](const LObject& o) { containerPtr(o); return Inventory{o}; }, + [](const GObject& o) { containerPtr(o); return Inventory{o}; } + ); + container["encumbrance"] = [](const Object& obj) -> float { + const MWWorld::Ptr& ptr = containerPtr(obj); + return ptr.getClass().getEncumbrance(ptr); + }; + container["capacity"] = [](const Object& obj) -> float { + const MWWorld::Ptr& ptr = containerPtr(obj); + return ptr.getClass().getCapacity(ptr); + }; + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + container["record"] = sol::overload( + [](const Object& obj) -> const ESM::Container* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Container* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Container"); + record[sol::meta_function::to_string] = [](const ESM::Container& rec) -> std::string { return "ESM3_Container[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mId; }); + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mScript; }); + record["weight"] = sol::readonly_property([](const ESM::Container& rec) -> float { return rec.mWeight; }); + } +} diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp new file mode 100644 index 0000000000..251edd24e0 --- /dev/null +++ b/apps/openmw/mwlua/types/creature.cpp @@ -0,0 +1,38 @@ +#include "types.hpp" + +#include + +#include +#include +#include + +#include "../stats.hpp" +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addCreatureBindings(sol::table creature, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + creature["record"] = sol::overload( + [](const Object& obj) -> const ESM::Creature* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Creature* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Creature"); + record[sol::meta_function::to_string] = [](const ESM::Creature& rec) { return "ESM3_Creature[" + rec.mId + "]"; }; + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mScript; }); + record["baseCreature"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mOriginal; }); + } +} diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp new file mode 100644 index 0000000000..4b215d0881 --- /dev/null +++ b/apps/openmw/mwlua/types/door.cpp @@ -0,0 +1,64 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + static const MWWorld::Ptr& doorPtr(const Object& o) { return verifyType(ESM::REC_DOOR, o.ptr()); } + + void addDoorBindings(sol::table door, const Context& context) + { + door["isTeleport"] = [](const Object& o) { return doorPtr(o).getCellRef().getTeleport(); }; + door["destPosition"] = [](const Object& o) -> osg::Vec3f + { + return doorPtr(o).getCellRef().getDoorDest().asVec3(); + }; + door["destRotation"] = [](const Object& o) -> osg::Vec3f + { + return doorPtr(o).getCellRef().getDoorDest().asRotationVec3(); + }; + door["destCell"] = [worldView=context.mWorldView](sol::this_state lua, const Object& o) -> sol::object + { + const MWWorld::CellRef& cellRef = doorPtr(o).getCellRef(); + if (!cellRef.getTeleport()) + return sol::nil; + MWWorld::CellStore* cell = worldView->findCell(cellRef.getDestCell(), cellRef.getDoorDest().asVec3()); + if (cell) + return o.getCell(lua, cell); + else + return sol::nil; + }; + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + door["record"] = sol::overload( + [](const Object& obj) -> const ESM::Door* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Door* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Door"); + record[sol::meta_function::to_string] = [](const ESM::Door& rec) -> std::string { return "ESM3_Door[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mId; }); + 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; }); + record["openSound"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mOpenSound; }); + record["closeSound"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mCloseSound; }); + } + +} diff --git a/apps/openmw/mwlua/types/lockpick.cpp b/apps/openmw/mwlua/types/lockpick.cpp new file mode 100644 index 0000000000..9a46399982 --- /dev/null +++ b/apps/openmw/mwlua/types/lockpick.cpp @@ -0,0 +1,45 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addLockpickBindings(sol::table lockpick, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + lockpick["record"] = sol::overload( + [](const Object& obj) -> const ESM::Lockpick* { return obj.ptr().get()->mBase;}, + [store](const std::string& recordId) -> const ESM::Lockpick* { return store->find(recordId);}); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Lockpick"); + record[sol::meta_function::to_string] = [](const ESM::Lockpick& rec) { return "ESM3_Lockpick[" + rec.mId + "]";}; + record["id"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mId;}); + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mScript;}); + record["icon"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string + { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] = sol::readonly_property([](const ESM::Lockpick& rec) -> int { return rec.mData.mUses;}); + record["value"] = sol::readonly_property([](const ESM::Lockpick& rec) -> int { return rec.mData.mValue;}); + record["weight"] = sol::readonly_property([](const ESM::Lockpick& rec) -> float { return rec.mData.mWeight;}); + record["quality"] = sol::readonly_property([](const ESM::Lockpick& rec) -> float { return rec.mData.mQuality;}); + } +} diff --git a/apps/openmw/mwlua/types/misc.cpp b/apps/openmw/mwlua/types/misc.cpp new file mode 100644 index 0000000000..be53d03d93 --- /dev/null +++ b/apps/openmw/mwlua/types/misc.cpp @@ -0,0 +1,44 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addMiscellaneousBindings(sol::table miscellaneous, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + miscellaneous["record"] = sol::overload( + [](const Object& obj) -> const ESM::Miscellaneous* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Miscellaneous* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Miscellaneous"); + record[sol::meta_function::to_string] = [](const ESM::Miscellaneous& rec) { return "ESM3_Miscellaneous[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { return rec.mId; }); + 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); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { return rec.mScript; }); + record["icon"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string + { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["isKey"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> bool { return rec.mData.mIsKey; }); + record["value"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> float { return rec.mData.mWeight; }); + } +} diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp new file mode 100644 index 0000000000..0c9dd3fb9c --- /dev/null +++ b/apps/openmw/mwlua/types/npc.cpp @@ -0,0 +1,35 @@ +#include "types.hpp" + +#include + +#include + +#include "../stats.hpp" +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addNpcBindings(sol::table npc, const Context& context) + { + addNpcStatsBindings(npc, context); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + npc["record"] = sol::overload( + [](const Object& obj) -> const ESM::NPC* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::NPC* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_NPC"); + record[sol::meta_function::to_string] = [](const ESM::NPC& rec) { return "ESM3_NPC[" + rec.mId + "]"; }; + record["name"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mName; }); + record["race"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mRace; }); + record["class"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mClass; }); + record["mwscript"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mScript; }); + record["hair"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHair; }); + record["head"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead; }); + } +} diff --git a/apps/openmw/mwlua/types/potion.cpp b/apps/openmw/mwlua/types/potion.cpp new file mode 100644 index 0000000000..c3c5ace281 --- /dev/null +++ b/apps/openmw/mwlua/types/potion.cpp @@ -0,0 +1,43 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addPotionBindings(sol::table potion, const Context& context) + { + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + potion["record"] = sol::overload( + [](const Object& obj) -> const ESM::Potion* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Potion* { return store->find(recordId); }); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Potion"); + record[sol::meta_function::to_string] = [](const ESM::Potion& rec) { return "ESM3_Potion[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mId; }); + 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); + }); + 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; }); + 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; }); + } +} diff --git a/apps/openmw/mwlua/types/probe.cpp b/apps/openmw/mwlua/types/probe.cpp new file mode 100644 index 0000000000..373ac622ef --- /dev/null +++ b/apps/openmw/mwlua/types/probe.cpp @@ -0,0 +1,45 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addProbeBindings(sol::table probe, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + probe["record"] = sol::overload( + [](const Object& obj) -> const ESM::Probe* { return obj.ptr().get()->mBase;}, + [store](const std::string& recordId) -> const ESM::Probe* { return store->find(recordId);}); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Probe"); + record[sol::meta_function::to_string] = [](const ESM::Probe& rec) { return "ESM3_Probe[" + rec.mId + "]";}; + record["id"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mId;}); + 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;}); + record["icon"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string + { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] = sol::readonly_property([](const ESM::Probe& rec) -> int { return rec.mData.mUses;}); + record["value"] = sol::readonly_property([](const ESM::Probe& rec) -> int { return rec.mData.mValue;}); + record["weight"] = sol::readonly_property([](const ESM::Probe& rec) -> float { return rec.mData.mWeight;}); + record["quality"] = sol::readonly_property([](const ESM::Probe& rec) -> float { return rec.mData.mQuality;}); + } +} diff --git a/apps/openmw/mwlua/types/repair.cpp b/apps/openmw/mwlua/types/repair.cpp new file mode 100644 index 0000000000..5b73e713c5 --- /dev/null +++ b/apps/openmw/mwlua/types/repair.cpp @@ -0,0 +1,45 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + void addRepairBindings(sol::table repair, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + repair["record"] = sol::overload( + [](const Object& obj) -> const ESM::Repair* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Repair* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Repair"); + record[sol::meta_function::to_string] = [](const ESM::Repair& rec) { return "ESM3_Repair[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mId; }); + 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; }); + record["icon"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string + { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] = sol::readonly_property([](const ESM::Repair& rec) -> int { return rec.mData.mUses; }); + record["value"] = sol::readonly_property([](const ESM::Repair& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Repair& rec) -> float { return rec.mData.mWeight; }); + record["quality"] = sol::readonly_property([](const ESM::Repair& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp new file mode 100644 index 0000000000..6235216670 --- /dev/null +++ b/apps/openmw/mwlua/types/types.cpp @@ -0,0 +1,191 @@ +#include "types.hpp" + +#include +#include + +namespace MWLua +{ + namespace ObjectTypeName + { + // Names of object types in Lua. + // These names are part of OpenMW Lua API. + constexpr std::string_view Actor = "Actor"; // base type for NPC, Creature, Player + constexpr std::string_view Item = "Item"; // base type for all items + + constexpr std::string_view Activator = "Activator"; + constexpr std::string_view Armor = "Armor"; + constexpr std::string_view Book = "Book"; + constexpr std::string_view Clothing = "Clothing"; + constexpr std::string_view Container = "Container"; + constexpr std::string_view Creature = "Creature"; + constexpr std::string_view Door = "Door"; + constexpr std::string_view Ingredient = "Ingredient"; + constexpr std::string_view Light = "Light"; + constexpr std::string_view MiscItem = "Miscellaneous"; + constexpr std::string_view NPC = "NPC"; + constexpr std::string_view Player = "Player"; + constexpr std::string_view Potion = "Potion"; + constexpr std::string_view Static = "Static"; + constexpr std::string_view Weapon = "Weapon"; + constexpr std::string_view Apparatus = "Apparatus"; + constexpr std::string_view Lockpick = "Lockpick"; + constexpr std::string_view Probe = "Probe"; + constexpr std::string_view Repair = "Repair"; + constexpr std::string_view Marker = "Marker"; + } + + namespace + { + const static std::unordered_map luaObjectTypeInfo = { + {ESM::REC_INTERNAL_PLAYER, ObjectTypeName::Player}, + {ESM::REC_INTERNAL_MARKER, ObjectTypeName::Marker}, + {ESM::REC_ACTI, ObjectTypeName::Activator}, + {ESM::REC_ARMO, ObjectTypeName::Armor}, + {ESM::REC_BOOK, ObjectTypeName::Book}, + {ESM::REC_CLOT, ObjectTypeName::Clothing}, + {ESM::REC_CONT, ObjectTypeName::Container}, + {ESM::REC_CREA, ObjectTypeName::Creature}, + {ESM::REC_DOOR, ObjectTypeName::Door}, + {ESM::REC_INGR, ObjectTypeName::Ingredient}, + {ESM::REC_LIGH, ObjectTypeName::Light}, + {ESM::REC_MISC, ObjectTypeName::MiscItem}, + {ESM::REC_NPC_, ObjectTypeName::NPC}, + {ESM::REC_ALCH, ObjectTypeName::Potion}, + {ESM::REC_STAT, ObjectTypeName::Static}, + {ESM::REC_WEAP, ObjectTypeName::Weapon}, + {ESM::REC_APPA, ObjectTypeName::Apparatus}, + {ESM::REC_LOCK, ObjectTypeName::Lockpick}, + {ESM::REC_PROB, ObjectTypeName::Probe}, + {ESM::REC_REPA, ObjectTypeName::Repair}, + }; + + } + + unsigned int getLiveCellRefType(const MWWorld::LiveCellRefBase* ref) + { + if (ref == nullptr) + throw std::runtime_error("Can't get type name from an empty object."); + const std::string_view id = ref->mRef.getRefId(); + if (id == "player") + return ESM::REC_INTERNAL_PLAYER; + if (Misc::ResourceHelpers::isHiddenMarker(id)) + return ESM::REC_INTERNAL_MARKER; + return ref->getType(); + } + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback) + { + auto it = luaObjectTypeInfo.find(type); + if (it != luaObjectTypeInfo.end()) + return it->second; + else + return fallback; + } + + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr) + { + return getLuaObjectTypeName(static_cast(getLiveCellRefType(ptr.mRef)), /*fallback=*/ptr.getTypeDescription()); + } + + const MWWorld::Ptr& verifyType(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) + { + if (ptr.getType() != recordType) + { + std::string msg = "Requires type '"; + msg.append(getLuaObjectTypeName(recordType)); + msg.append("', but applied to "); + msg.append(ptrToString(ptr)); + throw std::runtime_error(msg); + } + return ptr; + } + + sol::table getTypeToPackageTable(lua_State* L) + { + constexpr std::string_view key = "typeToPackage"; + sol::state_view lua(L); + if (lua[key] == sol::nil) + lua[key] = sol::table(lua, sol::create); + return lua[key]; + } + + sol::table getPackageToTypeTable(lua_State* L) + { + constexpr std::string_view key = "packageToType"; + sol::state_view lua(L); + if (lua[key] == sol::nil) + lua[key] = sol::table(lua, sol::create); + return lua[key]; + } + + sol::table initTypesPackage(const Context& context) + { + auto* lua = context.mLua; + sol::table types(lua->sol(), 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 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); + baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(types[*base]); + t[sol::metatable_key] = baseMeta; + } + t["objectIsInstance"] = [types=recTypes](const Object& o) + { + unsigned int type = getLiveCellRefType(o.ptr().mRef); + for (ESM::RecNameInts t : types) + if (t == type) + return true; + return false; + }; + types[name] = ro; + return t; + }; + + addActorBindings(addType(ObjectTypeName::Actor, {ESM::REC_INTERNAL_PLAYER, ESM::REC_CREA, ESM::REC_NPC_}), context); + 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}); + + addCreatureBindings(addType(ObjectTypeName::Creature, {ESM::REC_CREA}, ObjectTypeName::Actor), context); + addNpcBindings(addType(ObjectTypeName::NPC, {ESM::REC_INTERNAL_PLAYER, ESM::REC_NPC_}, ObjectTypeName::Actor), context); + addType(ObjectTypeName::Player, {ESM::REC_INTERNAL_PLAYER}, ObjectTypeName::NPC); + + addType(ObjectTypeName::Armor, {ESM::REC_ARMO}, ObjectTypeName::Item); + addType(ObjectTypeName::Clothing, {ESM::REC_CLOT}, ObjectTypeName::Item); + addType(ObjectTypeName::Ingredient, {ESM::REC_INGR}, ObjectTypeName::Item); + addType(ObjectTypeName::Light, {ESM::REC_LIGH}, ObjectTypeName::Item); + addMiscellaneousBindings(addType(ObjectTypeName::MiscItem, {ESM::REC_MISC}, ObjectTypeName::Item), context); + addPotionBindings(addType(ObjectTypeName::Potion, {ESM::REC_ALCH}, ObjectTypeName::Item), context); + addWeaponBindings(addType(ObjectTypeName::Weapon, {ESM::REC_WEAP}, ObjectTypeName::Item), context); + addBookBindings(addType(ObjectTypeName::Book, {ESM::REC_BOOK}, ObjectTypeName::Item), context); + addLockpickBindings(addType(ObjectTypeName::Lockpick, {ESM::REC_LOCK}, ObjectTypeName::Item), context); + addProbeBindings(addType(ObjectTypeName::Probe, {ESM::REC_PROB}, ObjectTypeName::Item), context); + addApparatusBindings(addType(ObjectTypeName::Apparatus, {ESM::REC_APPA}, ObjectTypeName::Item), context); + addRepairBindings(addType(ObjectTypeName::Repair, {ESM::REC_REPA}, ObjectTypeName::Item), context); + + addActivatorBindings(addType(ObjectTypeName::Activator, {ESM::REC_ACTI}), context); + addContainerBindings(addType(ObjectTypeName::Container, {ESM::REC_CONT}), context); + addDoorBindings(addType(ObjectTypeName::Door, {ESM::REC_DOOR}), context); + addType(ObjectTypeName::Static, {ESM::REC_STAT}); + + sol::table typeToPackage = getTypeToPackageTable(context.mLua->sol()); + sol::table packageToType = getPackageToTypeTable(context.mLua->sol()); + for (const auto& [type, name] : luaObjectTypeInfo) + { + sol::object t = types[name]; + if (t == sol::nil) + continue; + typeToPackage[type] = t; + packageToType[t] = type; + } + + return LuaUtil::makeReadOnly(types); + } +} diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp new file mode 100644 index 0000000000..c4b9b63a45 --- /dev/null +++ b/apps/openmw/mwlua/types/types.hpp @@ -0,0 +1,46 @@ +#ifndef MWLUA_TYPES_H +#define MWLUA_TYPES_H + +#include + +#include +#include + +#include "../context.hpp" + +namespace MWLua +{ + // `getLiveCellRefType()` is not exactly what we usually mean by "type" because some refids have special meaning. + // This function handles these special refids (and by this adds some performance overhead). + // We use this "fixed" type in Lua because we don't want to expose the weirdness of Morrowind internals to our API. + // TODO: Implement https://gitlab.com/OpenMW/openmw/-/issues/6617 and make `MWWorld::PtrBase::getType` work the + // same as `getLiveCellRefType`. + unsigned int getLiveCellRefType(const MWWorld::LiveCellRefBase* ref); + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback = "Unknown"); + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr); + const MWWorld::Ptr& verifyType(ESM::RecNameInts type, const MWWorld::Ptr& ptr); + + sol::table getTypeToPackageTable(lua_State* L); + sol::table getPackageToTypeTable(lua_State* L); + + sol::table initTypesPackage(const Context& context); + + // used in initTypesPackage + void addActivatorBindings(sol::table activator, const Context& context); + 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 addActorBindings(sol::table actor, const Context& context); + void addWeaponBindings(sol::table weapon, const Context& context); + void addNpcBindings(sol::table npc, const Context& context); + void addCreatureBindings(sol::table creature, const Context& context); + void addLockpickBindings(sol::table lockpick, const Context& context); + void addProbeBindings(sol::table probe, const Context& context); + void addApparatusBindings(sol::table apparatus, const Context& context); + void addRepairBindings(sol::table repair, const Context& context); + void addMiscellaneousBindings(sol::table miscellaneous, const Context& context); + void addPotionBindings(sol::table potion, const Context& context); +} + +#endif // MWLUA_TYPES_H diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp new file mode 100644 index 0000000000..628e47fc68 --- /dev/null +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -0,0 +1,77 @@ +#include "types.hpp" + +#include +#include +#include + +#include + +#include "../luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} +#include +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}, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + const MWWorld::Store* store = &MWBase::Environment::get().getWorld()->getStore().get(); + weapon["record"] = sol::overload( + [](const Object& obj) -> const ESM::Weapon* { return obj.ptr().get()->mBase; }, + [store](const std::string& recordId) -> const ESM::Weapon* { return store->find(recordId); }); + sol::usertype record = context.mLua->sol().new_usertype("ESM3_Weapon"); + record[sol::meta_function::to_string] = [](const ESM::Weapon& rec) -> std::string { return "ESM3_Weapon[" + rec.mId + "]"; }; + record["id"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mId; }); + 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); + }); + 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; }); + record["mwscript"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mScript; }); + record["isMagical"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Magical; }); + record["isSilver"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Silver; }); + record["weight"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mValue; }); + record["type"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mType; }); + record["health"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mHealth; }); + record["speed"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mSpeed; }); + record["reach"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mReach; }); + record["enchantCapacity"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mEnchant * 0.1f; }); + record["chopMinDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mChop[0]; }); + record["chopMaxDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mChop[1]; }); + record["slashMinDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mSlash[0]; }); + record["slashMaxDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mSlash[1]; }); + record["thrustMinDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mThrust[0]; }); + record["thrustMaxDamage"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mThrust[1]; }); + } + +} diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp new file mode 100644 index 0000000000..eec6509034 --- /dev/null +++ b/apps/openmw/mwlua/uibindings.cpp @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "context.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/windowmanager.hpp" + +namespace MWLua +{ + namespace + { + class UiAction final : public LuaManager::Action + { + public: + enum Type + { + CREATE = 0, + UPDATE, + DESTROY, + }; + + UiAction(Type type, std::shared_ptr element, LuaUtil::LuaState* state) + : Action(state) + , mType{ type } + , mElement{ std::move(element) } + {} + + void apply(WorldView&) const override + { + try { + switch (mType) + { + case CREATE: + mElement->create(); + break; + case UPDATE: + mElement->update(); + break; + case DESTROY: + mElement->destroy(); + break; + } + } + catch (std::exception&) + { + // prevent any actions on a potentially corrupted widget + mElement->mRoot = nullptr; + throw; + } + } + + std::string toString() const override + { + std::string result; + switch (mType) + { + case CREATE: + result += "Create"; + break; + case UPDATE: + result += "Update"; + break; + case DESTROY: + result += "Destroy"; + break; + } + result += " UI"; + return result; + } + + private: + Type mType; + std::shared_ptr mElement; + }; + + // 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; } + } + + sol::table initUserInterfacePackage(const Context& context) + { + auto uiContent = context.mLua->sol().new_usertype("UiContent"); + uiContent[sol::meta_function::length] = [](const LuaUi::Content& content) + { + return content.size(); + }; + uiContent[sol::meta_function::index] = sol::overload( + [](const LuaUi::Content& content, size_t index) + { + return content.at(fromLuaIndex(index)); + }, + [](const LuaUi::Content& content, std::string_view name) + { + return content.at(name); + }); + uiContent[sol::meta_function::new_index] = sol::overload( + [](LuaUi::Content& content, size_t index, const sol::table& table) + { + content.assign(fromLuaIndex(index), table); + }, + [](LuaUi::Content& content, size_t index, sol::nil_t nil) + { + content.remove(fromLuaIndex(index)); + }, + [](LuaUi::Content& content, std::string_view name, const sol::table& table) + { + content.assign(name, table); + }, + [](LuaUi::Content& content, std::string_view name, sol::nil_t nil) + { + content.remove(name); + }); + uiContent["insert"] = [](LuaUi::Content& content, size_t index, const sol::table& table) + { + content.insert(fromLuaIndex(index), table); + }; + uiContent["add"] = [](LuaUi::Content& content, const sol::table& table) + { + content.insert(content.size(), table); + }; + uiContent["indexOf"] = [](LuaUi::Content& content, const sol::table& table) -> sol::optional + { + size_t index = content.indexOf(table); + if (index < content.size()) + return toLuaIndex(index); + else + return sol::nullopt; + }; + { + auto pairs = [](LuaUi::Content& content) + { + auto next = [](LuaUi::Content& content, size_t i) -> sol::optional> + { + if (i < content.size()) + return std::make_tuple(i + 1, content.at(i)); + else + return sol::nullopt; + }; + return std::make_tuple(next, content, 0); + }; + uiContent[sol::meta_function::ipairs] = pairs; + uiContent[sol::meta_function::pairs] = pairs; + } + + 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"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy || element->mUpdate) + return; + element->mUpdate = true; + context.mLuaManager->addAction(std::make_unique(UiAction::UPDATE, element, context.mLua)); + }; + element["destroy"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy) + return; + element->mDestroy = true; + context.mLuaManager->addAction(std::make_unique(UiAction::DESTROY, element, context.mLua)); + }; + + sol::table api = context.mLua->newTable(); + 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))}, + })); + api["printToConsole"] = [luaManager=context.mLuaManager](const std::string& message, const Misc::Color& color) + { + luaManager->addInGameConsoleMessage(message + "\n", color); + }; + api["setConsoleMode"] = [luaManager=context.mLuaManager](std::string_view mode) + { + luaManager->addAction( + [mode = std::string(mode)]{ MWBase::Environment::get().getWindowManager()->setConsoleMode(mode); }); + }; + api["setConsoleSelectedObject"] = [luaManager=context.mLuaManager](const sol::object& obj) + { + const auto wm = MWBase::Environment::get().getWindowManager(); + if (obj == sol::nil) + luaManager->addAction([wm]{ wm->setConsoleSelectedObject(MWWorld::Ptr()); }); + else + { + if (!obj.is()) + throw std::runtime_error("Game object expected"); + luaManager->addAction([wm, obj=obj.as()]{ wm->setConsoleSelectedObject(obj.ptr()); }); + } + }; + api["content"] = [](const sol::table& table) + { + return LuaUi::Content(table); + }; + api["create"] = [context](const sol::table& layout) + { + auto element = LuaUi::Element::make(layout); + context.mLuaManager->addAction(std::make_unique(UiAction::CREATE, element, context.mLua)); + 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["_getMenuTransparency"] = []() + { + return Settings::Manager::getFloat("menu transparency", "GUI"); + }; + + 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 + { + size_t index = LuaUi::Layer::indexOf(name); + if (index == LuaUi::Layer::count()) + return sol::nullopt; + else + return toLuaIndex(index); + }; + layers["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); + if (index == LuaUi::Layer::count()) + throw std::logic_error(std::string("Layer not found")); + 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) + { + LuaUi::Layer::Options options; + options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); + size_t index = LuaUi::Layer::indexOf(beforename); + if (index == LuaUi::Layer::count()) + throw std::logic_error(std::string("Layer not found")); + context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); + }; + { + auto pairs = [layers](const sol::object&) + { + auto next = [](const sol::table& l, size_t i) -> sol::optional> + { + if (i < LuaUi::Layer::count()) + return std::make_tuple(i + 1, LuaUi::Layer(i)); + else + return sol::nullopt; + }; + return std::make_tuple(next, layers, 0); + }; + layers[sol::meta_function::pairs] = pairs; + layers[sol::meta_function::ipairs] = pairs; + } + api["layers"] = LuaUtil::makeReadOnly(layers); + + sol::table typeTable = context.mLua->newTable(); + 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["registerSettingsPage"] = &LuaUi::registerSettingsPage; + + api["texture"] = [luaManager=context.mLuaManager](const sol::table& options) + { + LuaUi::TextureData data; + sol::object path = LuaUtil::getFieldOrNil(options, "path"); + if (path.is()) + data.mPath = path.as(); + if (data.mPath.empty()) + throw std::logic_error("Invalid texture path"); + sol::object offset = LuaUtil::getFieldOrNil(options, "offset"); + if (offset.is()) + data.mOffset = offset.as(); + sol::object size = LuaUtil::getFieldOrNil(options, "size"); + if (size.is()) + data.mSize = size.as(); + return luaManager->uiResourceManager()->registerTexture(data); + }; + + api["screenSize"] = []() + { + return osg::Vec2f( + Settings::Manager::getInt("resolution x", "Video"), + Settings::Manager::getInt("resolution y", "Video") + ); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp new file mode 100644 index 0000000000..3aabbfe0ac --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -0,0 +1,63 @@ +#include "userdataserializer.hpp" + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class Serializer final : public LuaUtil::UserdataSerializer + { + public: + explicit Serializer(bool localSerializer, ObjectRegistry* registry, std::map* contentFileMapping) + : mLocalSerializer(localSerializer), mObjectRegistry(registry), mContentFileMapping(contentFileMapping) {} + + private: + // Appends serialized sol::userdata to the end of BinaryData. + // Returns false if this type of userdata is not supported by this serializer. + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is() || data.is()) + { + appendRefNum(out, data.as().id()); + return true; + } + return false; + } + + // Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push. + // Returns false if this type is not supported by this serializer. + bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override + { + if (typeName == sRefNumTypeName) + { + ObjectId id = loadRefNum(binaryData); + if (id.hasContentFile() && mContentFileMapping) + { + auto iter = mContentFileMapping->find(id.mContentFile); + if (iter != mContentFileMapping->end()) + id.mContentFile = iter->second; + } + if (mLocalSerializer) + sol::stack::push(lua, LObject(id, mObjectRegistry)); + else + sol::stack::push(lua, GObject(id, mObjectRegistry)); + return true; + } + return false; + } + + bool mLocalSerializer; + ObjectRegistry* mObjectRegistry; + std::map* mContentFileMapping; + }; + + std::unique_ptr createUserdataSerializer( + bool local, ObjectRegistry* registry, std::map* contentFileMapping) + { + return std::make_unique(local, registry, contentFileMapping); + } + +} diff --git a/apps/openmw/mwlua/userdataserializer.hpp b/apps/openmw/mwlua/userdataserializer.hpp new file mode 100644 index 0000000000..70af7ebd06 --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.hpp @@ -0,0 +1,22 @@ +#ifndef MWLUA_USERDATASERIALIZER_H +#define MWLUA_USERDATASERIALIZER_H + +#include "object.hpp" + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace MWLua +{ + // UserdataSerializer is an extension for components/lua/serialization.hpp + // Needed to serialize references to objects. + // If local=true, then during deserialization creates LObject, otherwise creates GObject. + // contentFileMapping is used only for deserialization. Needed to fix references if the order + // of content files was changed. + std::unique_ptr createUserdataSerializer( + bool local, ObjectRegistry* registry, std::map* contentFileMapping = nullptr); +} + +#endif // MWLUA_USERDATASERIALIZER_H diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp new file mode 100644 index 0000000000..e1bf3002a7 --- /dev/null +++ b/apps/openmw/mwlua/worldview.cpp @@ -0,0 +1,155 @@ +#include "worldview.hpp" + +#include +#include +#include + +#include "../mwbase/windowmanager.hpp" + +#include "../mwclass/container.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/timestamp.hpp" +#include "../mwworld/cellutils.hpp" + +namespace MWLua +{ + + void WorldView::update() + { + mObjectRegistry.update(); + mActivatorsInScene.updateList(); + mActorsInScene.updateList(); + mContainersInScene.updateList(); + mDoorsInScene.updateList(); + mItemsInScene.updateList(); + mPaused = MWBase::Environment::get().getWindowManager()->isGuiMode(); + } + + void WorldView::clear() + { + mObjectRegistry.clear(); + mActivatorsInScene.clear(); + mActorsInScene.clear(); + mContainersInScene.clear(); + mDoorsInScene.clear(); + mItemsInScene.clear(); + } + + WorldView::ObjectGroup* WorldView::chooseGroup(const MWWorld::Ptr& ptr) + { + // It is important to check `isMarker` first. + // For example "prisonmarker" has class "Door" despite that it is only an invisible marker. + if (isMarker(ptr)) + return nullptr; + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActivator()) + return &mActivatorsInScene; + if (cls.isActor()) + return &mActorsInScene; + if (cls.isDoor()) + return &mDoorsInScene; + if (typeid(cls) == typeid(MWClass::Container)) + return &mContainersInScene; + if (cls.hasToolTip(ptr)) + return &mItemsInScene; + return nullptr; + } + + void WorldView::objectAddedToScene(const MWWorld::Ptr& ptr) + { + mObjectRegistry.registerPtr(ptr); + ObjectGroup* group = chooseGroup(ptr); + if (group) + addToGroup(*group, ptr); + } + + void WorldView::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + ObjectGroup* group = chooseGroup(ptr); + if (group) + removeFromGroup(*group, ptr); + } + + double WorldView::getGameTime() const + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::TimeStamp timeStamp = world->getTimeStamp(); + return (static_cast(timeStamp.getDay()) * 24 + timeStamp.getHour()) * 3600.0; + } + + void WorldView::load(ESM::ESMReader& esm) + { + esm.getHNT(mSimulationTime, "LUAW"); + ObjectId lastAssignedId; + lastAssignedId.load(esm, true); + mObjectRegistry.setLastAssignedId(lastAssignedId); + } + + void WorldView::save(ESM::ESMWriter& esm) const + { + esm.writeHNT("LUAW", mSimulationTime); + mObjectRegistry.getLastAssignedId().save(esm, true); + } + + void WorldView::ObjectGroup::updateList() + { + if (mChanged) + { + mList->clear(); + for (const ObjectId& id : mSet) + mList->push_back(id); + mChanged = false; + } + } + + void WorldView::ObjectGroup::clear() + { + mChanged = false; + mList->clear(); + mSet.clear(); + } + + void WorldView::addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.insert(getId(ptr)); + group.mChanged = true; + } + + void WorldView::removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.erase(getId(ptr)); + group.mChanged = true; + } + + // TODO: If Lua scripts will use several threads at the same time, then `find*Cell` functions should have critical sections. + MWWorld::CellStore* WorldView::findCell(const std::string& name, osg::Vec3f position) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + bool exterior = name.empty() || world->getExterior(name); + if (exterior) + { + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(position.x(), position.y()); + return world->getExterior(cellIndex.x(), cellIndex.y()); + } + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findNamedCell(const std::string& name) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::Cell* esmCell = world->getExterior(name); + if (esmCell) + return world->getExterior(esmCell->getGridX(), esmCell->getGridY()); + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findExteriorCell(int x, int y) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + return world->getExterior(x, y); + } + +} diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp new file mode 100644 index 0000000000..181a73c0b0 --- /dev/null +++ b/apps/openmw/mwlua/worldview.hpp @@ -0,0 +1,95 @@ +#ifndef MWLUA_WORLDVIEW_H +#define MWLUA_WORLDVIEW_H + +#include "object.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include + +namespace ESM +{ + class ESMWriter; + class ESMReader; +} + +namespace MWLua +{ + + // Tracks all used game objects. + class WorldView + { + public: + void update(); // Should be called every frame. + void clear(); // Should be called every time before starting or loading a new game. + + // Whether the world is paused (i.e. game time is not changing and actors don't move). + bool isPaused() const { return mPaused; } + + // The number of seconds passed from the beginning of the game. + double getSimulationTime() const { return mSimulationTime; } + void setSimulationTime(double t) { mSimulationTime = t; } + + // The game time (in game seconds) passed from the beginning of the game. + // Note that game time generally goes faster than the simulation time. + double getGameTime() const; + double getGameTimeScale() const { return MWBase::Environment::get().getWorld()->getTimeScaleFactor(); } + void setGameTimeScale(double s) { MWBase::Environment::get().getWorld()->setGlobalFloat("timescale", s); } + + ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; } + ObjectIdList getActorsInScene() const { return mActorsInScene.mList; } + ObjectIdList getContainersInScene() const { return mContainersInScene.mList; } + ObjectIdList getDoorsInScene() const { return mDoorsInScene.mList; } + ObjectIdList getItemsInScene() const { return mItemsInScene.mList; } + + ObjectRegistry* getObjectRegistry() { return &mObjectRegistry; } + + void objectUnloaded(const MWWorld::Ptr& ptr) { mObjectRegistry.deregisterPtr(ptr); } + + void objectAddedToScene(const MWWorld::Ptr& ptr); + void objectRemovedFromScene(const MWWorld::Ptr& ptr); + + // Returns list of objects that meets the `query` criteria. + // If onlyActive = true, then search only among the objects that are currently in the scene. + // TODO: ObjectIdList selectObjects(const Queries::Query& query, bool onlyActive); + + MWWorld::CellStore* findCell(const std::string& name, osg::Vec3f position); + MWWorld::CellStore* findNamedCell(const std::string& name); + MWWorld::CellStore* findExteriorCell(int x, int y); + + void load(ESM::ESMReader& esm); + void save(ESM::ESMWriter& esm) const; + + // TODO: move this functionality to MWClass + bool isItem(const MWWorld::Ptr& ptr) { return chooseGroup(ptr) == &mItemsInScene; } + + private: + struct ObjectGroup + { + void updateList(); + void clear(); + + bool mChanged = false; + ObjectIdList mList = std::make_shared>(); + std::set mSet; + }; + + ObjectGroup* chooseGroup(const MWWorld::Ptr& ptr); + void addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + void removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + + ObjectRegistry mObjectRegistry; + ObjectGroup mActivatorsInScene; + ObjectGroup mActorsInScene; + ObjectGroup mContainersInScene; + ObjectGroup mDoorsInScene; + ObjectGroup mItemsInScene; + + double mSimulationTime = 0; + bool mPaused = false; + }; + +} + +#endif // MWLUA_WORLDVIEW_H diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 928293e649..e03947d7fc 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,351 +1,497 @@ #include "activespells.hpp" +#include + +#include + #include #include +#include + +#include -#include +#include + +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "spelleffects.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + #include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" -namespace MWMechanics +namespace { - void ActiveSpells::update(float duration) const + bool merge(std::vector& present, const std::vector& queued) { - bool rebuild = false; - - // Erase no longer active spells and effects - if (duration > 0) + // Can't merge if we already have an effect with the same effect index + auto problem = std::find_if(queued.begin(), queued.end(), [&] (const auto& qEffect) { - TContainer::iterator iter (mSpells.begin()); - while (iter!=mSpells.end()) - { - if (!timeToExpire (iter)) - { - mSpells.erase (iter++); - rebuild = true; - } - else - { - bool interrupt = false; - std::vector& effects = iter->second.mEffects; - for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) - { - if (effectIt->mTimeLeft <= 0) - { - rebuild = true; - - // Note: it we expire a Corprus effect, we should remove the whole spell. - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - iter = mSpells.erase (iter); - interrupt = true; - break; - } - - effectIt = effects.erase(effectIt); - } - else - { - effectIt->mTimeLeft -= duration; - ++effectIt; - } - } - - if (!interrupt) - ++iter; - } - } - } - - if (mSpellsChanged) - { - mSpellsChanged = false; - rebuild = true; - } - - if (rebuild) - rebuildEffects(); + return std::find_if(present.begin(), present.end(), [&] (const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end(); + }); + if(problem != queued.end()) + return false; + present.insert(present.end(), queued.begin(), queued.end()); + return true; } - void ActiveSpells::rebuildEffects() const + void addEffects(std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) { - mEffects = MagicEffects(); - - for (TIterator iter (begin()); iter!=end(); ++iter) + int currentEffectIndex = 0; + for(const auto& enam : list.mList) { - const std::vector& effects = iter->second.mEffects; - - for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) - { - if (effectIt->mTimeLeft > 0) - mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); - } + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mEffectIndex = currentEffectIndex++; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if(ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effects.emplace_back(effect); } } +} - ActiveSpells::ActiveSpells() - : mSpellsChanged (false) - {} +namespace MWMechanics +{ + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) + { + mActiveSpells.mIterating = true; + } - const MagicEffects& ActiveSpells::getMagicEffects() const + ActiveSpells::IterationGuard::~IterationGuard() { - update(0.f); - return mEffects; + mActiveSpells.mIterating = false; } - ActiveSpells::TIterator ActiveSpells::begin() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) + : mId(cast.mId), mDisplayName(cast.mSourceName), mCasterActorId(-1), mSlot(cast.mSlot), mType(cast.mType), mWorsenings(-1) { - return mSpells.begin(); + if(!caster.isEmpty() && caster.getClass().isActor()) + mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId(); } - ActiveSpells::TIterator ActiveSpells::end() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) + : mId(spell->mId), mDisplayName(spell->mName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()), mSlot(0) + , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability : ESM::ActiveSpells::Type_Permanent), mWorsenings(-1) { - return mSpells.end(); + assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + addEffects(mEffects, spell->mEffects, ignoreResistances); } - double ActiveSpells::timeToExpire (const TIterator& iterator) const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor) + : mId(item.getCellRef().getRefId()), mDisplayName(item.getClass().getName(item)), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(slotIndex), mType(ESM::ActiveSpells::Type_Enchantment), mWorsenings(-1) { - const std::vector& effects = iterator->second.mEffects; + assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); + addEffects(mEffects, enchantment->mEffects); + } - float duration = 0; + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) + : mId(params.mId), mEffects(params.mEffects), mDisplayName(params.mDisplayName), mCasterActorId(params.mCasterActorId) + , mSlot(params.mItem.isSet() ? params.mItem.mIndex : 0) + , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) + {} - for (std::vector::const_iterator iter (effects.begin()); - iter!=effects.end(); ++iter) + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) + : mId(params.mId), mDisplayName(params.mDisplayName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(params.mSlot), mType(params.mType), mWorsenings(-1) + {} + + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const + { + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = mId; + params.mEffects = mEffects; + params.mDisplayName = mDisplayName; + params.mCasterActorId = mCasterActorId; + params.mItem.unset(); + if(mSlot) { - if (iter->mTimeLeft > duration) - duration = iter->mTimeLeft; + // Note that we're storing the inventory slot as a RefNum instead of an int as a matter of future proofing + // mSlot needs to be replaced with a RefNum once inventory items get persistent RefNum (#4508 #6148) + params.mItem = { static_cast(mSlot), 0 }; } + params.mType = mType; + params.mWorsenings = mWorsenings; + params.mNextWorsening = mNextWorsening.toEsm(); + return params; + } - if (duration < 0) - return 0; - - return duration; + void ActiveSpells::ActiveSpellParams::worsen() + { + ++mWorsenings; + if(!mWorsenings) + mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); + mNextWorsening += CorprusStats::sWorseningPeriod; } - bool ActiveSpells::isSpellActive(const std::string& id) const + bool ActiveSpells::ActiveSpellParams::shouldWorsen() const { - for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->first, id)) - return true; - } - return false; + return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; } - const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const + void ActiveSpells::ActiveSpellParams::resetWorsenings() { - return mSpells; + mWorsenings = -1; } - void ActiveSpells::addSpell(const std::string &id, bool stack, std::vector effects, - const std::string &displayName, int casterActorId) + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) { - TContainer::iterator it(mSpells.find(id)); + const auto& creatureStats = ptr.getClass().getCreatureStats(ptr); + assert(&creatureStats.getActiveSpells() == this); + IterationGuard guard{*this}; + // 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) + { + ++spellIt; + continue; + } + bool removedSpell = false; + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f) + { + auto effect = *effectIt; + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + removedSpell = applyPurges(ptr, &spellIt, &effectIt); + if(removedSpell) + break; + } + else + { + ++effectIt; + } + } + if(removedSpell) + continue; + if(spellIt->mEffects.empty()) + spellIt = mSpells.erase(spellIt); + else + ++spellIt; + } - ActiveSpellParams params; - params.mEffects = effects; - params.mDisplayName = displayName; - params.mCasterActorId = casterActorId; + for(const auto& spell : mQueue) + addToSpells(ptr, spell); + mQueue.clear(); - if (it == end() || stack) + // Vanilla only does this on cell change I think + const auto& spells = creatureStats.getSpells(); + for(const ESM::Spell* spell : spells) { - mSpells.insert(std::make_pair(id, params)); + if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + mSpells.emplace_back(ActiveSpellParams{spell, ptr}); } - else + + if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { - // addSpell() is called with effects for a range. - // but a spell may have effects with different ranges (e.g. Touch & Target) - // so, if we see new effects for same spell assume additional - // spell effects and add to existing effects of spell - mergeEffects(params.mEffects, it->second.mEffects); - it->second = params; + auto& store = ptr.getClass().getInventoryStore(ptr); + if(store.getInvListener() != nullptr) + { + bool playNonLooping = !store.isFirstEquip(); + const auto world = MWBase::Environment::get().getWorld(); + for(int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if(slot == store.end()) + continue; + const auto& enchantmentId = slot->getClass().getEnchantment(*slot); + if(enchantmentId.empty()) + continue; + const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); + if(enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + continue; + if(std::find_if(mSpells.begin(), mSpells.end(), [&] (const ActiveSpellParams& params) + { + return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId(); + }) != mSpells.end()) + continue; + const ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{*slot, enchantment, slotIndex, ptr}); + for(const auto& effect : params.mEffects) + MWMechanics::playEffects(ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); + } + } } - mSpellsChanged = true; - } - - void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) - { - for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + // Update effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - // if effect is not in addTo, add it - bool missing = true; - for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? + bool removedSpell = false; + std::optional reflected; + for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(result == MagicApplicationResult::REFLECTED) { - missing = false; + if(!reflected) + { + static const bool keepOriginalCaster = Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"); + if(keepOriginalCaster) + reflected = {*spellIt, caster}; + else + reflected = {*spellIt, ptr}; + } + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if(result == MagicApplicationResult::REMOVED) + it = spellIt->mEffects.erase(it); + else + ++it; + removedSpell = applyPurges(ptr, &spellIt, &it); + if(removedSpell) break; + } + if(reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_Reflect"); + 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, std::string()); } + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } - if (missing) + if(removedSpell) + continue; + + bool remove = false; + if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent) { - addTo.push_back(*effect); + try + { + remove = !spells.hasSpell(spellIt->mId); + } + catch(const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); + } } + else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment) + { + const auto& store = ptr.getClass().getInventoryStore(ptr); + auto slot = store.getSlot(spellIt->mSlot); + remove = slot == store.end() || slot->getCellRef().getRefId() != spellIt->mId; + } + if(remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + continue; + } + ++spellIt; } } - void ActiveSpells::removeEffects(const std::string &id) + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) + if(spell.mType != ESM::ActiveSpells::Type_Consumable) { - if (spell->first == id) + auto found = std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& existing) + { + return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId && spell.mSlot == existing.mSlot; + }); + if(found != mSpells.end()) { - spell->second.mEffects.clear(); - mSpellsChanged = true; + if(merge(found->mEffects, spell.mEffects)) + return; + auto params = *found; + mSpells.erase(found); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); } } + mSpells.emplace_back(spell); } - void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const + ActiveSpells::ActiveSpells() : mIterating(false) + {} + + ActiveSpells::TIterator ActiveSpells::begin() const { - for (TContainer::const_iterator it = begin(); it != end(); ++it) - { - for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end(); ++effectIt) - { - std::string name = it->second.mDisplayName; + return mSpells.begin(); + } - float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); - } - } + ActiveSpells::TIterator ActiveSpells::end() const + { + return mSpells.end(); } - void ActiveSpells::purgeAll(float chance, bool spellOnly) + bool ActiveSpells::isSpellActive(std::string_view id) const { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) + return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell) { - const std::string spellId = it->first; + return Misc::StringUtils::ciEqual(spell.mId, id); + }) != mSpells.end(); + } - // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. - if (spellOnly) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) - { - ++it; - continue; - } - } + void ActiveSpells::addSpell(const ActiveSpellParams& params) + { + mQueue.emplace_back(params); + } - if (Misc::Rng::roll0to99() < chance) - mSpells.erase(it++); - else - ++it; - } - mSpellsChanged = true; + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) + { + mQueue.emplace_back(ActiveSpellParams{spell, actor, true}); } - void ActiveSpells::purgeEffect(short effectId) + void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{*this}; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) + void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{*this}; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purge(int casterActorId) + bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, std::vector::iterator* currentEffect) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + bool removedCurrentSpell = false; + while(!mPurges.empty()) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) + auto predicate = mPurges.front(); + mPurges.pop(); + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if (it->second.mCasterActorId == casterActorId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; + bool isCurrentSpell = currentSpell && *currentSpell == spellIt; + std::visit([&] (auto&& variant) + { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if(variant(*spellIt)) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + if(isCurrentSpell) + { + *currentSpell = spellIt; + removedCurrentSpell = true; + } + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + } + else + ++spellIt; + } + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(variant(*spellIt, *effectIt)) + { + auto effect = *effectIt; + if(isCurrentSpell && currentEffect) + { + auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect); + if(effectIt <= *currentEffect) + distance--; + effectIt = spellIt->mEffects.erase(effectIt); + *currentEffect = spellIt->mEffects.begin() + distance; + } + else + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + } + else + ++effectIt; + } + ++spellIt; + } + }, predicate); } } - mSpellsChanged = true; + return removedCurrentSpell; } - void ActiveSpells::purgeCorprusDisease() + void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, std::string_view id) { - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + purge([=] (const ActiveSpellParams& params) { - bool hasCorprusEffect = false; - for (std::vector::iterator effectIt = iter->second.mEffects.begin(); - effectIt != iter->second.mEffects.end();++effectIt) - { - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } - - if (hasCorprusEffect) - { - mSpells.erase(iter++); - mSpellsChanged = true; - } - else - ++iter; - } + return params.mId == id; + }, ptr); } - void ActiveSpells::clear() + void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId) { - mSpells.clear(); - mSpellsChanged = true; + purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect) + { + return effect.mEffectId == effectId; + }, ptr); } - void ActiveSpells::writeState(ESM::ActiveSpells &state) const + void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) + purge([=] (const ActiveSpellParams& params) { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ESM::ActiveSpells::ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; + return params.mCasterActorId == casterActorId; + }, ptr); + } - state.mSpells.insert (std::make_pair(it->first, params)); - } + void ActiveSpells::clear(const MWWorld::Ptr& ptr) + { + mQueue.clear(); + purge([] (const ActiveSpellParams& params) { return true; }, ptr); } - void ActiveSpells::readState(const ESM::ActiveSpells &state) + void ActiveSpells::skipWorsenings(double hours) { - for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for(auto& spell : mSpells) { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - mSpells.insert (std::make_pair(it->first, params)); - mSpellsChanged = true; + if(spell.mWorsenings >= 0) + spell.mNextWorsening += hours; } } + + void ActiveSpells::writeState(ESM::ActiveSpells &state) const + { + for(const auto& spell : mSpells) + state.mSpells.emplace_back(spell.toEsm()); + for(const auto& spell : mQueue) + state.mQueue.emplace_back(spell.toEsm()); + } + + void ActiveSpells::readState(const ESM::ActiveSpells &state) + { + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + mSpells.emplace_back(ActiveSpellParams{spell}); + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) + mQueue.emplace_back(ActiveSpellParams{spell}); + } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 4d36c717e4..55b089dc52 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -1,41 +1,85 @@ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H -#include -#include +#include +#include +#include #include +#include +#include -#include -#include +#include #include "../mwworld/timestamp.hpp" +#include "../mwworld/ptr.hpp" #include "magiceffects.hpp" +#include "spellcasting.hpp" + +namespace ESM +{ + struct Enchantment; + struct Spell; +} namespace MWMechanics { /// \brief Lasting spell effects /// - /// \note The name of this class is slightly misleading, since it also handels lasting potion + /// \note The name of this class is slightly misleading, since it also handles lasting potion /// effects. class ActiveSpells { public: - typedef ESM::ActiveEffect ActiveEffect; - - struct ActiveSpellParams + using ActiveEffect = ESM::ActiveEffect; + class ActiveSpellParams { - std::vector mEffects; - MWWorld::TimeStamp mTimeStamp; - std::string mDisplayName; + std::string mId; + std::vector mEffects; + std::string mDisplayName; + int mCasterActorId; + int mSlot; + ESM::ActiveSpells::EffectType mType; + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; + + ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params); + + ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false); + + ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + + ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor); + + ESM::ActiveSpells::ActiveSpellParams toEsm() const; + + friend class ActiveSpells; + public: + ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + + const std::string& getId() const { return mId; } + + const std::vector& getEffects() const { return mEffects; } + std::vector& getEffects() { return mEffects; } - // The caster that inflicted this spell on us - int mCasterActorId; + ESM::ActiveSpells::EffectType getType() const { return mType; } + + int getCasterActorId() const { return mCasterActorId; } + + int getWorsenings() const { return mWorsenings; } + + const std::string& getDisplayName() const { return mDisplayName; } + + // Increments worsenings count and sets the next timestamp + void worsen(); + + bool shouldWorsen() const; + + void resetWorsenings(); }; - typedef std::multimap TContainer; - typedef TContainer::const_iterator TIterator; + typedef std::list::const_iterator TIterator; void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; @@ -44,24 +88,29 @@ namespace MWMechanics TIterator end() const; - void update(float duration) const; + void update(const MWWorld::Ptr& ptr, float duration); private: + using ParamsPredicate = std::function; + using EffectPredicate = std::function; + using Predicate = std::variant; - mutable TContainer mSpells; - mutable MagicEffects mEffects; - mutable bool mSpellsChanged; + struct IterationGuard + { + ActiveSpells& mActiveSpells; - void rebuildEffects() const; + IterationGuard(ActiveSpells& spells); + ~IterationGuard(); + }; - /// Add any effects that are in "from" and not in "addTo" to "addTo" - void mergeEffects(std::vector& addTo, const std::vector& from); + std::list mSpells; + std::vector mQueue; + std::queue mPurges; + bool mIterating; - double timeToExpire (const TIterator& iterator) const; - ///< Returns time (in in-game hours) until the spell pointed to by \a iterator - /// expires. + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); - const TContainer& getActiveSpells() const; + bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); public: @@ -71,40 +120,31 @@ namespace MWMechanics /// /// \brief addSpell /// \param id ID for stacking purposes. - /// \param stack If false, the spell is not added if one with the same ID exists already. - /// \param effects - /// \param displayName Name for display in magic menu. /// - void addSpell (const std::string& id, bool stack, std::vector effects, - const std::string& displayName, int casterActorId); + void addSpell (const ActiveSpellParams& params); + + /// Bypasses resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects (const std::string& id); + void removeEffects (const MWWorld::Ptr& ptr, std::string_view id); /// Remove all active effects with this effect id - void purgeEffect (short effectId); - - /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); + void purgeEffect (const MWWorld::Ptr& ptr, short effectId); - /// Remove all active effects, if roll succeeds (for each effect) - void purgeAll(float chance, bool spellOnly = false); + void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); + void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); - /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId - void purge (int casterActorId); + /// Remove all effects that were cast by \a casterActorId + void purge (const MWWorld::Ptr& ptr, int casterActorId); /// Remove all spells - void clear(); + void clear(const MWWorld::Ptr& ptr); - bool isSpellActive (const std::string& id) const; + bool isSpellActive (std::string_view id) const; ///< case insensitive - void purgeCorprusDisease(); - - const MagicEffects& getMagicEffects() const; - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - + void skipWorsenings(double hours); }; } diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp deleted file mode 100644 index a5c55633ac..0000000000 --- a/apps/openmw/mwmechanics/actor.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "actor.hpp" - -#include "character.hpp" - -namespace MWMechanics -{ - Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) - { - mCharacterController.reset(new CharacterController(ptr, animation)); - } - - void Actor::updatePtr(const MWWorld::Ptr &newPtr) - { - mCharacterController->updatePtr(newPtr); - } - - CharacterController* Actor::getCharacterController() - { - return mCharacterController.get(); - } - - int Actor::getGreetingTimer() const - { - return mGreetingTimer; - } - - void Actor::setGreetingTimer(int timer) - { - mGreetingTimer = timer; - } - - float Actor::getAngleToPlayer() const - { - return mTargetAngleRadians; - } - - void Actor::setAngleToPlayer(float angle) - { - mTargetAngleRadians = angle; - } - - GreetingState Actor::getGreetingState() const - { - return mGreetingState; - } - - void Actor::setGreetingState(GreetingState state) - { - mGreetingState = state; - } - - bool Actor::isTurningToPlayer() const - { - return mIsTurningToPlayer; - } - - void Actor::setTurningToPlayer(bool turning) - { - mIsTurningToPlayer = turning; - } -} diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index 287ca420f4..1e257f613d 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -3,7 +3,13 @@ #include -#include "../mwmechanics/actorutil.hpp" +#include "character.hpp" +#include "greetingstate.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include namespace MWRender { @@ -16,37 +22,51 @@ namespace MWWorld namespace MWMechanics { - class CharacterController; - /// @brief Holds temporary state for an actor that will be discarded when the actor leaves the scene. class Actor { public: - Actor(const MWWorld::Ptr& ptr, MWRender::Animation* animation); + Actor(const MWWorld::Ptr& ptr, MWRender::Animation* animation) + : mCharacterController(ptr, animation) + , mPositionAdjusted(false) + {} + + const MWWorld::Ptr& getPtr() const { return mCharacterController.getPtr(); } /// Notify this actor of its new base object Ptr, use when the object changed cells - void updatePtr(const MWWorld::Ptr& newPtr); + void updatePtr(const MWWorld::Ptr& newPtr) { mCharacterController.updatePtr(newPtr); } + + CharacterController& getCharacterController() { return mCharacterController; } + const CharacterController& getCharacterController() const { return mCharacterController; } + + int getGreetingTimer() const { return mGreetingTimer; } + void setGreetingTimer(int timer) { mGreetingTimer = timer; } - CharacterController* getCharacterController(); + float getAngleToPlayer() const { return mTargetAngleRadians; } + void setAngleToPlayer(float angle) { mTargetAngleRadians = angle; } - int getGreetingTimer() const; - void setGreetingTimer(int timer); + GreetingState getGreetingState() const { return mGreetingState; } + void setGreetingState(GreetingState state) { mGreetingState = state; } - float getAngleToPlayer() const; - void setAngleToPlayer(float angle); + bool isTurningToPlayer() const { return mIsTurningToPlayer; } + void setTurningToPlayer(bool turning) { mIsTurningToPlayer = turning; } - GreetingState getGreetingState() const; - void setGreetingState(GreetingState state); + Misc::TimerStatus updateEngageCombatTimer(float duration) + { + return mEngageCombat.update(duration, MWBase::Environment::get().getWorld()->getPrng()); + } - bool isTurningToPlayer() const; - void setTurningToPlayer(bool turning); + void setPositionAdjusted(bool adjusted) { mPositionAdjusted = adjusted; } + bool getPositionAdjusted() const { return mPositionAdjusted; } private: - std::unique_ptr mCharacterController; + CharacterController mCharacterController; int mGreetingTimer{0}; float mTargetAngleRadians{0.f}; GreetingState mGreetingState{Greet_None}; bool mIsTurningToPlayer{false}; + Misc::DeviatingPeriodicTimer mEngageCombat{1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng())}; + bool mPositionAdjusted; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 8f3675ef7e..a5ab0a6ae6 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1,13 +1,16 @@ #include "actors.hpp" -#include -#include +#include + +#include +#include #include #include #include #include #include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -22,6 +25,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/aibreathe.hpp" @@ -40,9 +44,7 @@ #include "aiwander.hpp" #include "actor.hpp" #include "summoning.hpp" -#include "combat.hpp" #include "actorutil.hpp" -#include "tickableeffects.hpp" namespace { @@ -53,209 +55,118 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -int getBoundItemSlot (const std::string& itemId) +bool isCommanded(const MWWorld::Ptr& actor) { - static std::map boundItemsMap; - if (boundItemsMap.empty()) + const auto& actorClass = actor.getClass(); + const auto& stats = actorClass.getCreatureStats(actor); + const bool isActorNpc = actorClass.isNpc(); + const auto level = stats.getLevel(); + for (const auto& params : stats.getActiveSpells()) { - std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; + for (const auto& effect : params.getEffects()) + { + if (((effect.mEffectId == ESM::MagicEffect::CommandHumanoid && isActorNpc) + || (effect.mEffectId == ESM::MagicEffect::CommandCreature && !isActorNpc)) + && effect.mMagnitude >= level) + return true; + } } - - int slot = MWWorld::InventoryStore::Slot_CarriedRight; - std::map::iterator it = boundItemsMap.find(itemId); - if (it != boundItemsMap.end()) - slot = it->second; - - return slot; + return false; } -class CheckActorCommanded : public MWMechanics::EffectSourceVisitor -{ - MWWorld::Ptr mActor; -public: - bool mCommanded; - - CheckActorCommanded(const MWWorld::Ptr& actor) - : mActor(actor) - , mCommanded(false){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) - || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) - && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) - mCommanded = true; - } -}; - // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { - CheckActorCommanded check(actor); - MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - stats.getActiveSpells().visitEffectSources(check); + if (isCommanded(actor)) + return; - bool hasCommandPackage = false; + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - auto it = stats.getAiSequence().begin(); - for (; it != stats.getAiSequence().end(); ++it) + stats.getAiSequence().erasePackageIf([](auto& entry) { - if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Follow && - static_cast(it->get())->isCommanded()) + if (entry->getTypeId() == MWMechanics::AiPackageTypeId::Follow && + static_cast(entry.get())->isCommanded()) { - hasCommandPackage = true; - break; + return true; } - } - - if (!check.mCommanded && hasCommandPackage) - stats.getAiSequence().erase(it); + return false; + }); } -void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) +std::pair getRestorationPerHourOfSleep(const MWWorld::Ptr& ptr) { - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); - float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); - health = 0.1f * endurance; + const float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified(); + const float health = 0.1f * endurance; - float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat (); - magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); -} + static const float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat(); + const float magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); + return {health, magicka}; } -namespace MWMechanics +template +void forEachFollowingPackage(const std::list& actors, const MWWorld::Ptr& actorPtr, const MWWorld::Ptr& player, T&& func) { - static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player - static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement - static const float DECELERATE_DISTANCE = 512.f; - - class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor + for (const MWMechanics::Actor& actor : actors) { - public: - float mRemainingTime; + const MWWorld::Ptr &iteratedActor = actor.getPtr(); + if (iteratedActor == player || iteratedActor == actorPtr) + continue; - GetStuntedMagickaDuration(const MWWorld::Ptr& actor) - : mRemainingTime(0.f){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mRemainingTime == -1) return; - - if (key.mId == ESM::MagicEffect::StuntedMagicka) - { - if (totalTime == -1) - { - mRemainingTime = -1; - return; - } - - if (remainingTime > mRemainingTime) - mRemainingTime = remainingTime; - } - } - }; - - class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor - { - std::string mSpellId; - - public: - GetCurrentMagnitudes(const std::string& spellId) - : mSpellId(spellId) - { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (magnitude <= 0) - return; - - if (sourceId != mSpellId) - return; - - mMagnitudes.emplace_back(key, magnitude); - } - - std::vector> mMagnitudes; - }; - - class GetCorprusSpells : public MWMechanics::EffectSourceVisitor - { + const MWMechanics::CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); + if (stats.isDead()) + continue; - public: - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override + // An actor counts as following if AiFollow is the current AiPackage, + // or there are only Combat and Wander packages before the AiFollow package + for (const auto& package : stats.getAiSequence()) { - if (key.mId != ESM::MagicEffect::Corprus) - return; - - mSpells.push_back(sourceId); + if (!func(actor, package)) + break; } + } +} - std::vector mSpells; - }; - - class SoulTrap : public MWMechanics::EffectSourceVisitor +float getStuntedMagickaDuration(const MWWorld::Ptr& actor) +{ + float remainingTime = 0.f; + for(const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { - MWWorld::Ptr mCreature; - MWWorld::Ptr mActor; - bool mTrapped; - public: - SoulTrap(const MWWorld::Ptr& trappedCreature) - : mCreature(trappedCreature) - , mTrapped(false) + for(const auto& effect : params.getEffects()) { + if(effect.mEffectId == ESM::MagicEffect::StuntedMagicka) + { + if(effect.mDuration == -1.f) + return -1.f; + remainingTime = std::max(remainingTime, effect.mTimeLeft); + } } + } + return remainingTime; +} - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mTrapped) - return; - if (key.mId != ESM::MagicEffect::Soultrap) - return; - if (magnitude <= 0) - return; - - MWBase::World* world = MWBase::Environment::get().getWorld(); - - MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); +void soulTrap(const MWWorld::Ptr& creature) +{ + const auto& stats = creature.getClass().getCreatureStats(creature); + if(!stats.getMagicEffects().get(ESM::MagicEffect::Soultrap).getMagnitude()) + return; + const int creatureSoulValue = creature.get()->mBase->mData.mSoul; + if (creatureSoulValue == 0) + return; + MWBase::World* const world = MWBase::Environment::get().getWorld(); + static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(effect.mEffectId != ESM::MagicEffect::Soultrap || effect.mMagnitude <= 0.f) + continue; + MWWorld::Ptr caster = world->searchPtrViaActorId(params.getCasterActorId()); if (caster.isEmpty() || !caster.getClass().isActor()) - return; - - static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); - - int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; - if (creatureSoulValue == 0) - return; + continue; // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); @@ -280,176 +191,188 @@ namespace MWMechanics } if (gem == container.end()) - return; + continue; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem, caster); - gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); + gem->getCellRef().setSoul(creature.getCellRef().getRefId()); // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); - mTrapped = true; - - if (caster == getPlayer()) + if (caster == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Soul_Trap"); - if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", mCreature.getRefData().getPosition().asVec3()); + const ESM::Static* const fx = world->getStore().get().search("VFX_Soul_Trap"); + if (fx != nullptr) + { + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + world->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), + "", creature.getRefData().getPosition().asVec3()); + } - MWBase::Environment::get().getSoundManager()->playSound3D( - mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f - ); + MWBase::Environment::get().getSoundManager()->playSound3D(creature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f); + return; //remove to get vanilla behaviour } - }; - - void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; - - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); - - if (actor != MWMechanics::getPlayer()) - return; - - MWWorld::Ptr newItem = *store.getSlot(slot); - - if (newItem.isEmpty() || boundPtr != newItem) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState_Weapon); - - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } +} - void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) +void removeTemporaryEffects(const MWWorld::Ptr& ptr) +{ + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purge([] (const auto& spell) { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); + return spell.getType() == ESM::ActiveSpells::Type_Consumable || spell.getType() == ESM::ActiveSpells::Type_Temporary; + }, ptr); +} - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); +} - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); +namespace MWMechanics +{ + static constexpr int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static constexpr int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static constexpr int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement + static constexpr float DECELERATE_DISTANCE = 512.f; - if (actor != MWMechanics::getPlayer()) + namespace + { + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { - store.remove(itemId, 1, actor); + const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); + return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; + } - // Equip a replacement - if (!wasEquipped) + void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, bool inCombatOrPursue) + { + const auto& actorRefData = actor.getRefData(); + if (!actorRefData.getBaseNode()) return; - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) + if (targetActor.getClass().getCreatureStats(targetActor).isDead()) return; - if (actor.getClass().getCreatureStats(actor).isDead()) - return; + static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore() + .get().find("fMaxHeadTrackDistance")->mValue.getFloat(); + static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore() + .get().find("fInteriorHeadTrackMult")->mValue.getFloat(); + float maxDistance = fMaxHeadTrackDistance; + const ESM::Cell* currentCell = actor.getCell()->getCell(); + if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) + maxDistance *= fInteriorHeadTrackMult; - if (!actor.getClass().hasInventoryStore(actor)) - return; + const osg::Vec3f actor1Pos(actorRefData.getPosition().asVec3()); + const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); + const float sqrDist = (actor1Pos - actor2Pos).length2(); - if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) return; - actor.getClass().getInventoryStore(actor).autoEquip(actor); - - return; + // stop tracking when target is behind the actor + osg::Vec3f actorDirection = actorRefData.getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); + osg::Vec3f targetDirection(actor2Pos - actor1Pos); + actorDirection.z() = 0; + targetDirection.z() = 0; + if ((actorDirection * targetDirection > 0 || inCombatOrPursue) + // check LOS and awareness last as it's the most expensive function + && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) + { + sqrHeadTrackDistance = sqrDist; + headTrackTarget = targetActor; + } } - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player.getPreviousItem(itemId); - player.erasePreviousItem(itemId); - - if (!prevItemId.empty()) + void updateHeadTracking(const MWWorld::Ptr& ptr, const std::list& actors, bool isPlayer, CharacterController& ctrl) { - // Find previous item (or its replacement) by id. - // we should equip previous item only if expired bound item was equipped. - MWWorld::Ptr item = store.findReplacement(prevItemId); - if (!item.isEmpty() && wasEquipped) + float sqrHeadTrackDistance = std::numeric_limits::max(); + MWWorld::Ptr headTrackTarget; + + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const bool firstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); + + // 1. Unconsious actor can not track target + // 2. Actors in combat and pursue mode do not bother to headtrack anyone except their target + // 3. Player character does not use headtracking in the 1st-person view + if (!stats.getKnockedDown() && !firstPersonPlayer) { - MWWorld::ActionEquip action(item); - action.execute(actor); + bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().isInPursuit(); + if (inCombatOrPursue) + { + auto activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); + if (!activePackageTarget.isEmpty()) + { + // Track the specified target of package. + updateHeadTracking(ptr, activePackageTarget, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); + } + } + else + { + // Find something nearby. + for (const Actor& otherActor : actors) + { + if (otherActor.getPtr() == ptr) + continue; + + updateHeadTracking(ptr, otherActor.getPtr(), headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); + } + } } + + ctrl.setHeadTrackTarget(headTrackTarget); } - store.remove(itemId, 1, actor); + void updateLuaControls(const MWWorld::Ptr& ptr, bool isPlayer, MWBase::LuaManager::ActorControls& controls) + { + Movement& mov = ptr.getClass().getMovementSettings(ptr); + CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float speedFactor = isPlayer ? 1.f : mov.mSpeedFactor; + const osg::Vec2f movement = osg::Vec2f(mov.mPosition[0], mov.mPosition[1]) * speedFactor; + const float rotationX = mov.mRotation[0]; + const float rotationZ = mov.mRotation[2]; + const bool jump = mov.mPosition[2] == 1; + const bool runFlag = stats.getMovementFlag(MWMechanics::CreatureStats::Flag_Run); + const bool attackingOrSpell = stats.getAttackingOrSpell(); + if (controls.mChanged) + { + mov.mPosition[0] = controls.mSideMovement; + mov.mPosition[1] = controls.mMovement; + mov.mPosition[2] = controls.mJump ? 1 : 0; + mov.mRotation[0] = controls.mPitchChange; + mov.mRotation[1] = 0; + mov.mRotation[2] = controls.mYawChange; + mov.mSpeedFactor = osg::Vec2(controls.mMovement, controls.mSideMovement).length(); + stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, controls.mRun); + stats.setAttackingOrSpell((controls.mUse & 1) == 1); + controls.mChanged = false; + } + controls.mSideMovement = movement.x(); + controls.mMovement = movement.y(); + controls.mPitchChange = rotationX; + controls.mYawChange = rotationZ; + controls.mJump = jump; + controls.mRun = runFlag; + controls.mUse = attackingOrSpell ? controls.mUse | 1 : controls.mUse & ~1; + } } - void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) + void Actors::updateActor(const MWWorld::Ptr& ptr, float duration) const { // magic effects - adjustMagicEffects (ptr); - if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) - calculateDynamicStats (ptr); + adjustMagicEffects (ptr, duration); - calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } - void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance) - { - if (!actor.getRefData().getBaseNode()) - return; - - if (targetActor.getClass().getCreatureStats(targetActor).isDead()) - return; - - static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get() - .find("fMaxHeadTrackDistance")->mValue.getFloat(); - static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get() - .find("fInteriorHeadTrackMult")->mValue.getFloat(); - float maxDistance = fMaxHeadTrackDistance; - const ESM::Cell* currentCell = actor.getCell()->getCell(); - if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) - maxDistance *= fInteriorHeadTrackMult; - - const osg::Vec3f actor1Pos(actor.getRefData().getPosition().asVec3()); - const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); - float sqrDist = (actor1Pos - actor2Pos).length2(); - - if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance)) - return; - - // stop tracking when target is behind the actor - osg::Vec3f actorDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - osg::Vec3f targetDirection(actor2Pos - actor1Pos); - actorDirection.z() = 0; - targetDirection.z() = 0; - if (actorDirection * targetDirection > 0 - && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) - { - sqrHeadTrackDistance = sqrDist; - headTrackTarget = targetActor; - } - } - - void Actors::playIdleDialogue(const MWWorld::Ptr& actor) + void Actors::playIdleDialogue(const MWWorld::Ptr& actor) const { if (!actor.getClass().isActor() || actor == getPlayer() || MWBase::Environment::get().getSoundManager()->sayActive(actor)) return; const CreatureStats &stats = actor.getClass().getCreatureStats(actor); - if (stats.getAiSetting(CreatureStats::AI_Hello).getModified() == 0) + if (stats.getAiSetting(AiSetting::Hello).getModified() == 0) return; const MWMechanics::AiSequence& seq = stats.getAiSequence(); @@ -458,7 +381,7 @@ namespace MWMechanics const osg::Vec3f playerPos(getPlayer().getRefData().getPosition().asVec3()); const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - MWBase::World* world = MWBase::Environment::get().getWorld(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); if (world->isSwimming(actor) || (playerPos - actorPos).length2() >= 3000 * 3000) return; @@ -466,28 +389,29 @@ namespace MWMechanics // We chose to use the chance MW would have when run at 60 FPS with the default value of the GMST. const float delta = MWBase::Environment::get().getFrameDuration() * 6.f; static const float fVoiceIdleOdds = world->getStore().get().find("fVoiceIdleOdds")->mValue.getFloat(); - if (Misc::Rng::rollProbability() * 10000.f < fVoiceIdleOdds * delta && world->getLOS(getPlayer(), actor)) + if (Misc::Rng::rollProbability(world->getPrng()) * 10000.f < fVoiceIdleOdds * delta && world->getLOS(getPlayer(), actor)) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } - void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) + void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) const { if (mSmoothMovement) return; - CreatureStats &stats = actor.getClass().getCreatureStats(actor); - MWMechanics::AiSequence& seq = stats.getAiSequence(); + const auto& actorClass = actor.getClass(); + const CreatureStats &stats = actorClass.getCreatureStats(actor); + const MWMechanics::AiSequence& seq = stats.getAiSequence(); if (!seq.isEmpty() && seq.getActivePackage().useVariableSpeed()) { - osg::Vec3f targetPos = seq.getActivePackage().getDestination(); - osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - float distance = (targetPos - actorPos).length(); + const osg::Vec3f targetPos = seq.getActivePackage().getDestination(); + const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + const float distance = (targetPos - actorPos).length(); if (distance < DECELERATE_DISTANCE) { - float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); - auto& movement = actor.getClass().getMovementSettings(actor); + const float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); + auto& movement = actorClass.getMovementSettings(actor); movement.mPosition[0] *= speedCoef; movement.mPosition[1] *= speedCoef; } @@ -496,11 +420,12 @@ namespace MWMechanics void Actors::updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly) { - if (!actor.getClass().isActor() || actor == getPlayer()) + const auto& actorClass = actor.getClass(); + if (!actorClass.isActor() || actor == getPlayer()) return; - CreatureStats &stats = actor.getClass().getCreatureStats(actor); - const MWMechanics::AiSequence& seq = stats.getAiSequence(); + const CreatureStats& actorStats = actorClass.getCreatureStats(actor); + const MWMechanics::AiSequence& seq = actorStats.getAiSequence(); const auto packageId = seq.getTypeId(); if (seq.isInCombat() || @@ -513,10 +438,10 @@ namespace MWMechanics return; } - MWWorld::Ptr player = getPlayer(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - osg::Vec3f dir = playerPos - actorPos; + const MWWorld::Ptr player = getPlayer(); + const osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + const osg::Vec3f dir = playerPos - actorPos; if (actorState.isTurningToPlayer()) { @@ -527,7 +452,7 @@ namespace MWMechanics { actorState.setTurningToPlayer(false); // An original engine launches an endless idle2 when an actor greets player. - playAnimationGroup (actor, "idle2", 0, std::numeric_limits::max(), false); + playAnimationGroup(actor, "idle2", 0, std::numeric_limits::max(), false); } } @@ -535,17 +460,18 @@ namespace MWMechanics return; // Play a random voice greeting if the player gets too close - static int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() + static const int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() .get().find("iGreetDistanceMultiplier")->mValue.getInteger(); - float helloDistance = static_cast(stats.getAiSetting(CreatureStats::AI_Hello).getModified() * iGreetDistanceMultiplier); + const float helloDistance = static_cast(actorStats.getAiSetting(AiSetting::Hello).getModified() * iGreetDistanceMultiplier); + const auto& playerStats = player.getClass().getCreatureStats(player); int greetingTimer = actorState.getGreetingTimer(); GreetingState greetingState = actorState.getGreetingState(); if (greetingState == Greet_None) { - if ((playerPos - actorPos).length2() <= helloDistance*helloDistance && - !player.getClass().getCreatureStats(player).isDead() && !actor.getClass().getCreatureStats(actor).isParalyzed() + if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && + !playerStats.isDead() && !actorStats.isParalyzed() && MWBase::Environment::get().getWorld()->getLOS(player, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) greetingTimer++; @@ -562,7 +488,8 @@ namespace MWMechanics { greetingTimer++; - if (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor)) + if (!actorStats.getMovementFlag(CreatureStats::Flag_ForceJump) && !actorStats.getMovementFlag(CreatureStats::Flag_ForceSneak) + && (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor))) turnActorToFacePlayer(actor, actorState, dir); if (greetingTimer >= GREETING_COOLDOWN) @@ -575,7 +502,7 @@ namespace MWMechanics if (greetingState == Greet_Done) { float resetDist = 2 * helloDistance; - if ((playerPos - actorPos).length2() >= resetDist*resetDist) + if ((playerPos - actorPos).length2() >= resetDist * resetDist) greetingState = Greet_None; } @@ -583,10 +510,11 @@ namespace MWMechanics actorState.setGreetingState(greetingState); } - void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) + void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) const { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + auto& movementSettings = actor.getClass().getMovementSettings(actor); + movementSettings.mPosition[1] = 0; + movementSettings.mPosition[0] = 0; if (!actorState.isTurningToPlayer()) { @@ -600,7 +528,25 @@ namespace MWMechanics } } - void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer) + void Actors::stopCombat(const MWWorld::Ptr& ptr) const + { + auto& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector targets; + if(ai.getCombatTargets(targets)) + { + std::set allySet; + getActorsSidingWith(ptr, allySet); + allySet.insert(ptr); + std::vector allies(allySet.begin(), allySet.end()); + for(const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(targets); + for(const auto& target : targets) + target.getClass().getCreatureStats(target).getAiSequence().stopCombat(allies); + } + } + + void Actors::engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, + std::map>& cachedAllies, bool againstPlayer) const { // No combat for totally static creatures if (!actor1.getClass().isMobile(actor1)) @@ -616,9 +562,9 @@ namespace MWMechanics const osg::Vec3f actor1Pos(actor1.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(actor2.getRefData().getPosition().asVec3()); - float sqrDist = (actor1Pos - actor2Pos).length2(); + const float sqrDist = (actor1Pos - actor2Pos).length2(); - if (sqrDist > mActorsProcessingRange*mActorsProcessingRange) + if (sqrDist > mActorsProcessingRange * mActorsProcessingRange) return; // If this is set to true, actor1 will start combat with actor2 if the awareness check at the end of the method returns true @@ -630,15 +576,16 @@ namespace MWMechanics getActorsSidingWith(actor1, allies1, cachedAllies); + 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 actor2 - for (const MWWorld::Ptr &ally : allies1) + for (const MWWorld::Ptr& ally : allies1) { if (creatureStats1.getAiSequence().isInCombat(ally)) continue; if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId())) { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, 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()); @@ -660,21 +607,21 @@ namespace MWMechanics if (!aggressive && !isPlayerFollowerOrEscorter) { // Check that actor2 is in combat with actor1 - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) + if (creatureStats2.getAiSequence().isInCombat(actor1)) { std::set allies2; getActorsSidingWith(actor2, allies2, cachedAllies); // Check that an ally of actor2 is also in combat with actor1 - for (const MWWorld::Ptr &ally2 : allies2) + for (const MWWorld::Ptr& ally2 : allies2) { if (ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2); // Also have actor1's allies start combat for (const MWWorld::Ptr& ally1 : allies1) - MWBase::Environment::get().getMechanicsManager()->startCombat(ally1, actor2); + mechanicsManager->startCombat(ally1, actor2); return; } } @@ -689,13 +636,13 @@ namespace MWMechanics static const bool followersAttackOnSight = Settings::Manager::getBool("followers attack on sight", "Game"); if (!aggressive && isPlayerFollowerOrEscorter && followersAttackOnSight) { - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) + if (creatureStats2.getAiSequence().isInCombat(actor1)) aggressive = true; else { - for (const MWWorld::Ptr &ally : allies1) + for (const MWWorld::Ptr& ally : allies1) { - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(ally)) + if (creatureStats2.getAiSequence().isInCombat(ally)) { aggressive = true; break; @@ -712,15 +659,16 @@ namespace MWMechanics // Player followers and escorters with high fight should not initiate combat with the player or with // other player followers or escorters if (!isPlayerFollowerOrEscorter) - aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); + aggressive = mechanicsManager->isAggressive(actor1, actor2); } } // Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter + const auto world = MWBase::Environment::get().getWorld(); if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc() && creatureStats2.getAiSequence().isInCombat()) { // Check if the creature is too far - static const float fAlarmRadius = MWBase::Environment::get().getWorld()->getStore().get().find("fAlarmRadius")->mValue.getFloat(); + static const float fAlarmRadius = world->getStore().get().find("fAlarmRadius")->mValue.getFloat(); if (sqrDist > fAlarmRadius * fAlarmRadius) return; @@ -743,57 +691,71 @@ namespace MWMechanics // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, start combat with actor2. if (aggressive) { - bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); + bool LOS = world->getLOS(actor1, actor2) && + mechanicsManager->awarenessCheck(actor2, actor1); if (LOS) - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2); } } - void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) + void Actors::adjustMagicEffects(const MWWorld::Ptr& creature, float duration) const { - CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); - if (creatureStats.isDeathAnimationFinished()) - return; + CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + const bool wasDead = creatureStats.isDead(); - MagicEffects now = creatureStats.getSpells().getMagicEffects(); + creatureStats.getActiveSpells().update(creature, duration); - if (creature.getClass().hasInventoryStore(creature)) + if (!wasDead && creatureStats.isDead()) { - MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); - now += store.getMagicEffects(); - } + // The actor was killed by a magic effect. Figure out if the player was responsible for it. + const ActiveSpells& spells = creatureStats.getActiveSpells(); + const MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); - now += creatureStats.getActiveSpells().getMagicEffects(); + for (const ActiveSpells::ActiveSpellParams& spell : spells) + { + bool actorKilled = false; - creatureStats.modifyMagicEffects(now); - } + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); + for (const auto& effect : spell.getEffects()) + { + static const std::array damageEffects{ + ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, + ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth + }; - void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) - { - CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); + const bool isDamageEffect = std::find(damageEffects.begin(), damageEffects.end(), effect.mEffectId) != damageEffects.end(); - float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); + if (isDamageEffect) + { + if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); + actorKilled = true; + break; + } + } + } - float base = 1.f; - if (ptr == getPlayer()) - base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); - else - base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + if (actorKilled) + break; + } + } - double magickaFactor = base + - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + // updateSummons assumes the actor belongs to a cell. + // This assumption isn't always valid for the player character. + if (!creature.isInCell()) + return; - DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); - float currentToBaseRatio = (magicka.getCurrent() / magicka.getBase()); - magicka.setModified(magicka.getModified() + diff, 0); - magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); - creatureStats.setMagicka(magicka); + if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) + updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } - void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) + void Actors::restoreDynamicStats(const MWWorld::Ptr& ptr, double hours, bool sleep) const { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); if (stats.isDead()) @@ -803,34 +765,29 @@ namespace MWMechanics if (sleep) { - float health, magicka; - getRestorationPerHourOfSleep(ptr, health, magicka); + const auto [health, magicka] = getRestorationPerHourOfSleep(ptr); DynamicStat stat = stats.getHealth(); stat.setCurrent(stat.getCurrent() + health * hours); stats.setHealth(stat); double restoreHours = hours; - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; + const bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; if (stunted) { // Stunted Magicka effect should be taken into account. - GetStuntedMagickaDuration visitor(ptr); - stats.getActiveSpells().visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); + float remainingTime = getStuntedMagickaDuration(ptr); // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. - if (visitor.mRemainingTime > 0) + if (remainingTime > 0) { double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if(timeScale == 0.0) timeScale = 1; - restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); + restoreHours = std::max(0.0, hours - remainingTime * timeScale / 3600.f); } - else if (visitor.mRemainingTime == -1) + else if (remainingTime == -1) restoreHours = 0; } @@ -849,24 +806,24 @@ namespace MWMechanics return; // Restore fatigue - float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); - float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); - float fEndFatigueMult = settings.find("fEndFatigueMult")->mValue.getFloat (); + static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); + static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); + static const float fEndFatigueMult = settings.find("fEndFatigueMult")->mValue.getFloat (); - float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); + const float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); if (normalizedEncumbrance > 1) normalizedEncumbrance = 1; - float x = fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance); - x *= fEndFatigueMult * endurance; + const float x = (fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance)) + * (fEndFatigueMult * endurance); fatigue.setCurrent (fatigue.getCurrent() + 3600 * x * hours); stats.setFatigue (fatigue); } - void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration) + void Actors::calculateRestoration(const MWWorld::Ptr& ptr, float duration) const { if (ptr.getClass().getCreatureStats(ptr).isDead()) return; @@ -880,448 +837,45 @@ namespace MWMechanics return; // Restore fatigue - float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); + const float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); - float x = fFatigueReturnBase + fFatigueReturnMult * endurance; + const float x = fFatigueReturnBase + fFatigueReturnMult * endurance; fatigue.setCurrent (fatigue.getCurrent() + duration * x); stats.setFatigue (fatigue); } - class ExpiryVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - float mDuration; - - public: - ExpiryVisitor(const MWWorld::Ptr& actor, float duration) - : mActor(actor), mDuration(duration) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override - { - if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) - { - CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) - creatureStats.getMagicEffects().add(key, -magnitude); - } - } - }; - - void Actors::applyCureEffects(const MWWorld::Ptr& actor) - { - CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); - const MagicEffects &effects = creatureStats.getMagicEffects(); - - if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); - } - else if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); - } - else if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeCommonDisease(); - } - else if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeBlightDisease(); - } - else if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeCorprusDisease(); - creatureStats.getSpells().purgeCorprusDisease(); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); - } - else if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) - { - creatureStats.getSpells().purgeCurses(); - } - } - - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - const MagicEffects &effects = creatureStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - applyCureEffects(ptr); - - bool wasDead = creatureStats.isDead(); - - if (duration > 0) - { - // Apply correct magnitude for tickable effects that have just expired, - // in case duration > remaining time of effect. - // One case where this will happen is when the player uses the rest/wait command - // while there is a tickable effect active that should expire before the end of the rest/wait. - ExpiryVisitor visitor(ptr, duration); - creatureStats.getActiveSpells().visitEffectSources(visitor); - - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - // tickable effects (i.e. effects having a lasting impact after expiry) - effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); - - // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities - if (it->second.getMagnitude() > 0) - { - CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) - { - creatureStats.getSpells().purgeEffect(it->first.mId); - creatureStats.getActiveSpells().purgeEffect(it->first.mId); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); - } - } - } - } - - // purge levitate effect if levitation is disabled - // check only modifier, because base value can be setted from SetFlying console command. - if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) - { - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); - - if (ptr == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - } - } - - // dynamic stats - for (int i = 0; i < 3; ++i) - { - DynamicStat stat = creatureStats.getDynamic(i); - float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); - float drain = 0.f; - if (!godmode) - drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); - stat.setCurrentModifier(fortify - drain, - // Magicka can be decreased below zero due to a fortify effect wearing off - // Fatigue can be decreased below zero meaning the actor will be knocked out - i == 1 || i == 2); - - creatureStats.setDynamic(i, stat); - } - - // attributes - for(int i = 0;i < ESM::Attribute::Length;++i) - { - AttributeValue stat = creatureStats.getAttribute(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); - } - stat.setModifier(static_cast(fortify - drain - absorb)); - - creatureStats.setAttribute(i, stat); - } - - if (creatureStats.needToRecalcDynamicStats()) - calculateDynamicStats(ptr); - - if (ptr == getPlayer()) - { - GetCorprusSpells getCorprusSpellsVisitor; - creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); - creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); - std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; - std::vector corprusSpellsToRemove; - - for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) - { - if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) - { - // Corprus effect expired, remove entry and restore stats. - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); - corprusSpellsToRemove.push_back(it->first); - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - continue; - } - - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) - { - it->second.mNextWorsening += CorprusStats::sWorseningPeriod; - GetCurrentMagnitudes getMagnitudesVisitor (it->first); - creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); - creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); - for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) - { - if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - attr.damage(-effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - it->second.mWorsenings[effectMagnitude.first.mArg] = 0; - } - else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - int currentDamage = attr.getDamage(); - if (currentDamage >= 0) - it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); - - it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; - attr.damage(effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - } - } - - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - } - } - - for (std::string& oldCorprusSpell : corprusSpellsToRemove) - { - creatureStats.removeCorprusSpell(oldCorprusSpell); - } - - for (std::string& newCorprusSpell : corprusSpells) - { - CorprusStats corprus; - for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; - - creatureStats.addCorprusSpell(newCorprusSpell, corprus); - } - } - - // AI setting modifiers - int creature = !ptr.getClass().isNpc(); - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Humanoid) - creature = false; - // Note: the Creature variants only work on normal creatures, not on daedra or undead creatures. - if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); - - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - - if (!wasDead && creatureStats.isDead()) - { - // The actor was killed by a magic effect. Figure out if the player was responsible for it. - const ActiveSpells& spells = creatureStats.getActiveSpells(); - MWWorld::Ptr player = getPlayer(); - std::set playerFollowers; - getActorsSidingWith(player, playerFollowers); - - for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - bool actorKilled = false; - - const ActiveSpells::ActiveSpellParams& spell = it->second; - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - for (std::vector::const_iterator effectIt = spell.mEffects.begin(); - effectIt != spell.mEffects.end(); ++effectIt) - { - int effectId = effectIt->mEffectId; - bool isDamageEffect = false; - - int damageEffects[] = { - ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, - ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth - }; - - for (unsigned int i=0; iactorKilled(ptr, player); - actorKilled = true; - break; - } - } - } - - if (actorKilled) - break; - } - } - - // TODO: dirty flag for magic effects to avoid some unnecessary work below? - - // any value of calm > 0 will stop the actor from fighting - if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) - || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) - creatureStats.getAiSequence().stopCombat(); - - // Update bound effects - // Note: in vanilla MW multiple bound items of the same type can be created by different spells. - // As these extra copies are kinda useless this may or may not be important. - static std::map boundItemsMap; - if (boundItemsMap.empty()) - { - boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; - boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; - boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; - boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; - boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) - boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; - boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; - boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; - boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; - boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; - boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; - } - - for (std::map::iterator it = boundItemsMap.begin(); it != boundItemsMap.end(); ++it) - { - bool found = creatureStats.mBoundItems.find(it->first) != creatureStats.mBoundItems.end(); - float magnitude = effects.get(it->first).getMagnitude(); - if (found != (magnitude > 0)) - { - if (magnitude > 0) - creatureStats.mBoundItems.insert(it->first); - else - creatureStats.mBoundItems.erase(it->first); - - std::string itemGmst = it->second; - std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( - itemGmst)->mValue.getString(); - - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - - if (it->first == ESM::MagicEffect::BoundGloves) - { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundRightGauntletID")->mValue.getString(); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - } - } - } - - // Summoned creature update visitor assumes the actor belongs to a cell. - // This assumption isn't always valid for the player character. - if (!ptr.isInCell()) - return; - - bool hasSummonEffect = false; - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - if (isSummoningEffect(it->first.mId)) - { - hasSummonEffect = true; - break; - } - } - - if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) - { - UpdateSummonedCreatures updateSummonedCreatures(ptr); - creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); - creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); - updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); - } - } - - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) + bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) const { - NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); - const MagicEffects &effects = npcStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - // skills - for(int i = 0;i < ESM::Skill::Length;++i) - { - SkillValue& skill = npcStats.getSkill(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); - } - skill.setModifier(static_cast(fortify - drain - absorb)); - } - } - - bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) - { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isAttackPreparing(); + return it->second->getCharacterController().isAttackPreparing(); } - bool Actors::isRunning(const MWWorld::Ptr& ptr) + bool Actors::isRunning(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isRunning(); + return it->second->getCharacterController().isRunning(); } - bool Actors::isSneaking(const MWWorld::Ptr& ptr) + bool Actors::isSneaking(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isSneaking(); + return it->second->getCharacterController().isSneaking(); } - void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) + static void updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) { - NpcStats &stats = ptr.getClass().getNpcStats(ptr); + const auto& actorClass = ptr.getClass(); + NpcStats& stats = actorClass.getNpcStats(ptr); // When npc stats are just initialized, mTimeToStartDrowning == -1 and we should get value from GMST static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get().find("fHoldBreathTime")->mValue.getFloat(); @@ -1330,43 +884,43 @@ namespace MWMechanics if (!isPlayer && stats.getTimeToStartDrowning() < fHoldBreathTime / 2) { - AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + AiSequence& seq = actorClass.getCreatureStats(ptr).getAiSequence(); if (seq.getTypeId() != AiPackageTypeId::Breathe) //Only add it once seq.stack(AiBreathe(), ptr); } - MWBase::World *world = MWBase::Environment::get().getWorld(); - bool knockedOutUnderwater = (isKnockedOut && world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))); - if((world->isSubmerged(ptr) || knockedOutUnderwater) - && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) + const MWBase::World* const world = MWBase::Environment::get().getWorld(); + const bool knockedOutUnderwater = (isKnockedOut && world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))); + if ((world->isSubmerged(ptr) || knockedOutUnderwater) + && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; - if(knockedOutUnderwater) + if (knockedOutUnderwater) stats.setTimeToStartDrowning(0); else { timeLeft = stats.getTimeToStartDrowning() - duration; - if(timeLeft < 0.0f) + if (timeLeft < 0.0f) timeLeft = 0.0f; stats.setTimeToStartDrowning(timeLeft); } - bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); + const bool godmode = isPlayer && world->getGodModeState(); - if(timeLeft == 0.0f && !godmode) + if (timeLeft == 0.0f && !godmode) { // If drowning, apply 3 points of damage per second static const float fSuffocationDamage = world->getStore().get().find("fSuffocationDamage")->mValue.getFloat(); DynamicStat health = stats.getHealth(); - health.setCurrent(health.getCurrent() - fSuffocationDamage*duration); + health.setCurrent(health.getCurrent() - fSuffocationDamage * duration); stats.setHealth(health); // Play a drowning sound - MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); - if(!sndmgr->getSoundPlaying(ptr, "drown")) + MWBase::SoundManager* sndmgr = MWBase::Environment::get().getSoundManager(); + if (!sndmgr->getSoundPlaying(ptr, "drown")) sndmgr->playSound3D(ptr, "drown", 1.0f, 1.0f); - if(isPlayer) + if (isPlayer) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } @@ -1374,52 +928,55 @@ namespace MWMechanics stats.setTimeToStartDrowning(fHoldBreathTime); } - void Actors::updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip) + static void updateEquippedLight(const MWWorld::Ptr& ptr, float duration, bool mayEquip) { - bool isPlayer = (ptr == getPlayer()); + const bool isPlayer = (ptr == getPlayer()); + + const auto& actorClass = ptr.getClass(); + auto& inventoryStore = actorClass.getInventoryStore(ptr); + + auto heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - MWWorld::InventoryStore &inventoryStore = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator heldIter = - inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); /** * Automatically equip NPCs torches at night and unequip them at day */ if (!isPlayer) { - MWWorld::ContainerStoreIterator torch = inventoryStore.end(); - for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) - { - if (it->getTypeName() == typeid(ESM::Light).name() && - it->getClass().canBeEquipped(*it, ptr).first) + auto torchIter = std::find_if(std::begin(inventoryStore), std::end(inventoryStore), [&](auto entry) { - torch = it; - break; - } - } - + return entry.getType() == ESM::Light::sRecordId && + entry.getClass().canBeEquipped(entry, ptr).first; + }); if (mayEquip) { - if (torch != inventoryStore.end()) + if (torchIter != inventoryStore.end()) { - if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) + if (!actorClass.getCreatureStats(ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. - if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) + if (heldIter != inventoryStore.end() && heldIter->getType() != ESM::Light::sRecordId) inventoryStore.unequipItem(*heldIter, ptr); } + else if (heldIter == inventoryStore.end() || heldIter->getType() == ESM::Light::sRecordId) + { + // For hostile NPCs, see if they have anything better to equip first + auto shield = inventoryStore.getPreferredShield(ptr); + if (shield != inventoryStore.end()) + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield, ptr); + } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // If we have a torch and can equip it, then equip it now. if (heldIter == inventoryStore.end()) { - inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torchIter, ptr); } } } else { - if (heldIter != inventoryStore.end() && heldIter->getTypeName() == typeid(ESM::Light).name()) + if (heldIter != inventoryStore.end() && heldIter->getType() == ESM::Light::sRecordId) { // At day, unequip lights and auto equip shields or other suitable items // (Note: autoEquip will ignore lights) @@ -1433,11 +990,12 @@ namespace MWMechanics //If holding a light... if(heldIter.getType() == MWWorld::ContainerStore::Type_Light) { + const auto world = MWBase::Environment::get().getWorld(); // Use time from the player's light if(isPlayer) { // But avoid using it up if the light source is hidden - MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); + MWRender::Animation *anim = world->getAnimation(ptr); if (anim && anim->getCarriedLeftShown()) { float timeRemaining = heldIter->getClass().getRemainingUsageTime(*heldIter); @@ -1458,7 +1016,7 @@ namespace MWMechanics } // Both NPC and player lights extinguish in water. - if(MWBase::Environment::get().getWorld()->isSwimming(ptr)) + if(world->isSwimming(ptr)) { inventoryStore.remove(*heldIter, 1, ptr); // remove it @@ -1470,62 +1028,71 @@ namespace MWMechanics } } - void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) + void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const { - MWWorld::Ptr player = getPlayer(); - if (ptr != player && ptr.getClass().isNpc()) - { - // get stats of witness - CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + const MWWorld::Ptr player = getPlayer(); + if (ptr == player) + return; - if (player.getClass().getNpcStats(player).isWerewolf()) - return; + const auto& actorClass = ptr.getClass(); + if (!actorClass.isNpc()) + return; - if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackageTypeId::Pursue && !creatureStats.getAiSequence().isInCombat() - && creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0) + // get stats of witness + CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); + NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + + const auto& playerClass = player.getClass(); + const auto& playerStats = playerClass.getNpcStats(player); + if (playerStats.isWerewolf()) + return; + + const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + const auto world = MWBase::Environment::get().getWorld(); + + if (actorClass.isClass(ptr, "Guard") && !creatureStats.getAiSequence().isInPursuit() && !creatureStats.getAiSequence().isInCombat() + && creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0) + { + const MWWorld::ESMStore& esmStore = world->getStore(); + static const int cutoff = esmStore.get().find("iCrimeThreshold")->mValue.getInteger(); + // Force dialogue on sight if bounty is greater than the cutoff + // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or attack (>= 5000 bounty) + if (playerStats.getBounty() >= cutoff + // TODO: do not run these two every frame. keep an Aware state for each actor and update it every 0.2 s or so? + && world->getLOS(ptr, player) + && mechanicsManager->awarenessCheck(player, ptr)) { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - static const int cutoff = esmStore.get().find("iCrimeThreshold")->mValue.getInteger(); - // Force dialogue on sight if bounty is greater than the cutoff - // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or attack (>= 5000 bounty) - if ( player.getClass().getNpcStats(player).getBounty() >= cutoff - // TODO: do not run these two every frame. keep an Aware state for each actor and update it every 0.2 s or so? - && MWBase::Environment::get().getWorld()->getLOS(ptr, player) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, ptr)) + static const int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); + if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier) { - static const int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); - if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier) - { - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); - creatureStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Stops the guard from quitting combat if player is unreachable - } - else - creatureStats.getAiSequence().stack(AiPursue(player), ptr); - creatureStats.setAlarmed(true); - npcStats.setCrimeId(MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId()); + mechanicsManager->startCombat(ptr, player); + creatureStats.setHitAttemptActorId(playerClass.getCreatureStats(player).getActorId()); // Stops the guard from quitting combat if player is unreachable } + else + creatureStats.getAiSequence().stack(AiPursue(player), ptr); + creatureStats.setAlarmed(true); + npcStats.setCrimeId(world->getPlayer().getNewCrimeId()); } + } - // if I was a witness to a crime - if (npcStats.getCrimeId() != -1) + // if I was a witness to a crime + if (npcStats.getCrimeId() != -1) + { + // if you've paid for your crimes and I havent noticed + if (npcStats.getCrimeId() <= world->getPlayer().getCrimeId()) { - // if you've paid for your crimes and I havent noticed - if( npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId() ) - { - // Calm witness down - if (ptr.getClass().isClass(ptr, "Guard")) - creatureStats.getAiSequence().stopPursuit(); - creatureStats.getAiSequence().stopCombat(); - - // Reset factors to attack - creatureStats.setAttacked(false); - creatureStats.setAlarmed(false); - creatureStats.setAiSetting(CreatureStats::AI_Fight, ptr.getClass().getBaseFightRating(ptr)); - - // Update witness crime id - npcStats.setCrimeId(-1); - } + // Calm witness down + if (ptr.getClass().isClass(ptr, "Guard")) + creatureStats.getAiSequence().stopPursuit(); + stopCombat(ptr); + + // Reset factors to attack + creatureStats.setAttacked(false); + creatureStats.setAlarmed(false); + creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); + + // Update witness crime id + npcStats.setCrimeId(-1); } } } @@ -1537,11 +1104,6 @@ namespace MWMechanics updateProcessingRange(); } - Actors::~Actors() - { - clear(); - } - float Actors::getProcessingRange() const { return mActorsProcessingRange; @@ -1550,27 +1112,24 @@ namespace MWMechanics void Actors::updateProcessingRange() { // We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876) - static const float maxProcessingRange = 7168.f; - static const float minProcessingRange = maxProcessingRange / 2.f; + static constexpr float maxRange = 7168.f; + static constexpr float minRange = maxRange / 2.f; - float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); - actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); - actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); - mActorsProcessingRange = actorsProcessingRange; + mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - removeActor(ptr); + removeActor(ptr, true); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; - mActors.insert(std::make_pair(ptr, new Actor(ptr, anim))); + const auto it = mActors.emplace(mActors.end(), ptr, anim); + mIndex.emplace(ptr.mRef, it); - CharacterController* ctrl = mActors[ptr]->getCharacterController(); if (updateImmediately) - ctrl->update(0); + it->getCharacterController().update(0); // We should initially hide actors outside of processing range. // Note: since we update player after other actors, distance will be incorrect during teleportation. @@ -1578,10 +1137,10 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getPlayer().wasTeleported()) return; - updateVisibility(ptr, ctrl); + updateVisibility(ptr, it->getCharacterController()); } - void Actors::updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl) + void Actors::updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const { MWWorld::Ptr player = MWMechanics::getPlayer(); if (ptr == player) @@ -1598,35 +1157,37 @@ namespace MWMechanics // Fade away actors on large distance (>90% of actor's processing distance) float visibilityRatio = 1.0; - float fadeStartDistance = mActorsProcessingRange*0.9f; - float fadeEndDistance = mActorsProcessingRange; - float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance); + const float fadeStartDistance = mActorsProcessingRange*0.9f; + const float fadeEndDistance = mActorsProcessingRange; + const float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance); if (fadeRatio > 0) visibilityRatio -= std::max(0.f, fadeRatio); visibilityRatio = std::min(1.f, visibilityRatio); - ctrl->setVisibility(visibilityRatio); + ctrl.setVisibility(visibilityRatio); } - void Actors::removeActor (const MWWorld::Ptr& ptr) + void Actors::removeActor (const MWWorld::Ptr& ptr, bool keepActive) { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - delete iter->second; - mActors.erase(iter); + if(!keepActive) + removeTemporaryEffects(iter->second->getPtr()); + mActors.erase(iter->second); + mIndex.erase(iter); } } - void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string& spellId, bool manualSpell) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->castSpell(spellId, manualSpell); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().castSpell(spellId, manualSpell); } - bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) + bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const { if (!actor.getClass().isActor()) return false; @@ -1648,7 +1209,7 @@ namespace MWMechanics if (neighbor == actor) continue; - bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) + const bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, neighbor); if (result) @@ -1658,28 +1219,22 @@ namespace MWMechanics return false; } - void Actors::updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) + void Actors::updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) const { - PtrActorMap::iterator iter = mActors.find(old); - if(iter != mActors.end()) - { - Actor *actor = iter->second; - mActors.erase(iter); - - actor->updatePtr(ptr); - mActors.insert(std::make_pair(ptr, actor)); - } + const auto iter = mIndex.find(old.mRef); + if (iter != mIndex.end()) + iter->second->updatePtr(ptr); } void Actors::dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore) { - PtrActorMap::iterator iter = mActors.begin(); - while(iter != mActors.end()) + for (auto iter = mActors.begin(); iter != mActors.end();) { - if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) + if ((iter->getPtr().isInCell() && iter->getPtr().getCell() == cellStore) && iter->getPtr() != ignore) { - delete iter->second; - mActors.erase(iter++); + removeTemporaryEffects(iter->getPtr()); + mIndex.erase(iter->getPtr().mRef); + iter = mActors.erase(iter); } else ++iter; @@ -1688,21 +1243,21 @@ namespace MWMechanics void Actors::updateCombatMusic () { - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); bool hasHostiles = false; // need to know this to play Battle music - bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); + const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); if (aiActive) { - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if (iter->first == player) continue; + if (actor.getPtr() == player) continue; - bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange; + bool inProcessingRange = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange; if (inProcessingRange) { - MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); + MWMechanics::CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); if (!stats.isDead() && stats.getAiSequence().isInCombat()) { hasHostiles = true; @@ -1713,45 +1268,47 @@ namespace MWMechanics } // check if we still have any player enemies to switch music - static int currentMusic = 0; - - if (currentMusic != 1 && !hasHostiles && !(player.getClass().getCreatureStats(player).isDead() && - MWBase::Environment::get().getSoundManager()->isMusicPlaying())) + if (mCurrentMusic != MusicType::Explore && !hasHostiles + && !(player.getClass().getCreatureStats(player).isDead() + && MWBase::Environment::get().getSoundManager()->isMusicPlaying())) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); - currentMusic = 1; + mCurrentMusic = MusicType::Explore; } - else if (currentMusic != 2 && hasHostiles) + else if (mCurrentMusic != MusicType::Battle && hasHostiles) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); - currentMusic = 2; + mCurrentMusic = MusicType::Battle; } } - void Actors::predictAndAvoidCollisions() + void Actors::predictAndAvoidCollisions(float duration) const { + if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) + return; + const float minGap = 10.f; const float maxDistForPartialAvoiding = 200.f; const float maxDistForStrictAvoiding = 100.f; const float maxTimeToCheck = 2.0f; static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game"); - MWWorld::Ptr player = getPlayer(); - MWBase::World* world = MWBase::Environment::get().getWorld(); - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + const MWWorld::Ptr player = getPlayer(); + const MWBase::World* const world = MWBase::Environment::get().getWorld(); + for (const Actor& actor : mActors) { - const MWWorld::Ptr& ptr = iter->first; + const MWWorld::Ptr& ptr = actor.getPtr(); if (ptr == player) continue; // Don't interfere with player controls. - float maxSpeed = ptr.getClass().getMaxSpeed(ptr); + const float maxSpeed = ptr.getClass().getMaxSpeed(ptr); if (maxSpeed == 0.0) continue; // Can't move, so there is no sense to predict collisions. Movement& movement = ptr.getClass().getMovementSettings(ptr); - osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); - bool isMoving = origMovement.length2() > 0.01; + const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); + const bool isMoving = origMovement.length2() > 0.01; if (movement.mPosition[1] < 0) continue; // Actors can not see others when move backward. @@ -1759,49 +1316,58 @@ namespace MWMechanics // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either // follow player or have a AIWander package with non-empty wander area. bool shouldAvoidCollision = isMoving; + bool shouldGiveWay = false; bool shouldTurnToApproachingActor = !isMoving; MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). - for (const auto& package : ptr.getClass().getCreatureStats(ptr).getAiSequence()) + const auto& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (!aiSequence.isEmpty()) { - if (package->getTypeId() == AiPackageTypeId::Follow) + const auto& package = aiSequence.getActivePackage(); + if (package.getTypeId() == AiPackageTypeId::Follow) + { shouldAvoidCollision = true; - else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) + } + else if (package.getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { - if (!static_cast(package.get())->isStationary()) - shouldAvoidCollision = true; + if (!static_cast(package).isStationary()) + shouldGiveWay = true; } - else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) + else if (package.getTypeId() == AiPackageTypeId::Combat || package.getTypeId() == AiPackageTypeId::Pursue) { - currentTarget = package->getTarget(); + currentTarget = package.getTarget(); shouldAvoidCollision = isMoving; shouldTurnToApproachingActor = false; - break; } } - if (!shouldAvoidCollision) + + if (!shouldAvoidCollision && !shouldGiveWay) continue; - osg::Vec2f baseSpeed = origMovement * maxSpeed; - osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); - float baseRotZ = ptr.getRefData().getPosition().rot[2]; - osg::Vec3f halfExtents = world->getHalfExtents(ptr); - float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; + const osg::Vec2f baseSpeed = origMovement * maxSpeed; + const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); + const float baseRotZ = ptr.getRefData().getPosition().rot[2]; + const osg::Vec3f halfExtents = world->getHalfExtents(ptr); + const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; - float timeToCollision = maxTimeToCheck; + float timeToCheck = maxTimeToCheck; + if (!shouldGiveWay && !aiSequence.isEmpty()) + timeToCheck = std::min(timeToCheck, getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents)); + + float timeToCollision = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; // Iterate through all other actors and predict collisions. - for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) + for (const Actor& otherActor : mActors) { - const MWWorld::Ptr& otherPtr = otherIter->first; + const MWWorld::Ptr& otherPtr = otherActor.getPtr(); if (otherPtr == ptr || otherPtr == currentTarget) continue; - osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); - osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; - osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); - float dist = deltaPos.length(); + const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; + const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); + const float dist = deltaPos.length(); // Ignore actors which are not close enough or come from behind. if (dist > maxDistToCheck || relPos.y() < 0) @@ -1811,21 +1377,21 @@ namespace MWMechanics if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; - osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() * - otherPtr.getClass().getMaxSpeed(otherPtr); - float rotZ = otherPtr.getRefData().getPosition().rot[2]; - osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; + const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() + * otherPtr.getClass().getMaxSpeed(otherPtr); + const float rotZ = otherPtr.getRefData().getPosition().rot[2]; + const osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; - float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x(); + float collisionDist = minGap + halfExtents.x() + otherHalfExtents.x(); collisionDist = std::min(collisionDist, relPos.length()); // Find the earliest `t` when |relPos + relSpeed * t| == collisionDist. - float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); - float v2 = relSpeed.length2(); - float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); + const float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); + const float v2 = relSpeed.length2(); + const float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); if (Dh <= 0 || v2 == 0) continue; // No solution; distance is always >= collisionDist. - float t = (-vr - std::sqrt(Dh)) / v2; + const float t = (-vr - std::sqrt(Dh)) / v2; if (t < 0 || t > timeToCollision) continue; @@ -1838,16 +1404,16 @@ namespace MWMechanics timeToCollision = t; angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); - osg::Vec2f posAtT = relPos + relSpeed * t; - float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); - coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); + const osg::Vec2f posAtT = relPos + relSpeed * t; + const float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed) + * std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); movementCorrection = posAtT * coef; if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) // In case of dead body still try to go around (it looks natural), but reduce the correction twice. movementCorrection.y() *= 0.5f; } - if (timeToCollision < maxTimeToCheck) + if (timeToCollision < timeToCheck) { // Try to evade the nearest collision. osg::Vec2f newMovement = origMovement + movementCorrection; @@ -1868,32 +1434,33 @@ namespace MWMechanics { if(!paused) { - static float timerUpdateAITargets = 0; - static float timerUpdateHeadTrack = 0; - static float timerUpdateEquippedLight = 0; - static float timerUpdateHello = 0; const float updateEquippedLightInterval = 1.0f; - // target lists get updated once every 1.0 sec - if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0; - if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; - if (timerUpdateHello >= 0.25f) timerUpdateHello = 0; - if (mTimerDisposeSummonsCorpses >= 0.2f) mTimerDisposeSummonsCorpses = 0; - if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0; + if (mTimerUpdateHeadTrack >= 0.3f) + mTimerUpdateHeadTrack = 0; + + if (mTimerUpdateHello >= 0.25f) + mTimerUpdateHello = 0; + + if (mTimerDisposeSummonsCorpses >= 0.2f) + mTimerDisposeSummonsCorpses = 0; + + if (mTimerUpdateEquippedLight >= updateEquippedLightInterval) + mTimerUpdateEquippedLight = 0; // show torches only when there are darkness and no precipitations - MWBase::World* world = MWBase::Environment::get().getWorld(); - bool showTorches = world->useTorches(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); + const bool showTorches = world->useTorches(); - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); /// \todo move update logic to Actor class where appropriate std::map > cachedAllies; // will be filled as engageCombat iterates - bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); - int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); + const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); + const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); if (attackedByPlayerId != -1) { const MWWorld::Ptr playerHitAttemptActor = world->searchPtrViaActorId(attackedByPlayerId); @@ -1901,53 +1468,52 @@ namespace MWMechanics if (!playerHitAttemptActor.isInCell()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); + const bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); // AI and magic effects update - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (Actor& actor : mActors) { - bool isPlayer = iter->first == player; - CharacterController* ctrl = iter->second->getCharacterController(); + const bool isPlayer = actor.getPtr() == player; + CharacterController& ctrl = actor.getCharacterController(); + MWBase::LuaManager::ActorControls* luaControls = + MWBase::Environment::get().getLuaManager()->getActorControls(actor.getPtr()); - float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2(); + const float distSqr = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2(); // AI processing is only done within given distance to the player. - bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange; - - if (isPlayer) - ctrl->setAttackingOrSpell(world->getPlayer().getAttackingOrSpell()); + const bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange; // If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player. - if (iter->first != player && (iter->first.getClass().getCreatureStats(iter->first).isDead() - || !iter->first.getClass().getCreatureStats(iter->first).getAiSequence().isInCombat() + if (!isPlayer && (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead() + || !actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getAiSequence().isInCombat() || !inProcessingRange)) { - iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(-1); - if (player.getClass().getCreatureStats(player).getHitAttemptActorId() == iter->first.getClass().getCreatureStats(iter->first).getActorId()) + actor.getPtr().getClass().getCreatureStats(actor.getPtr()).setHitAttemptActorId(-1); + if (player.getClass().getCreatureStats(player).getHitAttemptActorId() == actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActorId()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + const Misc::TimerStatus engageCombatTimerStatus = actor.updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles - if (iter->first.getClass().getCreatureStats(iter->first).isDead()) + if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { // They can be added during the death animation - if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) - adjustMagicEffects(iter->first); - ctrl->updateContinuousVfx(); + if (!actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDeathAnimationFinished()) + adjustMagicEffects(actor.getPtr(), duration); + ctrl.updateContinuousVfx(); } else { - bool cellChanged = world->hasCellChanged(); - MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports - updateActor(actor, duration); + const bool cellChanged = world->hasCellChanged(); + const MWWorld::Ptr actorPtr = actor.getPtr(); // make a copy of the map key to avoid it being invalidated when the player teleports + updateActor(actorPtr, duration); // Looping magic VFX update // Note: we need to do this before any of the animations are updated. // Reaching the text keys may trigger Hit / Spellcast (and as such, particles), // so updating VFX immediately after that would just remove the particle effects instantly. // There needs to be a magic effect update in between. - ctrl->updateContinuousVfx(); + ctrl.updateContinuousVfx(); if (!cellChanged && world->hasCellChanged()) { @@ -1956,159 +1522,131 @@ namespace MWMechanics } if (aiActive && inProcessingRange) { - if (timerUpdateAITargets == 0) + if (engageCombatTimerStatus == Misc::TimerStatus::Elapsed) { if (!isPlayer) - adjustCommandedActor(iter->first); + adjustCommandedActor(actor.getPtr()); - for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + for (const Actor& otherActor : mActors) { - if (it->first == iter->first || isPlayer) // player is not AI-controlled + if (otherActor.getPtr() == actor.getPtr() || isPlayer) // player is not AI-controlled continue; - engageCombat(iter->first, it->first, cachedAllies, it->first == player); + engageCombat(actor.getPtr(), otherActor.getPtr(), cachedAllies, otherActor.getPtr() == player); } } - if (timerUpdateHeadTrack == 0) - { - float sqrHeadTrackDistance = std::numeric_limits::max(); - MWWorld::Ptr headTrackTarget; + if (mTimerUpdateHeadTrack == 0) + updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl); - MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); - bool firstPersonPlayer = isPlayer && world->isFirstPerson(); - bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); + if (actor.getPtr().getClass().isNpc() && !isPlayer) + updateCrimePursuit(actor.getPtr(), duration); - // 1. Unconsious actor can not track target - // 2. Actors in combat and pursue mode do not bother to headtrack - // 3. Player character does not use headtracking in the 1st-person view - if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue) - { - for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) - { - if (it->first == iter->first) - continue; - updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance); - } - } - - if (!stats.getKnockedDown() && !isPlayer && inCombatOrPursue) - { - // Actors in combat and pursue mode always look at their target. - for (const auto& package : stats.getAiSequence()) - { - headTrackTarget = package->getTarget(); - if (!headTrackTarget.isEmpty()) - break; - } - } - - ctrl->setHeadTrackTarget(headTrackTarget); - } - - if (iter->first.getClass().isNpc() && iter->first != player) - updateCrimePursuit(iter->first, duration); - - if (iter->first != player) + if (!isPlayer) { - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - if (isConscious(iter->first)) + CreatureStats &stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); + if (isConscious(actor.getPtr()) && !(luaControls && luaControls->mDisableAI)) { - stats.getAiSequence().execute(iter->first, *ctrl, duration); - updateGreetingState(iter->first, *iter->second, timerUpdateHello > 0); - playIdleDialogue(iter->first); - updateMovementSpeed(iter->first); + stats.getAiSequence().execute(actor.getPtr(), ctrl, duration); + updateGreetingState(actor.getPtr(), actor, mTimerUpdateHello > 0); + playIdleDialogue(actor.getPtr()); + updateMovementSpeed(actor.getPtr()); } } } - else if (aiActive && iter->first != player && isConscious(iter->first)) + else if (aiActive && !isPlayer && isConscious(actor.getPtr()) && !(luaControls && luaControls->mDisableAI)) { - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); + CreatureStats &stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); + stats.getAiSequence().execute(actor.getPtr(), ctrl, duration, /*outOfRange*/true); } - if(iter->first.getClass().isNpc()) + if (inProcessingRange && actor.getPtr().getClass().isNpc()) { // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe - if (inProcessingRange) - updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - - calculateNpcStatModifiers(iter->first, duration); - - if (timerUpdateEquippedLight == 0) - updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); + updateDrowning(actor.getPtr(), duration, ctrl.isKnockedOut(), isPlayer); } + if (mTimerUpdateEquippedLight == 0 && actor.getPtr().getClass().hasInventoryStore(actor.getPtr())) + updateEquippedLight(actor.getPtr(), updateEquippedLightInterval, showTorches); + + if (luaControls != nullptr && isConscious(actor.getPtr())) + updateLuaControls(actor.getPtr(), isPlayer, *luaControls); } } static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game"); if (avoidCollisions) - predictAndAvoidCollisions(); + predictAndAvoidCollisions(duration); - timerUpdateAITargets += duration; - timerUpdateHeadTrack += duration; - timerUpdateEquippedLight += duration; - timerUpdateHello += duration; + mTimerUpdateHeadTrack += duration; + mTimerUpdateEquippedLight += duration; + mTimerUpdateHello += duration; mTimerDisposeSummonsCorpses += duration; // Animation/movement update CharacterController* playerCharacter = nullptr; - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (Actor& actor : mActors) { - const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); - bool isPlayer = iter->first == player; - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + const float dist = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length(); + const bool isPlayer = actor.getPtr() == player; + CreatureStats &stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); // Actors with active AI should be able to move. bool alwaysActive = false; - if (!isPlayer && isConscious(iter->first) && !stats.isParalyzed()) + if (!isPlayer && isConscious(actor.getPtr()) && !stats.isParalyzed()) { MWMechanics::AiSequence& seq = stats.getAiSequence(); alwaysActive = !seq.isEmpty() && seq.getActivePackage().alwaysActive(); } - bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive; - int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) - if (isPlayer) - activeFlag = 2; - int active = inRange ? activeFlag : 0; + const bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive; + const int activeFlag = isPlayer ? 2 : 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) + const int active = inRange ? activeFlag : 0; - CharacterController* ctrl = iter->second->getCharacterController(); - ctrl->setActive(active); + CharacterController& ctrl = actor.getCharacterController(); + ctrl.setActive(active); if (!inRange) { - iter->first.getRefData().getBaseNode()->setNodeMask(0); - world->setActorCollisionMode(iter->first, false, false); + actor.getPtr().getRefData().getBaseNode()->setNodeMask(0); + world->setActorActive(actor.getPtr(), false); continue; } - else if (!isPlayer) - iter->first.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); - const bool isDead = iter->first.getClass().getCreatureStats(iter->first).isDead(); - if (!isDead && (!godmode || !isPlayer) && iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) - ctrl->skipAnim(); + 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()) + ctrl.skipAnim(); // Handle player last, in case a cell transition occurs by casting a teleportation spell // (would invalidate the iterator) - if (iter->first == getPlayer()) + if (isPlayer) { - playerCharacter = ctrl; + playerCharacter = &ctrl; continue; } - world->setActorCollisionMode(iter->first, true, !iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()); - ctrl->update(duration); + actor.getPtr().getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); + world->setActorCollisionMode(actor.getPtr(), true, !actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDeathAnimationFinished()); + + if (!actor.getPositionAdjusted()) + { + actor.getPtr().getClass().adjustPosition(actor.getPtr(), false); + actor.setPositionAdjusted(true); + } + + ctrl.update(duration); - updateVisibility(iter->first, ctrl); + updateVisibility(actor.getPtr(), ctrl); } if (playerCharacter) { + MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(duration); playerCharacter->update(duration); playerCharacter->setVisibility(1.f); } - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Class &cls = actor.getPtr().getClass(); + CreatureStats &stats = cls.getCreatureStats(actor.getPtr()); //KnockedOutOneFrameLogic //Used for "OnKnockedOut" command @@ -2138,91 +1676,79 @@ namespace MWMechanics ++mDeathCount[Misc::StringUtils::lowerCase(actor.getCellRef().getRefId())]; } - void Actors::resurrect(const MWWorld::Ptr &ptr) + void Actors::resurrect(const MWWorld::Ptr &ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - if(iter->second->getCharacterController()->isDead()) + if (iter->second->getCharacterController().isDead()) { // Actor has been resurrected. Notify the CharacterController and re-enable collision. - MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, true); - iter->second->getCharacterController()->resurrect(); + MWBase::Environment::get().getWorld()->enableActorCollision(iter->second->getPtr(), true); + iter->second->getCharacterController().resurrect(); } } } void Actors::killDeadActors() { - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (Actor& actor : mActors) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Class &cls = actor.getPtr().getClass(); + CreatureStats &stats = cls.getCreatureStats(actor.getPtr()); if(!stats.isDead()) continue; - MWBase::Environment::get().getWorld()->removeActorPath(iter->first); - CharacterController::KillResult killResult = iter->second->getCharacterController()->kill(); + MWBase::Environment::get().getWorld()->removeActorPath(actor.getPtr()); + CharacterController::KillResult killResult = actor.getCharacterController().kill(); if (killResult == CharacterController::Result_DeathAnimStarted) { // Play dying words // Note: It's not known whether the soundgen tags scream, roar, and moan are reliable // for NPCs since some of the npc death animation files are missing them. - MWBase::Environment::get().getDialogueManager()->say(iter->first, "hit"); + MWBase::Environment::get().getDialogueManager()->say(actor.getPtr(), "hit"); // Apply soultrap - if (iter->first.getTypeName() == typeid(ESM::Creature).name()) - { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); - } - - // Magic effects will be reset later, and the magic effect that could kill the actor - // needs to be determined now - calculateCreatureStatModifiers(iter->first, 0); + if (actor.getPtr().getType() == ESM::Creature::sRecordId) + soulTrap(actor.getPtr()); - if (cls.isEssential(iter->first)) + if (cls.isEssential(actor.getPtr())) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } else if (killResult == CharacterController::Result_DeathAnimJustFinished) { - bool isPlayer = iter->first == getPlayer(); - notifyDied(iter->first); + const bool isPlayer = actor.getPtr() == getPlayer(); + notifyDied(actor.getPtr()); // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.modifyMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); + const float vampirism = stats.getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude(); + stats.getActiveSpells().clear(actor.getPtr()); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); - // Reset dynamic stats, attributes and skills - calculateCreatureStatModifiers(iter->first, 0); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, 0); + stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { //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 { // NPC death animation is over, disable actor collision - MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); + MWBase::Environment::get().getWorld()->enableActorCollision(actor.getPtr(), false); } - - // Play Death Music if it was the player dying - if(iter->first == getPlayer()) - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } - void Actors::cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId) + void Actors::cleanupSummonedCreature(MWMechanics::CreatureStats& casterStats, int creatureActorId) const { - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); + const MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); if (!ptr.isEmpty()) { MWBase::Environment::get().getWorld()->deleteObject(ptr); @@ -2230,12 +1756,16 @@ namespace MWMechanics const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Summon_End"); if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + { + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + MWBase::Environment::get().getWorld()->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), "", ptr.getRefData().getPosition().asVec3()); + } // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2251,55 +1781,47 @@ namespace MWMechanics purgeSpellEffects(creatureActorId); } - void Actors::purgeSpellEffects(int casterActorId) + void Actors::purgeSpellEffects(int casterActorId) const { - for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + for (const Actor& actor : mActors) { - MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); - spells.purge(casterActorId); + MWMechanics::ActiveSpells& spells = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActiveSpells(); + spells.purge(actor.getPtr(), casterActorId); } } - void Actors::rest(double hours, bool sleep) + void Actors::rest(double hours, bool sleep) const { float duration = hours * 3600.f; - float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); + const float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if (timeScale != 0.f) duration /= timeScale; const MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if (iter->first.getClass().getCreatureStats(iter->first).isDead()) + if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + adjustMagicEffects(actor.getPtr(), duration); continue; } - if (!sleep || iter->first == player) - restoreDynamicStats(iter->first, hours, sleep); + if (!sleep || actor.getPtr() == player) + restoreDynamicStats(actor.getPtr(), hours, sleep); - if ((!iter->first.getRefData().getBaseNode()) || - (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) + if ((!actor.getPtr().getRefData().getBaseNode()) || + (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) continue; - adjustMagicEffects (iter->first); - if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) - calculateDynamicStats (iter->first); - - calculateCreatureStatModifiers (iter->first, duration); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, duration); + adjustMagicEffects (actor.getPtr(), duration); - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(actor.getPtr()); if (animation) { animation->removeEffects(); - MWBase::Environment::get().getWorld()->applyLoopingParticles(iter->first); + MWBase::Environment::get().getWorld()->applyLoopingParticles(actor.getPtr()); } } @@ -2308,15 +1830,13 @@ namespace MWMechanics void Actors::updateSneaking(CharacterController* ctrl, float duration) { - static float sneakTimer = 0.f; // Times update of sneak icon - if (!ctrl) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); if (!MWBase::Environment::get().getMechanicsManager()->isSneaking(player)) { @@ -2324,32 +1844,36 @@ namespace MWMechanics return; } - static float sneakSkillTimer = 0.f; // Times sneak skill progress from "avoid notice" - - MWBase::World* world = MWBase::Environment::get().getWorld(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); const MWWorld::Store& gmst = world->getStore().get(); static const float fSneakUseDist = gmst.find("fSneakUseDist")->mValue.getFloat(); static const float fSneakUseDelay = gmst.find("fSneakUseDelay")->mValue.getFloat(); - if (sneakTimer >= fSneakUseDelay) - sneakTimer = 0.f; + if (mSneakTimer >= fSneakUseDelay) + mSneakTimer = 0.f; - if (sneakTimer == 0.f) + if (mSneakTimer == 0.f) { // Set when an NPC is within line of sight and distance, but is still unaware. Used for skill progress. bool avoidedNotice = false; bool detected = false; std::vector observers; - osg::Vec3f position(player.getRefData().getPosition().asVec3()); - float radius = std::min(fSneakUseDist, mActorsProcessingRange); + const osg::Vec3f position(player.getRefData().getPosition().asVec3()); + const float radius = std::min(fSneakUseDist, mActorsProcessingRange); getObjectsInRange(position, radius, observers); + std::set sidingActors; + getActorsSidingWith(player, sidingActors); + for (const MWWorld::Ptr &observer : observers) { if (observer == player || observer.getClass().getCreatureStats(observer).isDead()) continue; + if (sidingActors.find(observer) != sidingActors.cend()) + continue; + if (world->getLOS(player, observer)) { if (MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, observer)) @@ -2366,60 +1890,58 @@ namespace MWMechanics } } - if (sneakSkillTimer >= fSneakUseDelay) - sneakSkillTimer = 0.f; + if (mSneakSkillTimer >= fSneakUseDelay) + mSneakSkillTimer = 0.f; - if (avoidedNotice && sneakSkillTimer == 0.f) + if (avoidedNotice && mSneakSkillTimer == 0.f) player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); } - sneakTimer += duration; - sneakSkillTimer += duration; + mSneakTimer += duration; + mSneakSkillTimer += duration; } int Actors::getHoursToRest(const MWWorld::Ptr &ptr) const { - float healthPerHour, magickaPerHour; - getRestorationPerHourOfSleep(ptr, healthPerHour, magickaPerHour); + const auto [healthPerHour, magickaPerHour] = getRestorationPerHourOfSleep(ptr); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; + const bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; - float healthHours = healthPerHour > 0 + const float healthHours = healthPerHour > 0 ? (stats.getHealth().getModified() - stats.getHealth().getCurrent()) / healthPerHour : 1.0f; - float magickaHours = magickaPerHour > 0 && !stunted + const float magickaHours = magickaPerHour > 0 && !stunted ? (stats.getMagicka().getModified() - stats.getMagicka().getCurrent()) / magickaPerHour : 1.0f; - int autoHours = static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); - return autoHours; + return static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); } int Actors::countDeaths (const std::string& id) const { - std::map::const_iterator iter = mDeathCount.find(id); + const auto iter = mDeathCount.find(id); if(iter != mDeathCount.end()) return iter->second; return 0; } - void Actors::forceStateUpdate(const MWWorld::Ptr & ptr) + void Actors::forceStateUpdate(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->forceStateUpdate(); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().forceStateUpdate(); } - bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) + bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + 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, persist); } else { @@ -2427,134 +1949,135 @@ namespace MWMechanics return false; } } - void Actors::skipAnimation(const MWWorld::Ptr& ptr) + void Actors::skipAnimation(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->skipAnim(); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().skipAnim(); } - bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) + bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - return iter->second->getCharacterController()->isAnimPlaying(groupName); + const auto iter = mIndex.find(ptr.mRef); + if(iter != mIndex.end()) + return iter->second->getCharacterController().isAnimPlaying(groupName); return false; } - void Actors::persistAnimationStates() + void Actors::persistAnimationStates() const { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) - iter->second->getCharacterController()->persistAnimationState(); + for (const Actor& actor : mActors) + actor.getCharacterController().persistAnimationState(); } - void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) + void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) - out.push_back(iter->first); + if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius*radius) + out.push_back(actor.getPtr()); } } - bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) + bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) const { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) + if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius*radius) return true; } return false; } - std::list Actors::getActorsSidingWith(const MWWorld::Ptr& actor) + std::vector Actors::getActorsSidingWith(const MWWorld::Ptr& actorPtr, bool excludeInfighting) const { - std::list list; - for(PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + std::vector list; + for (const Actor& actor : mActors) { - const MWWorld::Ptr &iteratedActor = iter->first; + const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == getPlayer()) continue; - const bool sameActor = (iteratedActor == actor); + const bool sameActor = (iteratedActor == actorPtr); const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; - // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package + // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Wander packages before the Follow/Escort package // Actors that are targeted by this actor's Follow or Escort packages also side with them for (const auto& package : stats.getAiSequence()) { + if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat && package->getTarget() == actorPtr) + break; if (package->sideWithTarget() && !package->getTarget().isEmpty()) { if (sameActor) { + if(excludeInfighting) + { + MWWorld::Ptr ally = package->getTarget(); + std::vector enemies; + if(ally.getClass().getCreatureStats(ally).getAiSequence().getCombatTargets(enemies) + && std::find(enemies.begin(), enemies.end(), actorPtr) != enemies.end()) + break; + } list.push_back(package->getTarget()); } - else if (package->getTarget() == actor) + else if (package->getTarget() == actorPtr) { list.push_back(iteratedActor); } break; } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + else if (package->getTypeId() > AiPackageTypeId::Wander && package->getTypeId() <= AiPackageTypeId::Activate) // Don't count "fake" package types break; } } return list; } - std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) + std::vector Actors::getActorsFollowing(const MWWorld::Ptr& actorPtr) const { - std::list list; - for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + std::vector list; + forEachFollowingPackage(mActors, actorPtr, getPlayer(), [&] (const Actor& actor, const std::shared_ptr& package) { - const MWWorld::Ptr &iteratedActor = iter->first; - if (iteratedActor == getPlayer() || iteratedActor == actor) - continue; - - const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); - if (stats.isDead()) - continue; - - // An actor counts as following if AiFollow is the current AiPackage, - // or there are only Combat and Wander packages before the AiFollow package - for (const auto& package : stats.getAiSequence()) - { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - list.push_back(iteratedActor); - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - break; - } - } + if (package->followTargetThroughDoors() && package->getTarget() == actorPtr) + list.push_back(actor.getPtr()); + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } - void Actors::getActorsFollowing(const MWWorld::Ptr &actor, std::set& out) { - std::list followers = getActorsFollowing(actor); + void Actors::getActorsFollowing(const MWWorld::Ptr &actor, std::set& out) const + { + auto followers = getActorsFollowing(actor); for(const MWWorld::Ptr &follower : followers) if (out.insert(follower).second) getActorsFollowing(follower, out); } - void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out) { - std::list followers = getActorsSidingWith(actor); + void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, bool excludeInfighting) const + { + auto followers = getActorsSidingWith(actor, excludeInfighting); for(const MWWorld::Ptr &follower : followers) if (out.insert(follower).second) - getActorsSidingWith(follower, out); + getActorsSidingWith(follower, out, excludeInfighting); } - void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies) { + 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 { - std::list followers = getActorsSidingWith(actor); - for (const MWWorld::Ptr &follower : followers) + for (const MWWorld::Ptr &follower : getActorsSidingWith(actor, true)) if (out.insert(follower).second) getActorsSidingWith(follower, out, cachedAllies); @@ -2569,39 +2092,46 @@ namespace MWMechanics } } - std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) + std::vector Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) const { - std::list list; - for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + std::vector list; + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (const Actor&, const std::shared_ptr& package) { - const MWWorld::Ptr &iteratedActor = iter->first; - if (iteratedActor == getPlayer() || iteratedActor == actor) - continue; - - const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); - if (stats.isDead()) - continue; - - // An actor counts as following if AiFollow is the current AiPackage, - // or there are only Combat and Wander packages before the AiFollow package - for (const auto& package : stats.getAiSequence()) + if (package->followTargetThroughDoors() && package->getTarget() == actor) { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - { - list.push_back(static_cast(package.get())->getFollowIndex()); - break; - } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - break; + list.push_back(static_cast(package.get())->getFollowIndex()); + return false; } - } + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } - std::list Actors::getActorsFighting(const MWWorld::Ptr& actor) { - std::list list; + std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor) const + { + std::map map; + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (const Actor& otherActor, const std::shared_ptr& package) + { + if (package->followTargetThroughDoors() && package->getTarget() == actor) + { + const int index = static_cast(package.get())->getFollowIndex(); + map[index] = otherActor.getPtr(); + return false; + } + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); + return map; + } + + std::vector Actors::getActorsFighting(const MWWorld::Ptr& actor) const + { + std::vector list; std::vector neighbors; - osg::Vec3f position (actor.getRefData().getPosition().asVec3()); + const osg::Vec3f position(actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, mActorsProcessingRange, neighbors); for(const MWWorld::Ptr& neighbor : neighbors) { @@ -2613,30 +2143,31 @@ namespace MWMechanics continue; if (stats.getAiSequence().isInCombat(actor)) - list.push_front(neighbor); + list.push_back(neighbor); } return list; } - std::list Actors::getEnemiesNearby(const MWWorld::Ptr& actor) + std::vector Actors::getEnemiesNearby(const MWWorld::Ptr& actor) const { - std::list list; + std::vector list; std::vector neighbors; osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, mActorsProcessingRange, neighbors); std::set followers; getActorsFollowing(actor, followers); - for (auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor) + for (const MWWorld::Ptr& neighbor : neighbors) { - const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor); - if (stats.isDead() || *neighbor == actor || neighbor->getClass().isPureWaterCreature(*neighbor)) + const CreatureStats &stats = neighbor.getClass().getCreatureStats(neighbor); + if (stats.isDead() || neighbor == actor || neighbor.getClass().isPureWaterCreature(neighbor)) continue; - const bool isFollower = followers.find(*neighbor) != followers.end(); + const bool isFollower = followers.find(neighbor) != followers.end(); - if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*neighbor, actor) && !isFollower)) - list.push_back(*neighbor); + if (stats.getAiSequence().isInCombat(actor) + || (MWBase::Environment::get().getMechanicsManager()->isAggressive(neighbor, actor) && !isFollower)) + list.push_back(neighbor); } return list; } @@ -2645,10 +2176,10 @@ namespace MWMechanics void Actors::write (ESM::ESMWriter& writer, Loading::Listener& listener) const { writer.startRecord(ESM::REC_DCOU); - for (std::map::const_iterator it = mDeathCount.begin(); it != mDeathCount.end(); ++it) + for (const auto& [id, count] : mDeathCount) { - writer.writeHNString("ID__", it->first); - writer.writeHNT ("COUN", it->second); + writer.writeHNString("ID__", id); + writer.writeHNT ("COUN", count); } writer.endRecord(ESM::REC_DCOU); } @@ -2670,56 +2201,47 @@ namespace MWMechanics void Actors::clear() { - PtrActorMap::iterator it(mActors.begin()); - for (; it != mActors.end(); ++it) - { - delete it->second; - it->second = nullptr; - } + mIndex.clear(); mActors.clear(); mDeathCount.clear(); } - void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) + void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) const { - adjustMagicEffects(ptr); - calculateCreatureStatModifiers(ptr, 0.f); - if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(ptr, 0.f); + adjustMagicEffects(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - return it->second->getCharacterController()->isReadyToBlock(); + return it->second->getCharacterController().isReadyToBlock(); } bool Actors::isCastingSpell(const MWWorld::Ptr &ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - return it->second->getCharacterController()->isCastingSpell(); + return it->second->getCharacterController().isCastingSpell(); } bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - return ctrl->isAttackingOrSpell(); + return it->second->getCharacterController().isAttackingOrSpell(); } int Actors::getGreetingTimer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return 0; return it->second->getGreetingTimer(); @@ -2727,8 +2249,8 @@ namespace MWMechanics float Actors::getAngleToPlayer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return 0.f; return it->second->getAngleToPlayer(); @@ -2736,8 +2258,8 @@ namespace MWMechanics GreetingState Actors::getGreetingState(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return Greet_None; return it->second->getGreetingState(); @@ -2745,23 +2267,22 @@ namespace MWMechanics bool Actors::isTurningToPlayer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; return it->second->isTurningToPlayer(); } - void Actors::fastForwardAi() + void Actors::fastForwardAi() const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; - // making a copy since fast-forward could move actor to a different cell and invalidate the mActors iterator - PtrActorMap map = mActors; - for (PtrActorMap::iterator it = map.begin(); it != map.end(); ++it) + for (auto it = mActors.begin(); it != mActors.end();) { - MWWorld::Ptr ptr = it->first; + const MWWorld::Ptr ptr = it->getPtr(); + ++it; if (ptr == getPlayer() || !isConscious(ptr) || ptr.getClass().getCreatureStats(ptr).isParalyzed()) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 4535400016..071e2e5d84 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -7,7 +7,7 @@ #include #include -#include "../mwmechanics/actorutil.hpp" +#include "actor.hpp" namespace ESM { @@ -39,52 +39,23 @@ namespace MWMechanics class Actors { - std::map mDeathCount; - - void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - - void adjustMagicEffects (const MWWorld::Ptr& creature); - - void calculateDynamicStats (const MWWorld::Ptr& ptr); - - void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); - - void calculateRestoration (const MWWorld::Ptr& ptr, float duration); - - void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); - - void updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip); - - void updateCrimePursuit (const MWWorld::Ptr& ptr, float duration); - - void killDeadActors (); - - void purgeSpellEffects (int casterActorId); - - void predictAndAvoidCollisions(); - public: Actors(); - ~Actors(); - typedef std::map PtrActorMap; - - PtrActorMap::const_iterator begin() { return mActors.begin(); } - PtrActorMap::const_iterator end() { return mActors.end(); } + std::list::const_iterator begin() const { return mActors.begin(); } + std::list::const_iterator end() const { return mActors.end(); } std::size_t size() const { return mActors.size(); } void notifyDied(const MWWorld::Ptr &actor); /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers - bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer); + bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const; /// 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) - void updateMagicEffects (const MWWorld::Ptr& ptr); + void updateMagicEffects(const MWWorld::Ptr& ptr) const; void updateProcessingRange(); float getProcessingRange() const; @@ -94,16 +65,16 @@ namespace MWMechanics /// /// \note Dead actors are ignored. - void removeActor (const MWWorld::Ptr& ptr); + void removeActor (const MWWorld::Ptr& ptr, bool keepActive); ///< Deregister an actor for stats management /// /// \note Ignored, if \a ptr is not a registered actor. - void resurrect (const MWWorld::Ptr& ptr); + void resurrect(const MWWorld::Ptr& ptr) const; - void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + void castSpell(const MWWorld::Ptr& ptr, const std::string& spellId, bool manualSpell = false) const; - void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); + void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr) const; ///< Updates an actor with a new Ptr void dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore); @@ -115,79 +86,75 @@ namespace MWMechanics void update (float duration, bool paused); ///< Update actor stats and store desired velocity vectors in \a movement - void updateActor (const MWWorld::Ptr& ptr, float duration); + void updateActor(const MWWorld::Ptr& ptr, float duration) const; ///< This function is normally called automatically during the update process, but it can /// also be called explicitly at any time to force an update. - /** Start combat between two actors - @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); + /// Removes an actor from combat and makes all of their allies stop fighting the actor's targets + void stopCombat(const MWWorld::Ptr& ptr) const; - void playIdleDialogue(const MWWorld::Ptr& actor); - void updateMovementSpeed(const MWWorld::Ptr& actor); + void playIdleDialogue(const MWWorld::Ptr& actor) const; + void updateMovementSpeed(const MWWorld::Ptr& actor) const; void updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly); - void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir); - - void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance); + void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) const; - void rest(double hours, bool sleep); + void rest(double hours, bool sleep) const; ///< Update actors while the player is waiting or sleeping. void updateSneaking(CharacterController* ctrl, float duration); ///< Update the sneaking indicator state according to the given player character controller. - void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep); + void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) const; int getHoursToRest(const MWWorld::Ptr& ptr) const; ///< Calculate how many hours the given actor needs to rest in order to be fully healed - void fastForwardAi(); + void fastForwardAi() const; ///< Simulate the passing of time int countDeaths (const std::string& id) const; ///< Return the number of deaths for actors with the given ID. - bool isAttackPreparing(const MWWorld::Ptr& ptr); - bool isRunning(const MWWorld::Ptr& ptr); - bool isSneaking(const MWWorld::Ptr& ptr); + bool isAttackPreparing(const MWWorld::Ptr& ptr) const; + bool isRunning(const MWWorld::Ptr& ptr) const; + bool isSneaking(const MWWorld::Ptr& ptr) const; - void forceStateUpdate(const MWWorld::Ptr &ptr); + void forceStateUpdate(const MWWorld::Ptr &ptr) const; - bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); - void skipAnimation(const MWWorld::Ptr& ptr); - bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName); - void persistAnimationStates(); + bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, + int number, bool persist = false) const; + void skipAnimation(const MWWorld::Ptr& ptr) const; + bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; + void persistAnimationStates() const; - void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out); + void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; - bool isAnyObjectInRange(const osg::Vec3f& position, float radius); + bool isAnyObjectInRange(const osg::Vec3f& position, float radius) const; - void cleanupSummonedCreature (CreatureStats& casterStats, int creatureActorId); + void cleanupSummonedCreature(CreatureStats& casterStats, int creatureActorId) const; ///Returns the list of actors which are siding with the given actor in fights /**ie AiFollow or AiEscort is active and the target is the actor **/ - std::list getActorsSidingWith(const MWWorld::Ptr& actor); - std::list getActorsFollowing(const MWWorld::Ptr& actor); + std::vector getActorsSidingWith(const MWWorld::Ptr& actor, + bool excludeInfighting = false) const; + std::vector getActorsFollowing(const MWWorld::Ptr& actor) const; /// Recursive version of getActorsFollowing - void getActorsFollowing(const MWWorld::Ptr &actor, std::set& out); + void getActorsFollowing(const MWWorld::Ptr &actor, std::set& out) const; /// Recursive version of getActorsSidingWith - void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out); - /// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of actors mapped to their allies - void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies); + void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, + bool excludeInfighting = false) const; /// Get the list of AiFollow::mFollowIndex for all actors following this target - std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); + std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) const; + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) const; ///Returns the list of actors which are fighting the given actor /**ie AiCombat is active and the target is the actor **/ - std::list getActorsFighting(const MWWorld::Ptr& actor); + std::vector getActorsFighting(const MWWorld::Ptr& actor) const; /// Unlike getActorsFighting, also returns actors that *would* fight the given actor if they saw him. - std::list getEnemiesNearby(const MWWorld::Ptr& actor); + std::vector getEnemiesNearby(const MWWorld::Ptr& actor) const; void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; @@ -204,15 +171,53 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; - private: - void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); - void applyCureEffects (const MWWorld::Ptr& actor); + private: + enum class MusicType + { + Title, + Explore, + Battle + }; + + std::map mDeathCount; + std::list mActors; + std::map::iterator> mIndex; + float mTimerDisposeSummonsCorpses; + float mTimerUpdateHeadTrack = 0; + float mTimerUpdateEquippedLight = 0; + float mTimerUpdateHello = 0; + float mSneakTimer = 0; // Times update of sneak icon + float mSneakSkillTimer = 0; // Times sneak skill progress from "avoid notice" + float mActorsProcessingRange; + bool mSmoothMovement; + MusicType mCurrentMusic = MusicType::Title; + + void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const; + + void adjustMagicEffects(const MWWorld::Ptr& creature, float duration) const; + + void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const; + + void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const; + + void killDeadActors (); + + void purgeSpellEffects(int casterActorId) const; + + void predictAndAvoidCollisions(float duration) const; + + /** Start combat between two actors + @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; - PtrActorMap mActors; - float mTimerDisposeSummonsCorpses; - float mActorsProcessingRange; + /// 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; - bool mSmoothMovement; }; } diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index e27c9de495..8615af9860 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -6,6 +6,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" +#include "../mwmechanics/magiceffects.hpp" +#include "../mwmechanics/creaturestats.hpp" + +#include + namespace MWMechanics { MWWorld::Ptr getPlayer() @@ -23,4 +28,10 @@ namespace MWMechanics MWBase::World* world = MWBase::Environment::get().getWorld(); return (actor.getClass().canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor); } + + bool hasWaterWalking(const MWWorld::Ptr& actor) + { + const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + } } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index 1e993f5606..608e26774e 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -1,19 +1,6 @@ #ifndef OPENMW_MWMECHANICS_ACTORUTIL_H #define OPENMW_MWMECHANICS_ACTORUTIL_H -#include - -#include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/esmstore.hpp" - -#include "./creaturestats.hpp" - namespace MWWorld { class Ptr; @@ -21,70 +8,10 @@ namespace MWWorld namespace MWMechanics { - enum GreetingState - { - Greet_None, - Greet_InProgress, - Greet_Done - }; - MWWorld::Ptr getPlayer(); bool isPlayerInCombat(); bool canActorMoveByZAxis(const MWWorld::Ptr& actor); - - template - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) - { - T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); - switch(setting) - { - case MWMechanics::CreatureStats::AiSetting::AI_Hello: - copy.mAiData.mHello = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Fight: - copy.mAiData.mFight = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Flee: - copy.mAiData.mFlee = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Alarm: - copy.mAiData.mAlarm = value; - break; - default: - assert(0); - } - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - } - - template - void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) - { - T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); - for(auto& it : copy.mInventory.mList) - { - if(Misc::StringUtils::ciEqual(it.mItem, itemId)) - { - int sign = it.mCount < 1 ? -1 : 1; - it.mCount = sign * std::max(it.mCount * sign + amount, 0); - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - return; - } - } - if(amount > 0) - { - ESM::ContItem cont; - cont.mItem = itemId; - cont.mCount = amount; - copy.mInventory.mList.push_back(cont); - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - } - } - - template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); - template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); - template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); - template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); - template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); + bool hasWaterWalking(const MWWorld::Ptr& actor); } #endif diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index b4ddf0c030..ece071c601 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -1,6 +1,6 @@ #include "aiactivate.hpp" -#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -13,8 +13,8 @@ namespace MWMechanics { - AiActivate::AiActivate(const std::string &objectId) - : mObjectId(objectId) + AiActivate::AiActivate(std::string_view objectId, bool repeat) + : TypedAiPackage(repeat), mObjectId(objectId) { } @@ -22,7 +22,7 @@ namespace MWMechanics { const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager @@ -46,17 +46,18 @@ namespace MWMechanics void AiActivate::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr activate(new ESM::AiSequence::AiActivate()); + auto activate = std::make_unique(); activate->mTargetId = mObjectId; + activate->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Activate; - package.mPackage = activate.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(activate); + sequence.mPackages.push_back(std::move(package)); } AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate) - : mObjectId(activate->mTargetId) + : AiActivate(activate->mTargetId, activate->mRepeat) { } } diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp index dc7e0bb26b..eae5ec5b6b 100644 --- a/apps/openmw/mwmechanics/aiactivate.hpp +++ b/apps/openmw/mwmechanics/aiactivate.hpp @@ -4,6 +4,7 @@ #include "typedaipackage.hpp" #include +#include #include "pathfinding.hpp" @@ -24,7 +25,7 @@ namespace MWMechanics public: /// Constructor /** \param objectId Reference to object to activate **/ - explicit AiActivate(const std::string &objectId); + explicit AiActivate(std::string_view objectId, bool repeat); explicit AiActivate(const ESM::AiSequence::AiActivate* activate); diff --git a/apps/openmw/mwmechanics/aiavoiddoor.cpp b/apps/openmw/mwmechanics/aiavoiddoor.cpp index 73a6385638..e8cb71add0 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.cpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.cpp @@ -45,13 +45,13 @@ bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, CharacterCont return true; //Door is no longer opening ESM::Position tPos = mDoorPtr.getRefData().getPosition(); //Position of the door - float x = pos.pos[0] - tPos.pos[0]; - float y = pos.pos[1] - tPos.pos[1]; + float x = pos.pos[1] - tPos.pos[1]; + float y = pos.pos[0] - tPos.pos[0]; actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); // Turn away from the door and move when turn completed - if (zTurn(actor, std::atan2(x,y) + getAdjustedAngle(), osg::DegreesToRadians(5.f))) + if (zTurn(actor, std::atan2(y,x) + getAdjustedAngle(), osg::DegreesToRadians(5.f))) actor.getClass().getMovementSettings(actor).mPosition[1] = 1; else actor.getClass().getMovementSettings(actor).mPosition[1] = 0; @@ -80,7 +80,8 @@ bool MWMechanics::AiAvoidDoor::isStuck(const osg::Vec3f& actorPos) const void MWMechanics::AiAvoidDoor::adjustDirection() { - mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS, prng); } float MWMechanics::AiAvoidDoor::getAdjustedAngle() const diff --git a/apps/openmw/mwmechanics/aiavoiddoor.hpp b/apps/openmw/mwmechanics/aiavoiddoor.hpp index 1781c5e4a8..183f429f3f 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.hpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.hpp @@ -3,10 +3,6 @@ #include "typedaipackage.hpp" -#include - -#include - #include "../mwworld/class.hpp" #include "pathfinding.hpp" diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 2740355b57..94e4ecd955 100644 --- a/apps/openmw/mwmechanics/aibreathe.cpp +++ b/apps/openmw/mwmechanics/aibreathe.cpp @@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; - smoothTurn(actor, -osg::PI / 2, 0); + smoothTurn(actor, static_cast(-osg::PI_2), 0); return false; } diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 9ad7b4c56d..630c04a6a7 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -1,5 +1,7 @@ #include "aicast.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" @@ -46,26 +48,28 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac { return false; } + } - osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); - if (target.getClass().isActor()) - { - osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); - targetPos.z() += halfExtents.z() * 2 * 0.75f; - } + osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); + // If the target of an on-target spell is an actor that is not the caster + // the target position must be adjusted so that it's not casted at the actor's feet. + if (target != actor && target.getClass().isActor()) + { + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); + targetPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; + } - osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); - actorPos.z() += halfExtents.z() * 2 * 0.75f; + osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); + actorPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; - osg::Vec3f dir = targetPos - actorPos; + osg::Vec3f dir = targetPos - actorPos; - bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); - turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); + bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); + turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); - if (!turned) - return false; - } + if (!turned) + return false; // Check if the actor is already casting another spell bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index b98d5ef491..5bb03bb751 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -3,11 +3,12 @@ #include #include -#include +#include #include #include +#include #include "../mwphysics/collisiontype.hpp" @@ -127,16 +128,20 @@ namespace MWMechanics { //Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame. updateLOS(actor, target, duration, storage); - float targetReachedTolerance = 0.0f; - if (storage.mLOS) - targetReachedTolerance = storage.mAttackRange; - const bool is_target_reached = pathTo(actor, target.getRefData().getPosition().asVec3(), duration, targetReachedTolerance); + const float targetReachedTolerance = storage.mLOS && !storage.mUseCustomDestination + ? storage.mAttackRange : 0.0f; + const osg::Vec3f destination = storage.mUseCustomDestination + ? storage.mCustomDestination : target.getRefData().getPosition().asVec3(); + const bool is_target_reached = pathTo(actor, destination, duration, targetReachedTolerance); if (is_target_reached) storage.mReadyToAttack = true; } storage.updateCombatMove(duration); + storage.mRotateMove = false; if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); - storage.updateAttack(characterController); + if (storage.mRotateMove) + return false; + storage.updateAttack(actor, characterController); } else { @@ -144,19 +149,10 @@ namespace MWMechanics } storage.mActionCooldown -= duration; - float& timerReact = storage.mTimerReact; - if (timerReact < AI_REACTION_TIME) - { - timerReact += duration; - } - else - { - timerReact = 0; - if (attack(actor, target, storage, characterController)) - return true; - } + if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) + return false; - return false; + return attack(actor, target, storage, characterController); } bool AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController) @@ -172,10 +168,10 @@ namespace MWMechanics if (!canFight(actor, target)) { storage.stopAttack(); - characterController.setAttackingOrSpell(false); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); storage.mActionCooldown = 0.f; // Continue combat if target is player or player follower/escorter and an attack has been attempted - const std::list& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer()); + const auto& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer()); bool targetSidesWithPlayer = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end()); if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId()) @@ -189,7 +185,7 @@ namespace MWMechanics actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; - std::shared_ptr& currentAction = storage.mCurrentAction; + std::unique_ptr& currentAction = storage.mCurrentAction; if (!forceFlee) { @@ -204,7 +200,7 @@ namespace MWMechanics } else { - currentAction.reset(new ActionFlee()); + currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); } @@ -232,10 +228,9 @@ namespace MWMechanics const ESM::Weapon* weapon = currentAction->getWeapon(); ESM::Position pos = actor.getRefData().getPosition(); - osg::Vec3f vActorPos(pos.asVec3()); - osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); + const osg::Vec3f vActorPos(pos.asVec3()); + const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); - osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target); float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS); @@ -243,25 +238,80 @@ namespace MWMechanics if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed - osg::Vec3f& lastTargetPos = storage.mLastTargetPos; - vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); - lastTargetPos = vTargetPos; + osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, storage.mLastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); } else { + osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, false); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated } + storage.mLastTargetPos = vTargetPos; + if (storage.mReadyToAttack) { storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); } + + // If actor uses custom destination it has to try to rebuild path because environment can change + // (door is opened between actor and target) or target position has changed and current custom destination + // is not good enough to attack target. + if (storage.mCurrentAction->isAttackingOrSpell() + && ((!storage.mReadyToAttack && !mPathFinder.isPathConstructed()) + || (storage.mUseCustomDestination && (storage.mCustomDestination - vTargetPos).length() > rangeAttack))) + { + 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 auto pathGridGraph = getPathGridGraph(actor.getCell()); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + + if (!mPathFinder.isPathConstructed()) + { + // If there is no path, try to find a point on a line from the actor position to target projected + // on navmesh to attack the target from there. + const auto navigator = world->getNavigator(); + const auto hit = DetourNavigator::raycast(*navigator, agentBounds, vActorPos, vTargetPos, navigatorFlags); + + if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) + { + // If the point is close enough, try to find a path to that point. + mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + if (mPathFinder.isPathConstructed()) + { + // If path to that point is found use it as custom destination. + storage.mCustomDestination = *hit; + storage.mUseCustomDestination = true; + } + } + + if (!mPathFinder.isPathConstructed()) + { + storage.mUseCustomDestination = false; + storage.stopAttack(); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + currentAction = std::make_unique(); + actionCooldown = currentAction->getActionCooldown(); + storage.startFleeing(); + MWBase::Environment::get().getDialogueManager()->say(actor, "flee"); + } + } + else + { + storage.mUseCustomDestination = false; + } + } + return false; } @@ -301,7 +351,7 @@ namespace MWMechanics bool runFallback = true; - if (pathgrid && !actor.getClass().isPureWaterCreature(actor)) + if (pathgrid != nullptr && !pathgrid->mPoints.empty() && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; Misc::CoordinateConverter coords(storage.mCell->getCell()); @@ -320,7 +370,8 @@ namespace MWMechanics if (!points.empty()) { - ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size())]; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size(), prng)]; coords.toWorld(dest); state = AiCombatStorage::FleeState_RunToDestination; @@ -395,27 +446,61 @@ namespace MWMechanics storage.mCurrentAction->getCombatRange(isRangedCombat); float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f); float targetAngleRadians = storage.mMovement.mRotation[axis]; - smoothTurn(actor, targetAngleRadians, axis, eps); + storage.mRotateMove = !smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const { - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + if (mCachedTarget.isEmpty() || mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + { + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + } + return mCachedTarget; } void AiCombat::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr combat(new ESM::AiSequence::AiCombat()); + auto combat = std::make_unique(); combat->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Combat; - package.mPackage = combat.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(combat); + sequence.mPackages.push_back(std::move(package)); + } + + + AiCombatStorage::AiCombatStorage() : + mAttackCooldown(0.0f), + mReaction(MWBase::Environment::get().getWorld()->getPrng()), + mTimerCombatMove(0.0f), + mReadyToAttack(false), + mAttack(false), + mAttackRange(0.0f), + mCombatMove(false), + mRotateMove(false), + mLastTargetPos(0, 0, 0), + mCell(nullptr), + mCurrentAction(), + mActionCooldown(0.0f), + mStrength(), + mForceNoShortcut(false), + mShortcutFailPos(), + mMovement(), + mFleeState(FleeState_None), + mLOS(false), + mUpdateLOSTimer(0.0f), + mFleeBlindRunTimer(0.0f), + mUseCustomDestination(false), + mCustomDestination() + { + } void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + // get the range of the target's weapon MWWorld::Ptr targetWeapon = MWWorld::Ptr(); const MWWorld::Class& targetClass = target.getClass(); @@ -433,7 +518,7 @@ namespace MWMechanics if (mMovement.mPosition[0] || mMovement.mPosition[1]) { - mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(prng); mCombatMove = true; } else if (isDistantCombat) @@ -486,12 +571,12 @@ namespace MWMechanics // Otherwise apply a random side step (kind of dodging) with some probability // if actor is within range of target's weapon. if (std::abs(angleToTarget) > osg::PI / 4) - moveDuration = 0.2; - else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) - moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + moveDuration = 0.2f; + else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability(prng) < 0.25) + moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(prng); if (moveDuration > 0) { - mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right + mMovement.mPosition[0] = Misc::Rng::rollProbability(prng) < 0.5 ? 1.0f : -1.0f; // to the left/right mTimerCombatMove = moveDuration; mCombatMove = true; } @@ -525,12 +610,13 @@ namespace MWMechanics if (mAttackCooldown <= 0) { mAttack = true; // attack starts just now - characterController.setAttackingOrSpell(true); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(true); if (!distantCombat) characterController.setAIAttackType(chooseBestAttack(weapon)); - mStrength = Misc::Rng::rollClosedProbability(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mStrength = Misc::Rng::rollClosedProbability(prng); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -542,24 +628,24 @@ namespace MWMechanics // Say a provoking combat phrase const int iVoiceAttackOdds = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); - if (Misc::Rng::roll0to99() < iVoiceAttackOdds) + if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds) { MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } - mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); + mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9); } else mAttackCooldown -= AI_REACTION_TIME; } } - void AiCombatStorage::updateAttack(CharacterController& characterController) + void AiCombatStorage::updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController) { if (mAttack && (characterController.getAttackStrength() >= mStrength || characterController.readyToPrepareAttack())) { mAttack = false; } - characterController.setAttackingOrSpell(mAttack); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mAttack); } void AiCombatStorage::stopAttack() @@ -607,7 +693,8 @@ std::string chooseBestAttack(const ESM::Weapon* weapon) int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; - float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + float roll = Misc::Rng::rollClosedProbability(prng) * (slash + chop + thrust); if(roll <= slash) attackType = "slash"; else if(roll <= (slash + thrust)) @@ -616,7 +703,7 @@ std::string chooseBestAttack(const ESM::Weapon* weapon) attackType = "chop"; } else - MWMechanics::CharacterController::setAttackTypeRandomly(attackType); + attackType = MWMechanics::CharacterController::getRandomAttackType(); return attackType; } @@ -650,7 +737,7 @@ osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& t // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same osg::Vec3f vTargetPos = target.getRefData().getPosition().asVec3(); - osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target); + osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, true); float distToTarget = vDirToTarget.length(); osg::Vec3f vTargetMoveDir = vTargetPos - vLastTargetPos; @@ -670,16 +757,18 @@ osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& t float t_collision; float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; + if (projVelDirSquared > 0) + { + osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; + vTargetMoveDirNormalized.normalize(); - osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; - vTargetMoveDirNormalized.normalize(); - - float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product - projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); + float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product + projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); - if (projVelDirSquared > 0) t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir); - else t_collision = 0; // speed of projectile is not enough to reach moving target + } + else + t_collision = 0; // speed of projectile is not enough to reach moving target return vDirToTarget + vTargetMoveDir * t_collision; } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 64645ca941..1d8b8a38b3 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -2,6 +2,7 @@ #define GAME_MWMECHANICS_AICOMBAT_H #include "typedaipackage.hpp" +#include "aitemporarybase.hpp" #include "../mwworld/cellstore.hpp" // for Doors @@ -9,7 +10,7 @@ #include "pathfinding.hpp" #include "movement.hpp" -#include "obstacle.hpp" +#include "aitimer.hpp" namespace ESM { @@ -27,15 +28,16 @@ namespace MWMechanics struct AiCombatStorage : AiTemporaryBase { float mAttackCooldown; - float mTimerReact; + AiReactionTimer mReaction; float mTimerCombatMove; bool mReadyToAttack; bool mAttack; float mAttackRange; bool mCombatMove; + bool mRotateMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; - std::shared_ptr mCurrentAction; + std::unique_ptr mCurrentAction; float mActionCooldown; float mStrength; bool mForceNoShortcut; @@ -55,34 +57,17 @@ namespace MWMechanics float mFleeBlindRunTimer; ESM::Pathgrid::Point mFleeDest; - AiCombatStorage(): - mAttackCooldown(0.0f), - mTimerReact(AI_REACTION_TIME), - mTimerCombatMove(0.0f), - mReadyToAttack(false), - mAttack(false), - mAttackRange(0.0f), - mCombatMove(false), - mLastTargetPos(0,0,0), - mCell(nullptr), - mCurrentAction(), - mActionCooldown(0.0f), - mStrength(), - mForceNoShortcut(false), - mShortcutFailPos(), - mMovement(), - mFleeState(FleeState_None), - mLOS(false), - mUpdateLOSTimer(0.0f), - mFleeBlindRunTimer(0.0f) - {} + bool mUseCustomDestination; + osg::Vec3f mCustomDestination; + + AiCombatStorage(); void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); void updateCombatMove(float duration); void stopCombatMove(); void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, const ESM::Weapon* weapon, bool distantCombat); - void updateAttack(CharacterController& characterController); + void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController); void stopAttack(); void startFleeing(); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index c26454aab5..48861e19ba 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -1,7 +1,7 @@ #include "aicombataction.hpp" -#include -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -40,7 +40,7 @@ namespace MWMechanics void ActionSpell::prepare(const MWWorld::Ptr &actor) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Spell); if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); @@ -64,7 +64,7 @@ namespace MWMechanics { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string()); actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Spell); } float ActionEnchantedItem::getCombatRange(bool& isRanged) const @@ -85,8 +85,7 @@ namespace MWMechanics void ActionPotion::prepare(const MWWorld::Ptr &actor) { - actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor); - actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor); + actor.getClass().consume(mPotion, actor); } void ActionWeapon::prepare(const MWWorld::Ptr &actor) @@ -107,7 +106,7 @@ namespace MWMechanics equip.execute(actor); } } - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Weapon); } float ActionWeapon::getCombatRange(bool& isRanged) const @@ -141,14 +140,14 @@ namespace MWMechanics return mWeapon.get()->mBase; } - std::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) + std::unique_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); float bestActionRating = 0.f; float antiFleeRating = 0.f; // Default to hand-to-hand combat - std::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); + std::unique_ptr bestAction = std::make_unique(MWWorld::Ptr()); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { bestAction->prepare(actor); @@ -165,7 +164,7 @@ namespace MWMechanics if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionPotion(*it)); + bestAction = std::make_unique(*it); antiFleeRating = std::numeric_limits::max(); } } @@ -176,7 +175,7 @@ namespace MWMechanics if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionEnchantedItem(it)); + bestAction = std::make_unique(it); antiFleeRating = std::numeric_limits::max(); } } @@ -202,25 +201,25 @@ namespace MWMechanics ammo = bestBolt; bestActionRating = rating; - bestAction.reset(new ActionWeapon(*it, ammo)); + bestAction = std::make_unique(*it, ammo); antiFleeRating = vanillaRateWeaponAndAmmo(*it, ammo, actor, enemy); } } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionSpell(it->first->mId)); - antiFleeRating = vanillaRateSpell(it->first, actor, enemy); + bestAction = std::make_unique(spell->mId); + antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } if (makeFleeDecision(actor, enemy, antiFleeRating)) - bestAction.reset(new ActionFlee()); + bestAction = std::make_unique(); if (bestAction.get()) bestAction->prepare(actor); @@ -266,9 +265,9 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; @@ -325,7 +324,7 @@ namespace MWMechanics static const float fHandToHandReach = gmst.find("fHandToHandReach")->mValue.getFloat(); dist = fHandToHandReach; } - else if (stats.getDrawState() == MWMechanics::DrawState_Spell) + else if (stats.getDrawState() == MWMechanics::DrawState::Spell) { dist = 1.0f; if (!selectedSpellId.empty()) @@ -469,15 +468,14 @@ namespace MWMechanics const CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - int flee = stats.getAiSetting(CreatureStats::AI_Flee).getModified(); + const int flee = stats.getAiSetting(AiSetting::Flee).getModified(); if (flee >= 100) return flee; static const float fAIFleeHealthMult = gmst.find("fAIFleeHealthMult")->mValue.getFloat(); static const float fAIFleeFleeMult = gmst.find("fAIFleeFleeMult")->mValue.getFloat(); - float healthPercentage = (stats.getHealth().getModified() == 0.0f) - ? 1.0f : stats.getHealth().getCurrent() / stats.getHealth().getModified(); + float healthPercentage = stats.getHealth().getRatio(false); float rating = (1.0f - healthPercentage) * fAIFleeHealthMult + flee * fAIFleeFleeMult; static const int iWereWolfLevelToAttack = gmst.find("iWereWolfLevelToAttack")->mValue.getInteger(); diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index 77a19f8044..56d2247e99 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -3,8 +3,6 @@ #include -#include - #include "../mwworld/ptr.hpp" #include "../mwworld/containerstore.hpp" @@ -87,7 +85,7 @@ namespace MWMechanics const ESM::Weapon* getWeapon() const override; }; - std::shared_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + std::unique_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float getBestActionRating(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy); float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool minusZDist=false); diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 5dc1e44db0..0426035b8d 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -1,7 +1,7 @@ #include "aiescort.hpp" -#include -#include +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -20,28 +20,25 @@ namespace MWMechanics { - AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(std::string_view actorId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mTargetActorRefId = actorId; + mTargetActorRefId = std::string(actorId); } - AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z) - : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(std::string_view actorId, std::string_view cellId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mTargetActorRefId = actorId; + mTargetActorRefId = std::string(actorId); } AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) - : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(escort->mRemainingDuration > 0) + : TypedAiPackage(escort->mRepeat), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) + , mDuration(escort->mData.mDuration) , mRemainingDuration(escort->mRemainingDuration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) @@ -67,29 +64,31 @@ namespace MWMechanics if (!mCellId.empty() && mCellId != actor.getCell()->getCell()->getCellId().mWorldspace) return false; // Not in the correct cell, pause and rely on the player to go back through a teleport door - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); const osg::Vec3f leaderPos = actor.getRefData().getPosition().asVec3(); const osg::Vec3f followerPos = follower.getRefData().getPosition().asVec3(); + const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) { const osg::Vec3f dest(mX, mY, mZ); - if (pathTo(actor, dest, duration)) //Returns true on path complete + if (pathTo(actor, dest, duration, maxHalfExtent)) //Returns true on path complete { mRemainingDuration = mDuration; return true; } - mMaxDist = 450; + mMaxDist = maxHalfExtent + 450.0f; } else { // Stop moving if the player is too far away MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - mMaxDist = 250; + mMaxDist = maxHalfExtent + 250.0f; } return false; @@ -97,19 +96,21 @@ namespace MWMechanics void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr escort(new ESM::AiSequence::AiEscort()); + auto escort = std::make_unique(); escort->mData.mX = mX; escort->mData.mY = mY; escort->mData.mZ = mZ; + escort->mData.mDuration = mDuration; escort->mTargetId = mTargetActorRefId; escort->mTargetActorId = mTargetActorId; escort->mRemainingDuration = mRemainingDuration; escort->mCellId = mCellId; + escort->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Escort; - package.mPackage = escort.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(escort); + sequence.mPackages.push_back(std::move(package)); } void AiEscort::fastForward(const MWWorld::Ptr& actor, AiState &state) diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 27a177893d..0f601a29ed 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -4,6 +4,7 @@ #include "typedaipackage.hpp" #include +#include namespace ESM { @@ -22,11 +23,11 @@ namespace MWMechanics /// Implementation of AiEscort /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time \implement AiEscort **/ - AiEscort(const std::string &actorId, int duration, float x, float y, float z); + AiEscort(std::string_view actorId, int duration, float x, float y, float z, bool repeat); /// Implementation of AiEscortCell /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time \implement AiEscortCell **/ - AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z); + AiEscort(std::string_view actorId, std::string_view cellId, int duration, float x, float y, float z, bool repeat); AiEscort(const ESM::AiSequence::AiEscort* escort); diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index b3c308d75f..9a64a5bee3 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -1,7 +1,7 @@ #include "aifollow.hpp" -#include -#include +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -14,38 +14,32 @@ #include "movement.hpp" #include "steering.hpp" -namespace MWMechanics +namespace { -int AiFollow::mFollowIndexCounter = 0; - -AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +osg::Vec3f::value_type getHalfExtents(const MWWorld::ConstPtr& actor) { - mTargetActorRefId = actorId; + if(actor.getClass().isNpc()) + return 64; + return MWBase::Environment::get().getWorld()->getHalfExtents(actor).y(); +} } -AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) +namespace MWMechanics { - mTargetActorRefId = actorId; -} +int AiFollow::mFollowIndexCounter = 0; -AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +AiFollow::AiFollow(std::string_view actorId, float duration, float x, float y, float z, bool repeat) +: TypedAiPackage(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) , mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + mTargetActorRefId = std::string(actorId); } -AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +AiFollow::AiFollow(std::string_view actorId, std::string_view cellId, float duration, float x, float y, float z, bool repeat) +: TypedAiPackage(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) , mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) { - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + mTargetActorRefId = std::string(actorId); } AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) @@ -58,12 +52,9 @@ AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) } AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) - : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded)) + : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded).withRepeat(follow->mRepeat)) , mAlwaysFollow(follow->mAlwaysFollow) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(follow->mRemainingDuration) + , mDuration(follow->mData.mDuration) , mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) @@ -81,7 +72,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) return false; - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); AiFollowStorage& storage = state.get(); @@ -113,24 +104,23 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte if (!mActive) return false; - // The distances below are approximations based on observations of the original engine. - // If only one actor is following the target, it uses 186. - // If there are multiple actors following the same target, they form a group with each group member at 313 + (130 * i) distance to the target. - - short followDistance = 186; - std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingIndices(target); - if (followers.size() >= 2) + // 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; + auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target); + if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex) { - followDistance = 313; - short i = 0; - followers.sort(); - for (int followIndex : followers) + for(auto& follower : followers) { - if (followIndex == mFollowIndex) - followDistance += 130 * i; - ++i; + auto halfExtent = getHalfExtents(follower.second); + if(halfExtent > floatingDistance) + floatingDistance = halfExtent; } + floatingDistance += 128; } + floatingDistance += getHalfExtents(target) + 64; + floatingDistance += getHalfExtents(actor) * 2; + short followDistance = static_cast(floatingDistance); if (!mAlwaysFollow) //Update if you only follow for a bit { @@ -151,12 +141,15 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte if (actor.getCell()->isExterior()) //Outside? { if (mCellId == "") //No cell to travel to + { + mRemainingDuration = mDuration; return true; + } } - else + else if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to { - if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to - return true; + mRemainingDuration = mDuration; + return true; } } } @@ -208,10 +201,11 @@ bool AiFollow::isCommanded() const void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr follow(new ESM::AiSequence::AiFollow()); + auto follow = std::make_unique(); follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; + follow->mData.mDuration = mDuration; follow->mTargetId = mTargetActorRefId; follow->mTargetActorId = mTargetActorId; follow->mRemainingDuration = mRemainingDuration; @@ -219,11 +213,12 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const follow->mAlwaysFollow = mAlwaysFollow; follow->mCommanded = isCommanded(); follow->mActive = mActive; + follow->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Follow; - package.mPackage = follow.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(follow); + sequence.mPackages.push_back(std::move(package)); } int AiFollow::getFollowIndex() const diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index e6aeebb246..c1969a7e27 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -2,22 +2,19 @@ #define GAME_MWMECHANICS_AIFOLLOW_H #include "typedaipackage.hpp" +#include "aitemporarybase.hpp" #include +#include #include #include "../mwworld/ptr.hpp" -#include "pathfinding.hpp" - -namespace ESM -{ -namespace AiSequence +namespace ESM::AiSequence { struct AiFollow; } -} namespace MWMechanics { @@ -42,12 +39,10 @@ namespace MWMechanics class AiFollow final : public TypedAiPackage { public: - AiFollow(const std::string &actorId, float duration, float x, float y, float z); - AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z); /// Follow Actor for duration or until you arrive at a world position - AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z); + AiFollow(std::string_view actorId, float duration, float x, float y, float z, bool repeat); /// Follow Actor for duration or until you arrive at a position in a cell - AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z); + AiFollow(std::string_view actorId, std::string_view cellId, float duration, float x, float y, float z, bool repeat); /// Follow Actor indefinitively AiFollow(const MWWorld::Ptr& actor, bool commanded=false); diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bffd28baa..906894dd9c 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -1,8 +1,7 @@ #include "aipackage.hpp" -#include -#include -#include +#include +#include #include #include #include @@ -15,8 +14,6 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwphysics/collisiontype.hpp" - #include "pathgrid.hpp" #include "creaturestats.hpp" #include "movement.hpp" @@ -25,12 +22,32 @@ #include +namespace +{ + float divOrMax(float dividend, float divisor) + { + return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; + } + + float getPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) + { + const float actorTolerance = 2 * speed * duration + 1.2 * std::max(halfExtents.x(), halfExtents.y()); + return std::max(MWMechanics::MIN_TOLERANCE, actorTolerance); + } + + bool canOpenDoors(const MWWorld::Ptr& ptr) + { + return ptr.getClass().isBipedal(ptr) || ptr.getClass().hasInventoryStore(ptr); + } +} + MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : mTypeId(typeId), mOptions(options), - mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild + mReaction(MWBase::Environment::get().getWorld()->getPrng()), mTargetActorRefId(""), mTargetActorId(-1), + mCachedTarget(), mRotateOnTheRunChecks(0), mIsShortcutting(false), mShortcutProhibited(false), @@ -40,47 +57,63 @@ MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { + if (!mCachedTarget.isEmpty()) + { + if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + mCachedTarget = MWWorld::Ptr(); + else + return mCachedTarget; + } + if (mTargetActorId == -2) return MWWorld::Ptr(); if (mTargetActorId == -1) { - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); - if (target.isEmpty()) + if (mTargetActorRefId.empty()) { mTargetActorId = -2; - return target; + return MWWorld::Ptr(); + } + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); + if (mCachedTarget.isEmpty()) + { + mTargetActorId = -2; + return mCachedTarget; } else - mTargetActorId = target.getClass().getCreatureStats(target).getActorId(); + mTargetActorId = mCachedTarget.getClass().getCreatureStats(mCachedTarget).getActorId(); } if (mTargetActorId != -1) - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); else return MWWorld::Ptr(); + + return mCachedTarget; } void MWMechanics::AiPackage::reset() { // reset all members - mTimer = AI_REACTION_TIME + 1.0f; + mReaction.reset(); mIsShortcutting = false; mShortcutProhibited = false; mShortcutFailPos = osg::Vec3f(); + mCachedTarget = MWWorld::Ptr(); mPathFinder.clearPath(); mObstacleCheck.clear(); } -bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance) +bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + float destTolerance, float endTolerance, PathType pathType) { - mTimer += duration; //Update timer + const Misc::TimerStatus timerStatus = mReaction.update(duration); const osg::Vec3f position = actor.getRefData().getPosition().asVec3(); //position of the actor MWBase::World* world = MWBase::Environment::get().getWorld(); - - const osg::Vec3f halfExtents = world->getHalfExtents(actor); + 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 @@ -90,21 +123,23 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); + world->updateActorPath(actor, mPathFinder.getPath(), agentBounds, position, dest); return false; } + mLastDestinationTolerance = destTolerance; + const float distToTarget = distance(position, dest); const bool isDestReached = (distToTarget <= destTolerance); + const bool actorCanMoveByZ = canActorMoveByZAxis(actor); - if (!isDestReached && mTimer > AI_REACTION_TIME) + if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { - if (actor.getClass().isBipedal(actor)) + if (canOpenDoors(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; bool destInLOS = false; - const bool actorCanMoveByZ = canActorMoveByZAxis(actor); // Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions. mIsShortcutting = actorCanMoveByZ @@ -114,9 +149,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { if (wasShortcutting || doesPathNeedRecalc(dest, actor)) // if need to rebuild path { - const auto pathfindingHalfExtents = world->getPathfindingHalfExtents(actor); - mPathFinder.buildPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), - pathfindingHalfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), + agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -142,15 +176,15 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go } } - - mTimer = 0; } - const float actorTolerance = 2 * actor.getClass().getMaxSpeed(actor) * duration - + 1.2 * std::max(halfExtents.x(), halfExtents.y()); - const float pointTolerance = std::max(MIN_TOLERANCE, actorTolerance); + const float pointTolerance = getPointTolerance(actor.getClass().getMaxSpeed(actor), duration, + world->getHalfExtents(actor)); - mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE); + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE, + /*shortenIfAlmostStraight=*/smoothMovement, actorCanMoveByZ, + agentBounds, getNavigatorFlags(actor)); if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished { @@ -160,8 +194,10 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& world->removeActorPath(actor); return true; } + else if (mPathFinder.getPath().empty()) + return false; - world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); + world->updateActorPath(actor, mPathFinder.getPath(), agentBounds, position, dest); if (mRotateOnTheRunChecks == 0 || isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point @@ -175,10 +211,9 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& zTurn(actor, zAngleToNext); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); - const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front(); + const auto destination = getNextPathPoint(dest); mObstacleCheck.update(actor, destination, duration); - static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); if (smoothMovement) { const float smoothTurnReservedDist = 150; @@ -214,7 +249,7 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (!door.isEmpty() && actor.getClass().isBipedal(actor)) + if (!door.isEmpty() && canOpenDoors(actor)) { openDoors(actor); } @@ -411,16 +446,27 @@ bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& act DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const { + static const bool allowToFollowOverWaterSurface = Settings::Manager::getBool("allow actors to follow over water surface", "Game"); + const MWWorld::Class& actorClass = actor.getClass(); DetourNavigator::Flags result = DetourNavigator::Flag_none; - if (actorClass.isPureWaterCreature(actor) || (getTypeId() != AiPackageTypeId::Wander && actorClass.canSwim(actor))) + if ((actorClass.isPureWaterCreature(actor) + || (getTypeId() != AiPackageTypeId::Wander + && ((allowToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) + || actorClass.canSwim(actor) + || hasWaterWalking(actor))) + ) && actorClass.getSwimSpeed(actor) > 0) result |= DetourNavigator::Flag_swim; - if (actorClass.canWalk(actor)) + if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) + { result |= DetourNavigator::Flag_walk; + if (getTypeId() == AiPackageTypeId::Travel) + result |= DetourNavigator::Flag_usePathgrid; + } - if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) + if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; @@ -432,20 +478,43 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - if (flags & DetourNavigator::Flag_swim) - costs.mWater = costs.mWater / actorClass.getSwimSpeed(actor); + const float swimSpeed = (flags & DetourNavigator::Flag_swim) == 0 + ? 0.0f + : actorClass.getSwimSpeed(actor); - if (flags & DetourNavigator::Flag_walk) + const float walkSpeed = [&] { - float walkCost; + if ((flags & DetourNavigator::Flag_walk) == 0) + return 0.0f; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = 1.0 / actorClass.getWalkSpeed(actor); - else - walkCost = 1.0 / actorClass.getRunSpeed(actor); - costs.mDoor = costs.mDoor * walkCost; - costs.mPathgrid = costs.mPathgrid * walkCost; - costs.mGround = costs.mGround * walkCost; - } + return actorClass.getWalkSpeed(actor); + return actorClass.getRunSpeed(actor); + } (); + + const float maxSpeed = std::max(swimSpeed, walkSpeed); + + if (maxSpeed == 0) + return costs; + + const float swimFactor = swimSpeed / maxSpeed; + const float walkFactor = walkSpeed / maxSpeed; + + costs.mWater = divOrMax(costs.mWater, swimFactor); + costs.mDoor = divOrMax(costs.mDoor, walkFactor); + costs.mPathgrid = divOrMax(costs.mPathgrid, walkFactor); + costs.mGround = divOrMax(costs.mGround, walkFactor); return costs; } + +osg::Vec3f MWMechanics::AiPackage::getNextPathPoint(const osg::Vec3f& destination) const +{ + return mPathFinder.getPath().empty() ? destination : mPathFinder.getPath().front(); +} + +float MWMechanics::AiPackage::getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const +{ + if (mPathFinder.getPathSize() <= 1) + return std::max(DEFAULT_TOLERANCE, mLastDestinationTolerance); + return getPointTolerance(speed, duration, halfExtents); +} diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 4201de5c84..684ff3338d 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -3,18 +3,15 @@ #include -#include #include #include "pathfinding.hpp" #include "obstacle.hpp" -#include "aistate.hpp" #include "aipackagetypeid.hpp" +#include "aitimer.hpp" +#include "aistatefwd.hpp" -namespace MWWorld -{ - class Ptr; -} +#include "../mwworld/ptr.hpp" namespace ESM { @@ -28,8 +25,6 @@ namespace ESM namespace MWMechanics { - const float AI_REACTION_TIME = 0.25f; - class CharacterController; class PathgridGraph; @@ -110,7 +105,7 @@ namespace MWMechanics /// Upon adding this Ai package, should the Ai Sequence attempt to cancel previous Ai packages (default true)? bool shouldCancelPreviousAi() const { return mOptions.mShouldCancelPreviousAi; } - /// Return true if this package should repeat. Currently only used for Wander packages. + /// Return true if this package should repeat. bool getRepeat() const { return mOptions.mRepeat; } virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } @@ -124,10 +119,15 @@ namespace MWMechanics /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing. static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest); + osg::Vec3f getNextPathPoint(const osg::Vec3f& destination) const; + + float getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const; + protected: /// Handles path building and shortcutting with obstacles avoiding /** \return If the actor has arrived at his destination **/ - bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f); + bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + float destTolerance = 0.0f, float endTolerance = 0.0f, PathType pathType = PathType::Full); /// Check if there aren't any obstacles along the path to make shortcut possible /// If a shortcut is possible then path will be cleared and filled with the destination point. @@ -158,16 +158,18 @@ namespace MWMechanics PathFinder mPathFinder; ObstacleCheck mObstacleCheck; - float mTimer; + AiReactionTimer mReaction; std::string mTargetActorRefId; mutable int mTargetActorId; + mutable MWWorld::Ptr mCachedTarget; short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility bool mIsShortcutting; // if shortcutting at the moment bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt osg::Vec3f mShortcutFailPos; // position of last shortcut fail + float mLastDestinationTolerance = 0; private: bool isNearInactiveCell(osg::Vec3f position); diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index bfe860d6d9..6e60a79b3f 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -1,6 +1,6 @@ #include "aipursue.hpp" -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -8,7 +8,6 @@ #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/action.hpp" #include "movement.hpp" #include "creaturestats.hpp" @@ -45,7 +44,7 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte if (target.getClass().getCreatureStats(target).isDead()) return true; - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); //Set the target destination const osg::Vec3f dest = target.getRefData().getPosition().asVec3(); @@ -53,9 +52,14 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte const float pathTolerance = 100.f; - if (pathTo(actor, dest, duration, pathTolerance) && - std::abs(dest.z() - actorPos.z()) < pathTolerance) // check the true distance in case the target is far away in Z-direction + // check the true distance in case the target is far away in Z-direction + bool reached = pathTo(actor, dest, duration, pathTolerance, (actorPos - dest).length(), PathType::Partial) && + std::abs(dest.z() - actorPos.z()) < pathTolerance; + + if (reached) { + if (!MWBase::Environment::get().getWorld()->getLOS(target, actor)) + return false; MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, actor); //Arrest player when reached return true; } @@ -67,18 +71,26 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte MWWorld::Ptr AiPursue::getTarget() const { - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + if (!mCachedTarget.isEmpty()) + { + if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + mCachedTarget = MWWorld::Ptr(); + else + return mCachedTarget; + } + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + return mCachedTarget; } void AiPursue::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr pursue(new ESM::AiSequence::AiPursue()); + auto pursue = std::make_unique(); pursue->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Pursue; - package.mPackage = pursue.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(pursue); + sequence.mPackages.push_back(std::move(package)); } } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 57d32898cc..a9de4a5152 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -1,11 +1,10 @@ #include "aisequence.hpp" #include +#include #include -#include - -#include "../mwbase/world.hpp" +#include #include "aipackage.hpp" #include "aistate.hpp" @@ -31,16 +30,18 @@ void AiSequence::copy (const AiSequence& sequence) // We need to keep an AiWander storage, if present - it has a state machine. // Not sure about another temporary storages sequence.mAiState.copy(mAiState); + + mNumCombatPackages = sequence.mNumCombatPackages; + mNumPursuitPackages = sequence.mNumPursuitPackages; } -AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(AiPackageTypeId::None) {} +AiSequence::AiSequence() : mDone (false), mLastAiPackage(AiPackageTypeId::None) {} AiSequence::AiSequence (const AiSequence& sequence) { copy (sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; - mRepeat = sequence.mRepeat; } AiSequence& AiSequence::operator= (const AiSequence& sequence) @@ -61,6 +62,28 @@ AiSequence::~AiSequence() clear(); } +void AiSequence::onPackageAdded(const AiPackage& package) +{ + if (package.getTypeId() == AiPackageTypeId::Combat) + mNumCombatPackages++; + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages++; + + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); +} + +void AiSequence::onPackageRemoved(const AiPackage& package) +{ + if (package.getTypeId() == AiPackageTypeId::Combat) + mNumCombatPackages--; + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages--; + + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); +} + AiPackageTypeId AiSequence::getTypeId() const { if (mPackages.empty()) @@ -90,42 +113,30 @@ bool AiSequence::getCombatTargets(std::vector &targetActors) const return !targetActors.empty(); } -std::list>::const_iterator AiSequence::begin() const +AiPackages::iterator AiSequence::erase(AiPackages::iterator package) { - return mPackages.begin(); -} + // Not sure if manually terminated packages should trigger mDone, probably not? + auto& ptr = *package; + onPackageRemoved(*ptr); -std::list>::const_iterator AiSequence::end() const -{ - return mPackages.end(); + return mPackages.erase(package); } -void AiSequence::erase(std::list>::const_iterator package) +bool AiSequence::isInCombat() const { - // Not sure if manually terminated packages should trigger mDone, probably not? - for(auto it = mPackages.begin(); it != mPackages.end(); ++it) - { - if (package == it) - { - mPackages.erase(it); - return; - } - } - throw std::runtime_error("can't find package to erase"); + return mNumCombatPackages > 0; } -bool AiSequence::isInCombat() const +bool AiSequence::isInPursuit() const { - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) - { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) - return true; - } - return false; + return mNumPursuitPackages > 0; } bool AiSequence::isEngagedWithActor() const { + if (!isInCombat()) + return false; + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) @@ -140,16 +151,18 @@ bool AiSequence::isEngagedWithActor() const bool AiSequence::hasPackage(AiPackageTypeId typeId) const { - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + auto it = std::find_if(mPackages.begin(), mPackages.end(), [typeId](const auto& package) { - if ((*it)->getTypeId() == typeId) - return true; - } - return false; + return package->getTypeId() == typeId; + }); + return it != mPackages.end(); } bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const { + if (!isInCombat()) + return false; + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) @@ -161,32 +174,42 @@ bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const return false; } -void AiSequence::stopCombat() +void AiSequence::removePackagesById(AiPackageTypeId id) { - for(auto it = mPackages.begin(); it != mPackages.end(); ) + for (auto it = mPackages.begin(); it != mPackages.end(); ) { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + if ((*it)->getTypeId() == id) { - it = mPackages.erase(it); + it = erase(it); } else ++it; } } -void AiSequence::stopPursuit() +void AiSequence::stopCombat() +{ + removePackagesById(AiPackageTypeId::Combat); +} + +void AiSequence::stopCombat(const std::vector& targets) { for(auto it = mPackages.begin(); it != mPackages.end(); ) { - if ((*it)->getTypeId() == AiPackageTypeId::Pursue) + if ((*it)->getTypeId() == AiPackageTypeId::Combat && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end()) { - it = mPackages.erase(it); + it = erase(it); } else ++it; } } +void AiSequence::stopPursuit() +{ + removePackagesById(AiPackageTypeId::Pursue); +} + bool AiSequence::isPackageDone() const { return mDone; @@ -203,112 +226,124 @@ namespace void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) { - if(actor != getPlayer()) + if (actor == getPlayer()) { - if (mPackages.empty()) - { - mLastAiPackage = AiPackageTypeId::None; - return; - } + // Players don't use this. + return; + } - auto packageIt = mPackages.begin(); - MWMechanics::AiPackage* package = packageIt->get(); - if (!package->alwaysActive() && outOfRange) - return; + if (mPackages.empty()) + { + mLastAiPackage = AiPackageTypeId::None; + return; + } - auto packageTypeId = package->getTypeId(); - // workaround ai packages not being handled as in the vanilla engine - if (isActualAiPackage(packageTypeId)) - mLastAiPackage = packageTypeId; - // if active package is combat one, choose nearest target - if (packageTypeId == AiPackageTypeId::Combat) - { - auto itActualCombat = mPackages.end(); + auto* package = mPackages.front().get(); + if (!package->alwaysActive() && outOfRange) + return; - float nearestDist = std::numeric_limits::max(); - osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); + auto packageTypeId = package->getTypeId(); + // workaround ai packages not being handled as in the vanilla engine + if (isActualAiPackage(packageTypeId)) + mLastAiPackage = packageTypeId; + // if active package is combat one, choose nearest target + if (packageTypeId == AiPackageTypeId::Combat) + { + auto itActualCombat = mPackages.end(); - float bestRating = 0.f; + float nearestDist = std::numeric_limits::max(); + osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); - for (auto it = mPackages.begin(); it != mPackages.end();) - { - if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; + float bestRating = 0.f; - MWWorld::Ptr target = (*it)->getTarget(); + for (auto it = mPackages.begin(); it != mPackages.end();) + { + if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; - // target disappeared (e.g. summoned creatures) - if (target.isEmpty()) - { - it = mPackages.erase(it); - } - else - { - float rating = MWMechanics::getBestActionRating(actor, target); + MWWorld::Ptr target = (*it)->getTarget(); - const ESM::Position &targetPos = target.getRefData().getPosition(); + // target disappeared (e.g. summoned creatures) + if (target.isEmpty()) + { + it = erase(it); + } + else + { + float rating = MWMechanics::getBestActionRating(actor, target); - float distTo = (targetPos.asVec3() - vActorPos).length2(); + const ESM::Position &targetPos = target.getRefData().getPosition(); - // Small threshold for changing target - if (it == mPackages.begin()) - distTo = std::max(0.f, distTo - 2500.f); + float distTo = (targetPos.asVec3() - vActorPos).length2(); - // if a target has higher priority than current target or has same priority but closer - if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) - { - nearestDist = distTo; - itActualCombat = it; - bestRating = rating; - } - ++it; + // Small threshold for changing target + if (it == mPackages.begin()) + distTo = std::max(0.f, distTo - 2500.f); + + // if a target has higher priority than current target or has same priority but closer + if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) + { + nearestDist = distTo; + itActualCombat = it; + bestRating = rating; } + ++it; } + } - assert(!mPackages.empty()); - - if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) - { - assert(itActualCombat != mPackages.end()); - // move combat package with nearest target to the front - mPackages.splice(mPackages.begin(), mPackages, itActualCombat); - } + if (mPackages.empty()) + return; - packageIt = mPackages.begin(); - package = packageIt->get(); - packageTypeId = package->getTypeId(); + if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) + { + assert(itActualCombat != mPackages.end()); + // move combat package with nearest target to the front + std::rotate(mPackages.begin(), itActualCombat, std::next(itActualCombat)); } - try + package = mPackages.front().get(); + packageTypeId = package->getTypeId(); + } + + try + { + if (package->execute(actor, characterController, mAiState, duration)) { - if (package->execute(actor, characterController, mAiState, duration)) + // Put repeating non-combat AI packages on the end of the stack so they can be used again + if (isActualAiPackage(packageTypeId) && package->getRepeat()) { - // Put repeating noncombat AI packages on the end of the stack so they can be used again - if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) - { - package->reset(); - mPackages.push_back(package->clone()); - } - // To account for the rare case where AiPackage::execute() queued another AI package - // (e.g. AiPursue executing a dialogue script that uses startCombat) - mPackages.erase(packageIt); - if (isActualAiPackage(packageTypeId)) - mDone = true; - } - else - { - mDone = false; + package->reset(); + mPackages.push_back(package->clone()); } + + // The active package is typically the first entry, this is however not always the case + // e.g. AiPursue executing a dialogue script that uses startCombat adds a combat package to the front + // due to the priority. + auto activePackageIt = std::find_if(mPackages.begin(), mPackages.end(), [&](auto& entry) + { + return entry.get() == package; + }); + + erase(activePackageIt); + + if (isActualAiPackage(packageTypeId)) + mDone = true; } - catch (std::exception& e) + else { - Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); + mDone = false; } } + catch (std::exception& e) + { + Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); + } } void AiSequence::clear() { mPackages.clear(); + mNumCombatPackages = 0; + mNumPursuitPackages = 0; } void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) @@ -352,39 +387,41 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo { if((*it)->canCancel()) { - it = mPackages.erase(it); + it = erase(it); } else ++it; } - mRepeat=false; } // insert new package in correct place depending on priority for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { - // We should keep current AiCast package, if we try to add a new one. + // We should override current AiCast package, if we try to add a new one. if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Cast && package.getTypeId() == MWMechanics::AiPackageTypeId::Cast) { - continue; + *it = package.clone(); + return; } if((*it)->getPriority() <= package.getPriority()) { + onPackageAdded(package); mPackages.insert(it, package.clone()); return; } } + onPackageAdded(package); mPackages.push_back(package.clone()); // Make sure that temporary storage is empty if (cancelOther) { - mAiState.moveIn(new AiCombatStorage()); - mAiState.moveIn(new AiFollowStorage()); - mAiState.moveIn(new AiWanderStorage()); + mAiState.moveIn(std::make_unique()); + mAiState.moveIn(std::make_unique()); + mAiState.moveIn(std::make_unique()); } } @@ -393,7 +430,7 @@ bool MWMechanics::AiSequence::isEmpty() const return mPackages.empty(); } -const AiPackage& MWMechanics::AiSequence::getActivePackage() +const AiPackage& MWMechanics::AiSequence::getActivePackage() const { if(mPackages.empty()) throw std::runtime_error(std::string("No AI Package!")); @@ -402,10 +439,6 @@ const AiPackage& MWMechanics::AiSequence::getActivePackage() void AiSequence::fill(const ESM::AIPackageList &list) { - // If there is more than one package in the list, enable repeating - if (!list.mList.empty() && list.mList.begin() != (list.mList.end()-1)) - mRepeat = true; - for (const auto& esmPackage : list.mList) { std::unique_ptr package; @@ -421,23 +454,25 @@ void AiSequence::fill(const ESM::AIPackageList &list) else if (esmPackage.mType == ESM::AI_Escort) { ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); + package = std::make_unique(data.mId.toStringView(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Travel) { ESM::AITravel data = esmPackage.mTravel; - package = std::make_unique(data.mX, data.mY, data.mZ); + package = std::make_unique(data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Activate) { ESM::AIActivate data = esmPackage.mActivate; - package = std::make_unique(data.mName.toString()); + package = std::make_unique(data.mName.toStringView(), data.mShouldRepeat != 0); } else //if (esmPackage.mType == ESM::AI_Follow) { ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); + package = std::make_unique(data.mId.toStringView(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } + + onPackageAdded(*package); mPackages.push_back(std::move(package)); } } @@ -455,17 +490,6 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) if (!sequence.mPackages.empty()) clear(); - // If there is more than one non-combat, non-pursue package in the list, enable repeating. - int count = 0; - for (auto& container : sequence.mPackages) - { - if (isActualAiPackage(static_cast(container.mType))) - count++; - } - - if (count > 1) - mRepeat = true; - // Load packages for (auto& container : sequence.mPackages) { @@ -474,41 +498,41 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) { case ESM::AiSequence::Ai_Wander: { - package.reset(new AiWander(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } case ESM::AiSequence::Ai_Travel: { - const auto source = static_cast(container.mPackage); - if (source->mHidden) - package.reset(new AiInternalTravel(source)); + const ESM::AiSequence::AiTravel& source = static_cast(*container.mPackage); + if (source.mHidden) + package = std::make_unique(&source); else - package.reset(new AiTravel(source)); + package = std::make_unique(&source); break; } case ESM::AiSequence::Ai_Escort: { - package.reset(new AiEscort(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } case ESM::AiSequence::Ai_Follow: { - package.reset(new AiFollow(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } case ESM::AiSequence::Ai_Activate: { - package.reset(new AiActivate(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } case ESM::AiSequence::Ai_Combat: { - package.reset(new AiCombat(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } case ESM::AiSequence::Ai_Pursue: { - package.reset(new AiPursue(static_cast(container.mPackage))); + package = std::make_unique(&static_cast(*container.mPackage)); break; } default: @@ -518,6 +542,7 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) if (!package.get()) continue; + onPackageAdded(*package); mPackages.push_back(std::move(package)); } diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 645524d381..e6f5a108c2 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -1,13 +1,14 @@ #ifndef GAME_MWMECHANICS_AISEQUENCE_H #define GAME_MWMECHANICS_AISEQUENCE_H -#include #include +#include +#include #include "aistate.hpp" #include "aipackagetypeid.hpp" -#include +#include namespace MWWorld { @@ -22,29 +23,25 @@ namespace ESM } } - - namespace MWMechanics { class AiPackage; class CharacterController; - - template< class Base > class DerivedClassStorage; - struct AiTemporaryBase; - typedef DerivedClassStorage AiState; + + using AiPackages = std::vector>; /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ class AiSequence { ///AiPackages to run though - std::list> mPackages; + AiPackages mPackages; ///Finished with top AIPackage, set for one frame - bool mDone; + bool mDone{}; - ///Does this AI sequence repeat (repeating of Wander packages handled separately) - bool mRepeat; + int mNumCombatPackages{}; + int mNumPursuitPackages{}; ///Copy AiSequence void copy (const AiSequence& sequence); @@ -53,6 +50,11 @@ namespace MWMechanics AiPackageTypeId mLastAiPackage; AiState mAiState; + void onPackageAdded(const AiPackage& package); + void onPackageRemoved(const AiPackage& package); + + AiPackages::iterator erase(AiPackages::iterator package); + public: ///Default constructor AiSequence(); @@ -66,10 +68,31 @@ namespace MWMechanics virtual ~AiSequence(); /// Iterator may be invalidated by any function calls other than begin() or end(). - std::list>::const_iterator begin() const; - std::list>::const_iterator end() const; - - void erase(std::list>::const_iterator package); + AiPackages::const_iterator begin() const { return mPackages.begin(); } + AiPackages::const_iterator end() const { return mPackages.end(); } + + /// Removes all packages controlled by the predicate. + template + void erasePackagesIf(const F&& pred) + { + mPackages.erase(std::remove_if(mPackages.begin(), mPackages.end(), [&](auto& entry) + { + const bool doRemove = pred(entry); + if (doRemove) + onPackageRemoved(*entry); + return doRemove; + }), mPackages.end()); + } + + /// Removes a single package controlled by the predicate. + template + void erasePackageIf(const F&& pred) + { + auto it = std::find_if(mPackages.begin(), mPackages.end(), pred); + if (it == mPackages.end()) + return; + erase(it); + } /// Returns currently executing AiPackage type /** \see enum class AiPackageTypeId **/ @@ -90,6 +113,12 @@ namespace MWMechanics /// Is there any combat package? bool isInCombat () const; + /// Is there any pursuit package. + bool isInPursuit() const; + + /// Removes all packages using the specified id. + void removePackagesById(AiPackageTypeId id); + /// Are we in combat with any other actor, who's also engaging us? bool isEngagedWithActor () const; @@ -105,6 +134,9 @@ namespace MWMechanics /// Removes all combat packages until first non-combat or stack empty. void stopCombat(); + /// Removes all combat packages with the given targets + void stopCombat(const std::vector& targets); + /// Has a package been completed during the last update? bool isPackageDone() const; @@ -127,7 +159,7 @@ namespace MWMechanics /// Return the current active package. /** If there is no active package, it will throw an exception **/ - const AiPackage& getActivePackage(); + const AiPackage& getActivePackage() const; /// Fills the AiSequence with packages /** Typically used for loading from the ESM diff --git a/apps/openmw/mwmechanics/aisetting.hpp b/apps/openmw/mwmechanics/aisetting.hpp new file mode 100644 index 0000000000..3a274722fe --- /dev/null +++ b/apps/openmw/mwmechanics/aisetting.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_AISETTING_H +#define OPENMW_MWMECHANICS_AISETTING_H + +namespace MWMechanics +{ + enum class AiSetting + { + Hello = 0, + Fight = 1, + Flee = 2, + Alarm = 3 + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp index 976e21c65c..ca05c44b36 100644 --- a/apps/openmw/mwmechanics/aistate.hpp +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -1,7 +1,10 @@ #ifndef AISTATE_H #define AISTATE_H -#include +#include "aistatefwd.hpp" +#include "aitemporarybase.hpp" + +#include namespace MWMechanics { @@ -13,28 +16,24 @@ namespace MWMechanics */ template< class Base > class DerivedClassStorage - { + { private: - Base* mStorage; - - //if needed you have to provide a clone member function - DerivedClassStorage( const DerivedClassStorage& other ); - DerivedClassStorage& operator=( const DerivedClassStorage& ); - + std::unique_ptr mStorage; + public: /// \brief returns reference to stored object or deletes it and creates a fitting template< class Derived > Derived& get() { - Derived* result = dynamic_cast(mStorage); - - if(!result) + Derived* result = dynamic_cast(mStorage.get()); + + if (result == nullptr) { - if(mStorage) - delete mStorage; - mStorage = result = new Derived(); + auto storage = std::make_unique(); + result = storage.get(); + mStorage = std::move(storage); } - + //return a reference to the (new allocated) object return *result; } @@ -42,61 +41,24 @@ namespace MWMechanics template< class Derived > void copy(DerivedClassStorage& destination) const { - Derived* result = dynamic_cast(mStorage); + Derived* result = dynamic_cast(mStorage.get()); if (result != nullptr) destination.store(*result); } - + template< class Derived > void store( const Derived& payload ) { - if(mStorage) - delete mStorage; - mStorage = new Derived(payload); + mStorage = std::make_unique(payload); } - + /// \brief takes ownership of the passed object - template< class Derived > - void moveIn( Derived* p ) - { - if(mStorage) - delete mStorage; - mStorage = p; - } - - bool empty() const - { - return mStorage == nullptr; - } - - const std::type_info& getType() const - { - return typeid(mStorage); - } - - DerivedClassStorage():mStorage(nullptr){} - ~DerivedClassStorage() + template + void moveIn(std::unique_ptr&& storage) { - if(mStorage) - delete mStorage; + mStorage = std::move(storage); } }; - - - /// \brief base class for the temporary storage of AiPackages. - /** - * Each AI package with temporary values needs a AiPackageStorage class - * which is derived from AiTemporaryBase. The Actor holds a container - * AiState where one of these storages can be stored at a time. - * The execute(...) member function takes this container as an argument. - * */ - struct AiTemporaryBase - { - virtual ~AiTemporaryBase(){} - }; - - /// \brief Container for AI package status. - typedef DerivedClassStorage AiState; } #endif // AISTATE_H diff --git a/apps/openmw/mwmechanics/aistatefwd.hpp b/apps/openmw/mwmechanics/aistatefwd.hpp new file mode 100644 index 0000000000..83216d1d27 --- /dev/null +++ b/apps/openmw/mwmechanics/aistatefwd.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_AISTATEFWD_H +#define OPENMW_MWMECHANICS_AISTATEFWD_H + +namespace MWMechanics +{ + template + class DerivedClassStorage; + + struct AiTemporaryBase; + + /// \brief Container for AI package status. + using AiState = DerivedClassStorage; +} + +#endif diff --git a/apps/openmw/mwmechanics/aitemporarybase.hpp b/apps/openmw/mwmechanics/aitemporarybase.hpp new file mode 100644 index 0000000000..8ac8e4b71b --- /dev/null +++ b/apps/openmw/mwmechanics/aitemporarybase.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_MWMECHANICS_AISTATE_H +#define OPENMW_MWMECHANICS_AISTATE_H + +namespace MWMechanics +{ + /// \brief base class for the temporary storage of AiPackages. + /** + * Each AI package with temporary values needs a AiPackageStorage class + * which is derived from AiTemporaryBase. The Actor holds a container + * AiState where one of these storages can be stored at a time. + * The execute(...) member function takes this container as an argument. + * */ + struct AiTemporaryBase + { + virtual ~AiTemporaryBase() = default; + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aitimer.hpp b/apps/openmw/mwmechanics/aitimer.hpp new file mode 100644 index 0000000000..e4bc07e52d --- /dev/null +++ b/apps/openmw/mwmechanics/aitimer.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_MECHANICS_AITIMER_H +#define OPENMW_MECHANICS_AITIMER_H + +#include +#include + +namespace MWMechanics +{ + constexpr float AI_REACTION_TIME = 0.25f; + + class AiReactionTimer + { + public: + static constexpr float sDeviation = 0.1f; + + AiReactionTimer(Misc::Rng::Generator& prng) + : mPrng{ prng } + , mImpl{ AI_REACTION_TIME, sDeviation, Misc::Rng::deviate(0, sDeviation, prng) } + { + } + + Misc::TimerStatus update(float duration) { return mImpl.update(duration, mPrng); } + + void reset() { mImpl.reset(Misc::Rng::deviate(0, sDeviation, mPrng)); } + + private: + Misc::Rng::Generator& mPrng; + Misc::DeviatingPeriodicTimer mImpl; + + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index b2a506ca65..71432a5537 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,6 +1,9 @@ #include "aitravel.hpp" -#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -15,6 +18,8 @@ namespace { + constexpr float TRAVEL_FINISH_TIME = 2.f; + bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) { // Maximum travel distance for vanilla compatibility. @@ -27,23 +32,24 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) namespace MWMechanics { - AiTravel::AiTravel(float x, float y, float z, AiTravel*) - : mX(x), mY(y), mZ(z), mHidden(false) + AiTravel::AiTravel(float x, float y, float z, bool repeat, AiTravel*) + : TypedAiPackage(repeat), mX(x), mY(y), mZ(z), mHidden(false), mDestinationTimer(TRAVEL_FINISH_TIME) { } AiTravel::AiTravel(float x, float y, float z, AiInternalTravel* derived) - : TypedAiPackage(derived), mX(x), mY(y), mZ(z), mHidden(true) + : TypedAiPackage(derived), mX(x), mY(y), mZ(z), mHidden(true), mDestinationTimer(TRAVEL_FINISH_TIME) { } - AiTravel::AiTravel(float x, float y, float z) - : AiTravel(x, y, z, this) + AiTravel::AiTravel(float x, float y, float z, bool repeat) + : AiTravel(x, y, z, repeat, this) { } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) - : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false) + : TypedAiPackage(travel->mRepeat), mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false) + , mDestinationTimer(TRAVEL_FINISH_TIME) { // Hidden ESM::AiSequence::AiTravel package should be converted into MWMechanics::AiInternalTravel type assert(!travel->mHidden); @@ -52,67 +58,77 @@ namespace MWMechanics bool AiTravel::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); + auto& stats = actor.getClass().getCreatureStats(actor); - if (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress) + if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) + && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) return false; const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f targetPos(mX, mY, mZ); - auto& stats = actor.getClass().getCreatureStats(actor); stats.setMovementFlag(CreatureStats::Flag_Run, false); - stats.setDrawState(DrawState_Nothing); + stats.setDrawState(DrawState::Nothing); // Note: we should cancel internal "return after combat" package, if original location is too far away if (!isWithinMaxRange(targetPos, actorPos)) return mHidden; - // Unfortunately, with vanilla assets destination is sometimes blocked by other actor. - // If we got close to target, check for actors nearby. If they are, finish AI package. - int destinationTolerance = 64; - if (distance(actorPos, targetPos) <= destinationTolerance) - { - std::vector targetActors; - std::pair result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors); - - if (!result.first.isEmpty()) - { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - return true; - } - } - if (pathTo(actor, targetPos, duration)) { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; return true; } - return false; + + // If we've been close enough to the destination for some time give up like Morrowind. + // The end condition should be pretty much accurate. + // FIXME: But the timing isn't. Right now we're being very generous, + // but Morrowind might stop the actor prematurely under unclear conditions. + + // Note Morrowind uses the halved eye level, but this is close enough. + float dist = distanceIgnoreZ(actorPos, targetPos) - MWBase::Environment::get().getWorld()->getHalfExtents(actor).z(); + const float endTolerance = std::max(64.f, actor.getClass().getCurrentSpeed(actor) * duration); + + // Even if we have entered the threshold, we might have been pushed away. Reset the timer if we're currently too far. + if (dist > endTolerance) + { + mDestinationTimer = TRAVEL_FINISH_TIME; + return false; + } + + mDestinationTimer -= duration; + if (mDestinationTimer > 0) + return false; + + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + return true; } void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) { - if (!isWithinMaxRange(osg::Vec3f(mX, mY, mZ), actor.getRefData().getPosition().asVec3())) + osg::Vec3f pos(mX, mY, mZ); + if (!isWithinMaxRange(pos, actor.getRefData().getPosition().asVec3())) return; // does not do any validation on the travel target (whether it's in air, inside collision geometry, etc), // that is the user's responsibility - MWBase::Environment::get().getWorld()->moveObject(actor, mX, mY, mZ); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); reset(); } void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const { - std::unique_ptr travel(new ESM::AiSequence::AiTravel()); + auto travel = std::make_unique(); travel->mData.mX = mX; travel->mData.mY = mY; travel->mData.mZ = mZ; travel->mHidden = mHidden; + travel->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; - package.mPackage = travel.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(travel); + sequence.mPackages.push_back(std::move(package)); } AiInternalTravel::AiInternalTravel(float x, float y, float z) diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 2ea2a8f717..60a68d9cc5 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -19,11 +19,11 @@ namespace MWMechanics class AiTravel : public TypedAiPackage { public: - AiTravel(float x, float y, float z, AiTravel* derived); + AiTravel(float x, float y, float z, bool repeat, AiTravel* derived); AiTravel(float x, float y, float z, AiInternalTravel* derived); - AiTravel(float x, float y, float z); + AiTravel(float x, float y, float z, bool repeat); explicit AiTravel(const ESM::AiSequence::AiTravel* travel); @@ -52,6 +52,8 @@ namespace MWMechanics const float mZ; const bool mHidden; + + float mDestinationTimer; }; struct AiInternalTravel final : public AiTravel diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 375209a250..a2c97d641a 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -2,16 +2,17 @@ #include +#include + #include #include -#include -#include +#include +#include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -60,7 +61,8 @@ namespace MWMechanics osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) { - const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const float randomDirection = Misc::Rng::rollClosedProbability(prng) * 2.0f * osg::PI; osg::Matrixf rotation; rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0)); return position + osg::Vec3f(distance, 0.0, 0.0) * rotation; @@ -71,7 +73,7 @@ namespace MWMechanics const auto position = actor.getRefData().getPosition().asVec3(); const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor).mHalfExtents; osg::Vec3f direction = destination - position; direction.normalize(); const auto visibleDestination = ( @@ -86,18 +88,11 @@ namespace MWMechanics return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor); } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor); - } - void stopMovement(const MWWorld::Ptr& actor) { - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + auto& movementSettings = actor.getClass().getMovementSettings(actor); + movementSettings.mPosition[0] = 0; + movementSettings.mPosition[1] = 0; } std::vector getInitialIdle(const std::vector& idle) @@ -111,10 +106,26 @@ namespace MWMechanics { return std::vector(std::begin(idle), std::end(idle)); } + + } + + AiWanderStorage::AiWanderStorage() : + mReaction(MWBase::Environment::get().getWorld()->getPrng()), + mState(Wander_ChooseAction), + mIsWanderingManually(false), + mCanWanderAlongPathGrid(true), + mIdleAnimation(0), + mBadIdles(), + mPopulateAvailableNodes(true), + mAllowedNodes(), + mTrimCurrentNode(false), + mCheckIdlePositionTimer(0), + mStuckCount(0) + { } AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): - TypedAiPackage(makeDefaultOptions().withRepeat(repeat)), + TypedAiPackage(repeat), mDistance(std::max(0, distance)), mDuration(std::max(0, duration)), mRemainingDuration(duration), mTimeOfDay(timeOfDay), @@ -185,7 +196,7 @@ namespace MWMechanics mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); - cStats.setDrawState(DrawState_Nothing); + cStats.setDrawState(DrawState::Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); @@ -201,37 +212,37 @@ namespace MWMechanics } else { - const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), - getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + getPathGridGraph(actor.getCell()), agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), + endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking); } - GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); - if (greetingState == Greet_InProgress) + if(!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { - if (storage.mState == AiWanderStorage::Wander_Walking) + GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); + if (greetingState == Greet_InProgress) { - stopMovement(actor); - mObstacleCheck.clear(); - storage.setState(AiWanderStorage::Wander_IdleNow); + if (storage.mState == AiWanderStorage::Wander_Walking) + { + stopMovement(actor); + mObstacleCheck.clear(); + storage.setState(AiWanderStorage::Wander_IdleNow); + } } } doPerFrameActionsForState(actor, duration, storage); - float& lastReaction = storage.mReaction; - lastReaction += duration; - if (AI_REACTION_TIME <= lastReaction) - { - lastReaction = 0; - return reactionTimeActions(actor, storage, pos); - } - else + if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; + + return reactionTimeActions(actor, storage, pos); } bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) @@ -259,9 +270,10 @@ namespace MWMechanics getAllowedNodes(actor, actor.getCell()->getCell(), storage); } + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander - if (Misc::Rng::rollDice(100) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { + if (Misc::Rng::rollDice(100, prng) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { wanderNearStart(actor, storage, mDistance); } @@ -271,7 +283,7 @@ namespace MWMechanics // randomly idle or wander near spawn point else if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander - if (Misc::Rng::rollDice(100) >= 96) { + if (Misc::Rng::rollDice(100, prng) >= 96) { wanderNearStart(actor, storage, mDistance); } else { storage.setState(AiWanderStorage::Wander_IdleNow); @@ -335,18 +347,24 @@ namespace MWMechanics const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const auto world = MWBase::Environment::get().getWorld(); - const auto halfExtents = world->getPathfindingHalfExtents(actor); + const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto navigator = world->getNavigator(); const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); do { + // Determine a random location within radius of original position - const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance; + const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability(prng) * 0.8f) * wanderDistance; if (!isWaterCreature && !isFlyingCreature) { // findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance - if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, mInitialActorPosition, wanderDistance, navigatorFlags)) + if (const auto destination = DetourNavigator::findRandomPointAroundCircle(*navigator, agentBounds, + mInitialActorPosition, wanderDistance, navigatorFlags, []() { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return Misc::Rng::rollProbability(prng); + })) mDestination = *destination; else mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); @@ -364,11 +382,13 @@ namespace MWMechanics if (isAreaOccupiedByOtherActor(actor, mDestination)) continue; + constexpr float endTolerance = 0; + if (isWaterCreature || isFlyingCreature) mPathFinder.buildStraightPath(mDestination); else - mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags, - areaCosts); + mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, agentBounds, navigatorFlags, + areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) { @@ -514,8 +534,8 @@ namespace MWMechanics { if (mUsePathgrid) { - const auto halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); - mPathFinder.buildPathByNavMeshToNextPoint(actor, halfExtents, getNavigatorFlags(actor), + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + mPathFinder.buildPathByNavMeshToNextPoint(actor, agentBounds, getNavigatorFlags(actor), getAreaCosts(actor)); } @@ -550,7 +570,8 @@ namespace MWMechanics void AiWander::setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]); ToWorldCoordinates(dest, actor.getCell()->getCell()); @@ -656,11 +677,11 @@ namespace MWMechanics for(unsigned int counter = 0; counter < mIdle.size(); counter++) { - static float fIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore() - .get().find("fIdleChanceMultiplier")->mValue.getFloat(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + static float fIdleChanceMultiplier = world->getStore().get().find("fIdleChanceMultiplier")->mValue.getFloat(); unsigned short idleChance = static_cast(fIdleChanceMultiplier * mIdle[counter]); - unsigned short randSelect = (int)(Misc::Rng::rollProbability() * int(100 / fIdleChanceMultiplier)); + unsigned short randSelect = (int)(Misc::Rng::rollProbability(world->getPrng()) * int(100 / fIdleChanceMultiplier)); if(randSelect < idleChance && randSelect > idleRoll) { selectedAnimation = counter + GroupIndex_MinIdle; @@ -684,7 +705,8 @@ namespace MWMechanics if (storage.mAllowedNodes.empty()) return; - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); ESM::Pathgrid::Point dest = storage.mAllowedNodes[index]; ESM::Pathgrid::Point worldDest = dest; ToWorldCoordinates(worldDest, actor.getCell()->getCell()); @@ -706,7 +728,7 @@ namespace MWMechanics // AI will try to move the NPC towards every neighboring node until suitable place will be found for (int i = 0; i < initialSize; i++) { - int randomIndex = Misc::Rng::rollDice(points.size()); + int randomIndex = Misc::Rng::rollDice(points.size(), prng); ESM::Pathgrid::Point connDest = points[randomIndex]; // add an offset towards random neighboring node @@ -745,10 +767,10 @@ namespace MWMechanics ToWorldCoordinates(dest, actor.getCell()->getCell()); - state.moveIn(new AiWanderStorage()); + state.moveIn(std::make_unique()); - MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), - static_cast(dest.mY), static_cast(dest.mZ)); + osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); } @@ -757,6 +779,9 @@ namespace MWMechanics const ESM::Pathgrid *pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*currentCell->getCell()); + if (pathgrid == nullptr || pathgrid->mPoints.empty()) + return; + int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(currentCell).getNeighbouringPoints(index, points); @@ -876,7 +901,7 @@ namespace MWMechanics else remainingDuration = mDuration; - std::unique_ptr wander(new ESM::AiSequence::AiWander()); + auto wander = std::make_unique(); wander->mData.mDistance = mDistance; wander->mData.mDuration = mDuration; wander->mData.mTimeOfDay = mTimeOfDay; @@ -891,8 +916,8 @@ namespace MWMechanics ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Wander; - package.mPackage = wander.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(wander); + sequence.mPackages.push_back(std::move(package)); } AiWander::AiWander (const ESM::AiSequence::AiWander* wander) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 68bcddf228..1adb4ed84e 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -5,11 +5,10 @@ #include -#include "../mwworld/timestamp.hpp" - #include "pathfinding.hpp" #include "obstacle.hpp" -#include "aistate.hpp" +#include "aitemporarybase.hpp" +#include "aitimer.hpp" namespace ESM { @@ -25,7 +24,7 @@ namespace MWMechanics /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. struct AiWanderStorage : AiTemporaryBase { - float mReaction; // update some actions infrequently + AiReactionTimer mReaction; // AiWander states enum WanderState @@ -56,19 +55,7 @@ namespace MWMechanics float mCheckIdlePositionTimer; int mStuckCount; - AiWanderStorage(): - mReaction(0), - mState(Wander_ChooseAction), - mIsWanderingManually(false), - mCanWanderAlongPathGrid(true), - mIdleAnimation(0), - mBadIdles(), - mPopulateAvailableNodes(true), - mAllowedNodes(), - mTrimCurrentNode(false), - mCheckIdlePositionTimer(0), - mStuckCount(0) - {}; + AiWanderStorage(); void setState(const WanderState wanderState, const bool isManualWander = false) { @@ -99,7 +86,6 @@ namespace MWMechanics { AiPackage::Options options; options.mUseVariableSpeed = true; - options.mRepeat = false; return options; } @@ -129,7 +115,6 @@ namespace MWMechanics short unsigned getRandomIdle(); void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void turnActorToFacePlayer(const osg::Vec3f& actorPosition, const osg::Vec3f& playerPosition, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 116937fcdb..d60eac15de 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -9,10 +9,10 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -21,7 +21,6 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwworld/manualref.hpp" #include "magiceffects.hpp" #include "creaturestats.hpp" @@ -255,7 +254,7 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co return &(*iter); } - return 0; + return nullptr; } void MWMechanics::Alchemy::removeIngredients() @@ -290,7 +289,8 @@ void MWMechanics::Alchemy::addPotion (const std::string& name) newRecord.mName = name; - int index = Misc::Rng::rollDice(6); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int index = Misc::Rng::rollDice(6, prng); assert (index>=0 && index<6); static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; @@ -528,8 +528,8 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle () removeIngredients(); return Result_RandomFailure; } - - if (getAlchemyFactor() < Misc::Rng::roll0to99()) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (getAlchemyFactor() < Misc::Rng::roll0to99(prng)) { removeIngredients(); return Result_RandomFailure; diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index d23f978ead..8c91a81177 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "../mwworld/ptr.hpp" diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 662cfe473b..d82b8505f1 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -68,7 +68,8 @@ namespace MWMechanics if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); - if (baseMagicka < iAutoSpellTimesCanCast * spell.mData.mCost) + int spellCost = MWMechanics::calcSpellCost(spell); + if (baseMagicka < iAutoSpellTimesCanCast * spellCost) continue; if (race && race->mPowers.exists(spell.mId)) @@ -83,7 +84,7 @@ namespace MWMechanics assert(school >= 0 && school < 6); SchoolCaps& cap = schoolCaps[school]; - if (cap.mReachedLimit && spell.mData.mCost <= cap.mMinCost) + if (cap.mReachedLimit && spellCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); @@ -102,6 +103,7 @@ namespace MWMechanics for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = spells.find(testSpellName); + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); //int testSchool; //float dummySkillTerm; @@ -115,9 +117,9 @@ namespace MWMechanics // already erased it, and so the number of spells would often exceed the sum of limits. // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested. //testSchool == school && - testSpell->mData.mCost < cap.mMinCost) + testSpellCost < cap.mMinCost) { - cap.mMinCost = testSpell->mData.mCost; + cap.mMinCost = testSpellCost; cap.mWeakestSpell = testSpell->mId; } } @@ -128,10 +130,10 @@ namespace MWMechanics if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; - if (spell.mData.mCost < cap.mMinCost) + if (spellCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; - cap.mMinCost = spell.mData.mCost; + cap.mMinCost = spellCost; } } } @@ -159,11 +161,13 @@ namespace MWMechanics continue; if (!(spell.mData.mFlags & ESM::Spell::F_PCStart)) continue; - if (reachedLimit && spell.mData.mCost <= minCost) + + int spellCost = MWMechanics::calcSpellCost(spell); + if (reachedLimit && spellCost <= minCost) continue; if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell.mId) != race->mPowers.mList.end()) continue; - if (baseMagicka < spell.mData.mCost) + if (baseMagicka < spellCost) continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); @@ -185,19 +189,20 @@ namespace MWMechanics for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); - if (testSpell->mData.mCost < minCost) + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); + if (testSpellCost < minCost) { - minCost = testSpell->mData.mCost; + minCost = testSpellCost; weakestSpell = testSpell; } } } else { - if (spell.mData.mCost < minCost) + if (spellCost < minCost) { weakestSpell = &spell; - minCost = weakestSpell->mData.mCost; + minCost = MWMechanics::calcSpellCost(*weakestSpell); } static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) @@ -291,7 +296,9 @@ namespace MWMechanics else calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this - float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; + float castChance = skillTerm - MWMechanics::calcSpellCost(*spell) + + 0.2f * actorAttributes[ESM::Attribute::Willpower] + + 0.1f * actorAttributes[ESM::Attribute::Luck]; return castChance; } } diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 657f2e2eca..c6f28cca64 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -19,10 +19,12 @@ #include "character.hpp" -#include +#include #include #include +#include +#include #include @@ -54,9 +56,9 @@ namespace std::string getBestAttack (const ESM::Weapon* weapon) { - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + int slash = weapon->mData.mSlash[0] + weapon->mData.mSlash[1]; + int chop = weapon->mData.mChop[0] + weapon->mData.mChop[1]; + int thrust = weapon->mData.mThrust[0] + weapon->mData.mThrust[1]; if (slash == chop && slash == thrust) return "slash"; else if (thrust >= chop && thrust >= slash) @@ -67,41 +69,149 @@ std::string getBestAttack (const ESM::Weapon* weapon) return "chop"; } -// Converts a movement Run state to its equivalent Walk state. +// Converts a movement Run state to its equivalent Walk state, if there is one. MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) { using namespace MWMechanics; - CharacterState ret = state; switch (state) { - case CharState_RunForward: - ret = CharState_WalkForward; - break; - case CharState_RunBack: - ret = CharState_WalkBack; - break; - case CharState_RunLeft: - ret = CharState_WalkLeft; - break; - case CharState_RunRight: - ret = CharState_WalkRight; - break; - case CharState_SwimRunForward: - ret = CharState_SwimWalkForward; - break; - case CharState_SwimRunBack: - ret = CharState_SwimWalkBack; - break; - case CharState_SwimRunLeft: - ret = CharState_SwimWalkLeft; - break; - case CharState_SwimRunRight: - ret = CharState_SwimWalkRight; - break; + case CharState_RunForward: return CharState_WalkForward; + case CharState_RunBack: return CharState_WalkBack; + case CharState_RunLeft: return CharState_WalkLeft; + case CharState_RunRight: return CharState_WalkRight; + case CharState_SwimRunForward: return CharState_SwimWalkForward; + case CharState_SwimRunBack: return CharState_SwimWalkBack; + case CharState_SwimRunLeft: return CharState_SwimWalkLeft; + case CharState_SwimRunRight: return CharState_SwimWalkRight; + default: return state; + } +} + +// Converts a Hit state to its equivalent Death state. +MWMechanics::CharacterState hitStateToDeathState (MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + switch (state) + { + case CharState_SwimKnockDown: return CharState_SwimDeathKnockDown; + case CharState_SwimKnockOut: return CharState_SwimDeathKnockOut; + case CharState_KnockDown: return CharState_DeathKnockDown; + case CharState_KnockOut: return CharState_DeathKnockOut; + default: return CharState_None; + } +} + +// Converts a movement state to its equivalent base animation group as long as it is a movement state. +std::string movementStateToAnimGroup(MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + switch (state) + { + case CharState_WalkForward: return "walkforward"; + case CharState_WalkBack: return "walkback"; + case CharState_WalkLeft: return "walkleft"; + case CharState_WalkRight: return "walkright"; + + case CharState_SwimWalkForward: return "swimwalkforward"; + case CharState_SwimWalkBack: return "swimwalkback"; + case CharState_SwimWalkLeft: return "swimwalkleft"; + case CharState_SwimWalkRight: return "swimwalkright"; + + case CharState_RunForward: return "runforward"; + case CharState_RunBack: return "runback"; + case CharState_RunLeft: return "runleft"; + case CharState_RunRight: return "runright"; + + case CharState_SwimRunForward: return "swimrunforward"; + case CharState_SwimRunBack: return "swimrunback"; + case CharState_SwimRunLeft: return "swimrunleft"; + case CharState_SwimRunRight: return "swimrunright"; + + case CharState_SneakForward: return "sneakforward"; + case CharState_SneakBack: return "sneakback"; + case CharState_SneakLeft: return "sneakleft"; + case CharState_SneakRight: return "sneakright"; + + case CharState_TurnLeft: return "turnleft"; + case CharState_TurnRight: return "turnright"; + case CharState_SwimTurnLeft: return "swimturnleft"; + case CharState_SwimTurnRight: return "swimturnright"; + default: return {}; + } +} + +// Converts a death state to its equivalent animation group as long as it is a death state. +std::string deathStateToAnimGroup(MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + switch (state) + { + case CharState_SwimDeath: return "swimdeath"; + case CharState_SwimDeathKnockDown: return "swimdeathknockdown"; + case CharState_SwimDeathKnockOut: return "swimdeathknockout"; + case CharState_DeathKnockDown: return "deathknockdown"; + case CharState_DeathKnockOut: return "deathknockout"; + case CharState_Death1: return "death1"; + case CharState_Death2: return "death2"; + case CharState_Death3: return "death3"; + case CharState_Death4: return "death4"; + case CharState_Death5: return "death5"; + default: return {}; + } +} + +// Converts a hit state to its equivalent animation group as long as it is a hit state. +std::string hitStateToAnimGroup(MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + switch (state) + { + case CharState_SwimHit: return "swimhit"; + case CharState_SwimKnockDown: return "swimknockdown"; + case CharState_SwimKnockOut: return "swimknockout"; + + case CharState_Hit: return "hit"; + case CharState_KnockDown: return "knockdown"; + case CharState_KnockOut: return "knockout"; + + case CharState_Block: return "shield"; + + default: return {}; + } +} + +// Converts an idle state to its equivalent animation group. +std::string idleStateToAnimGroup(MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + switch (state) + { + case CharState_IdleSwim: + return "idleswim"; + case CharState_IdleSneak: + return "idlesneak"; + case CharState_Idle: + case CharState_SpecialIdle: + return "idle"; default: - break; + return {}; + } +} + +MWRender::Animation::AnimPriority getIdlePriority(MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + MWRender::Animation::AnimPriority priority(Priority_Default); + switch (state) + { + case CharState_IdleSwim: + return Priority_SwimIdle; + case CharState_IdleSneak: + priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + [[fallthrough]]; + default: + return priority; } - return ret; } float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) @@ -133,246 +243,219 @@ float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) return 0.f; } +bool isRealWeapon(int weaponType) +{ + return weaponType != ESM::Weapon::HandToHand + && weaponType != ESM::Weapon::Spell + && weaponType != ESM::Weapon::None; +} + } namespace MWMechanics { -struct StateInfo { - CharacterState state; - const char groupname[32]; -}; - -static const StateInfo sMovementList[] = { - { CharState_WalkForward, "walkforward" }, - { CharState_WalkBack, "walkback" }, - { CharState_WalkLeft, "walkleft" }, - { CharState_WalkRight, "walkright" }, - - { CharState_SwimWalkForward, "swimwalkforward" }, - { CharState_SwimWalkBack, "swimwalkback" }, - { CharState_SwimWalkLeft, "swimwalkleft" }, - { CharState_SwimWalkRight, "swimwalkright" }, - - { CharState_RunForward, "runforward" }, - { CharState_RunBack, "runback" }, - { CharState_RunLeft, "runleft" }, - { CharState_RunRight, "runright" }, - - { CharState_SwimRunForward, "swimrunforward" }, - { CharState_SwimRunBack, "swimrunback" }, - { CharState_SwimRunLeft, "swimrunleft" }, - { CharState_SwimRunRight, "swimrunright" }, - - { CharState_SneakForward, "sneakforward" }, - { CharState_SneakBack, "sneakback" }, - { CharState_SneakLeft, "sneakleft" }, - { CharState_SneakRight, "sneakright" }, - - { CharState_Jump, "jump" }, - - { CharState_TurnLeft, "turnleft" }, - { CharState_TurnRight, "turnright" }, - { CharState_SwimTurnLeft, "swimturnleft" }, - { CharState_SwimTurnRight, "swimturnright" }, -}; -static const StateInfo *sMovementListEnd = &sMovementList[sizeof(sMovementList)/sizeof(sMovementList[0])]; - - -class FindCharState { - CharacterState state; - -public: - FindCharState(CharacterState _state) : state(_state) { } - - bool operator()(const StateInfo &info) const - { return info.state == state; } -}; - - std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int numAnims=0; while (mAnimation->hasAnimation(prefix + std::to_string(numAnims+1))) ++numAnims; - int roll = Misc::Rng::rollDice(numAnims) + 1; // [1, numAnims] + int roll = Misc::Rng::rollDice(numAnims, prng) + 1; // [1, numAnims] if (num) *num = roll; return prefix + std::to_string(roll); } -void CharacterController::refreshHitRecoilAnims(CharacterState& idle) + +void CharacterController::clearStateAnimation(std::string &anim) const { - bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); - bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); - bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); - bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - if(mHitState == CharState_None) - { - if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 - || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) - && mAnimation->hasAnimation("knockout")) - { - mTimeUntilWake = Misc::Rng::rollClosedProbability() * 2 + 1; // Wake up after 1 to 3 seconds - if (isSwimming && mAnimation->hasAnimation("swimknockout")) - { - mHitState = CharState_SwimKnockOut; - mCurrentHit = "swimknockout"; - } - else - { - mHitState = CharState_KnockOut; - mCurrentHit = "knockout"; - } + if (anim.empty()) + return; + if (mAnimation) + mAnimation->disable(anim); + anim.clear(); +} - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(true); - } - else if(knockdown && mAnimation->hasAnimation("knockdown")) - { - if (isSwimming && mAnimation->hasAnimation("swimknockdown")) - { - mHitState = CharState_SwimKnockDown; - mCurrentHit = "swimknockdown"; - } - else - { - mHitState = CharState_KnockDown; - mCurrentHit = "knockdown"; - } +void CharacterController::resetCurrentJumpState() +{ + clearStateAnimation(mCurrentJump); + mJumpState = JumpState_None; +} - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else if (recovery) - { - std::string anim = chooseRandomGroup("swimhit"); - if (isSwimming && mAnimation->hasAnimation(anim)) - { - mHitState = CharState_SwimHit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else - { - anim = chooseRandomGroup("hit"); - if (mAnimation->hasAnimation(anim)) - { - mHitState = CharState_Hit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - } - } - else if (block && mAnimation->hasAnimation("shield")) - { - mHitState = CharState_Block; - mCurrentHit = "shield"; - MWRender::Animation::AnimPriority priorityBlock (Priority_Hit); - priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - priorityBlock[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; - mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0); - } +void CharacterController::resetCurrentMovementState() +{ + clearStateAnimation(mCurrentMovement); + mMovementState = CharState_None; +} - // Cancel upper body animations - if (isKnockedOut() || isKnockedDown()) - { - if (mUpperBodyState > UpperCharState_WeapEquiped) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; - if (mWeaponType > ESM::Weapon::None) - mAnimation->showWeapons(true); - } - else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_Nothing; - } - } - if (mHitState != CharState_None) - idle = CharState_None; +void CharacterController::resetCurrentIdleState() +{ + clearStateAnimation(mCurrentIdle); + mIdleState = CharState_None; +} + +void CharacterController::resetCurrentHitState() +{ + clearStateAnimation(mCurrentHit); + mHitState = CharState_None; +} + +void CharacterController::resetCurrentWeaponState() +{ + clearStateAnimation(mCurrentWeapon); + mUpperBodyState = UpperCharState_Nothing; +} + +void CharacterController::resetCurrentDeathState() +{ + clearStateAnimation(mCurrentDeath); + mDeathState = CharState_None; +} + +void CharacterController::refreshHitRecoilAnims() +{ + auto& charClass = mPtr.getClass(); + if (!charClass.isActor()) + return; + const auto world = MWBase::Environment::get().getWorld(); + auto& stats = charClass.getCreatureStats(mPtr); + bool knockout = stats.getFatigue().getCurrent() < 0 || stats.getFatigue().getBase() == 0; + bool recovery = stats.getHitRecovery(); + bool knockdown = stats.getKnockedDown(); + bool block = stats.getBlock(); + bool isSwimming = world->isSwimming(mPtr); + + if (mHitState != CharState_None) + { + if (!mAnimation->isPlaying(mCurrentHit)) + { + mHitState = CharState_None; + mCurrentHit.clear(); + stats.setKnockedDown(false); + stats.setHitRecovery(false); + stats.setBlock(false); + resetCurrentIdleState(); + } + else if (isKnockedOut()) + mAnimation->setLoopingEnabled(mCurrentHit, knockout); + return; } - else if(!mAnimation->isPlaying(mCurrentHit)) + + if (!knockout && !knockdown && !recovery && !block) + return; + + MWRender::Animation::AnimPriority priority(Priority_Knockdown); + std::string startKey = "start"; + std::string stopKey = "stop"; + if (knockout) { - mCurrentHit.erase(); - if (knockdown) - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); - if (recovery) - mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); - if (block) - mPtr.getClass().getCreatureStats(mPtr).setBlock(false); - mHitState = CharState_None; + mHitState = isSwimming ? CharState_SwimKnockOut : CharState_KnockOut; + stats.setKnockedDown(true); } - else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0 - && mTimeUntilWake <= 0) + else if (knockdown) { mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; - mAnimation->disable(mCurrentHit); - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0); } -} + else if (recovery) + { + mHitState = isSwimming ? CharState_SwimHit : CharState_Hit; + priority = Priority_Hit; + } + else if (block) + { + mHitState = CharState_Block; + priority = Priority_Hit; + priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; + priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + startKey = "block start"; + stopKey = "block stop"; + } -void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force) -{ - if (!force && jump == mJumpState && idle == CharState_None) + mCurrentHit = hitStateToAnimGroup(mHitState); + + if (isRecovery()) + { + mCurrentHit = chooseRandomGroup(mCurrentHit); + if (mHitState == CharState_SwimHit && !mAnimation->hasAnimation(mCurrentHit)) + mCurrentHit = chooseRandomGroup(hitStateToAnimGroup(CharState_Hit)); + } + + if (!mAnimation->hasAnimation(mCurrentHit)) + { + // The hit animation is missing. Reset the current hit state and immediately cancel all states as if the animation were instantaneous. + mHitState = CharState_None; + mCurrentHit.clear(); + stats.setKnockedDown(false); + stats.setHitRecovery(false); + stats.setBlock(false); + resetCurrentIdleState(); return; + } - std::string jumpAnimName; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; - if (jump != JumpState_None) + // Cancel upper body animations + if (isKnockedOut() || isKnockedDown()) { - jumpAnimName = "jump"; - if(!weapShortGroup.empty()) + if (!mCurrentWeapon.empty()) + mAnimation->disable(mCurrentWeapon); + if (mUpperBodyState > UpperCharState_WeapEquiped) { - jumpAnimName += weapShortGroup; - if(!mAnimation->hasAnimation(jumpAnimName)) - { - jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); - - // If we apply jump only for lower body, do not reset idle animations. - // For upper body there will be idle animation. - if (jumpmask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) - idle = CharState_Idle; - } + mUpperBodyState = UpperCharState_WeapEquiped; + if (mWeaponType > ESM::Weapon::None) + mAnimation->showWeapons(true); + } + else if (mUpperBodyState < UpperCharState_WeapEquiped) + { + mUpperBodyState = UpperCharState_Nothing; } } + mAnimation->play(mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); +} + +void CharacterController::refreshJumpAnims(JumpingState jump, bool force) +{ if (!force && jump == mJumpState) return; - bool startAtLoop = (jump == mJumpState); - mJumpState = jump; - - if (!mCurrentJump.empty()) + if (jump == JumpState_None) { - mAnimation->disable(mCurrentJump); - mCurrentJump.clear(); + if (!mCurrentJump.empty()) + resetCurrentIdleState(); + resetCurrentJumpState(); + return; } - if(mJumpState == JumpState_InAir) + std::string weapShortGroup = getWeaponShortGroup(mWeaponType); + std::string jumpAnimName = "jump" + weapShortGroup; + MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; + if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName)) + jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); + + if (!mAnimation->hasAnimation(jumpAnimName)) { - if (mAnimation->hasAnimation(jumpAnimName)) - { - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, - 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); - mCurrentJump = jumpAnimName; - } + if (!mCurrentJump.empty()) + resetCurrentIdleState(); + resetCurrentJumpState(); + return; } + + bool startAtLoop = (jump == mJumpState); + mJumpState = jump; + clearStateAnimation(mCurrentJump); + + mCurrentJump = jumpAnimName; + if(mJumpState == JumpState_InAir) + mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); else if (mJumpState == JumpState_Landing) - { - if (mAnimation->hasAnimation(jumpAnimName)) - { - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, - 1.0f, "loop stop", "stop", 0.0f, 0); - mCurrentJump = jumpAnimName; - } - } + mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); } -bool CharacterController::onOpen() +bool CharacterController::onOpen() const { - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + if (mPtr.getType() == ESM::Container::sRecordId) { if (!mAnimation->hasAnimation("containeropen")) return true; @@ -391,9 +474,9 @@ bool CharacterController::onOpen() return true; } -void CharacterController::onClose() +void CharacterController::onClose() const { - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + if (mPtr.getType() == ESM::Container::sRecordId) { if (!mAnimation->hasAnimation("containerclose")) return; @@ -410,8 +493,7 @@ void CharacterController::onClose() std::string CharacterController::getWeaponAnimation(int weaponType) const { std::string weaponGroup = getWeaponType(weaponType)->mLongGroup; - bool isRealWeapon = weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell && weaponType != ESM::Weapon::None; - if (isRealWeapon && !mAnimation->hasAnimation(weaponGroup)) + if (isRealWeapon(weaponType) && !mAnimation->hasAnimation(weaponGroup)) { static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; @@ -421,17 +503,25 @@ std::string CharacterController::getWeaponAnimation(int weaponType) const // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) weaponGroup = twoHandFallback; - else if (isRealWeapon) + else weaponGroup = oneHandFallback; } + else if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr)) + return "attack1"; return weaponGroup; } -std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) +std::string CharacterController::getWeaponShortGroup(int weaponType) const { - bool isRealWeapon = mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; - if (!isRealWeapon) + if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr)) + return {}; + return getWeaponType(weaponType)->mShortGroup; +} + +std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const +{ + if (!isRealWeapon(mWeaponType)) { if (blendMask != nullptr) *blendMask = MWRender::Animation::BlendMask_LowerBody; @@ -439,16 +529,16 @@ std::string CharacterController::fallbackShortWeaponGroup(const std::string& bas return baseGroupName; } - static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mShortGroup; - static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mShortGroup; + static const std::string oneHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeOneHand); + static const std::string twoHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeTwoHand); std::string groupName = baseGroupName; const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType); // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones - if (isRealWeapon && weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) + if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) groupName += twoHandFallback; - else if (isRealWeapon) + else groupName += oneHandFallback; // Special case for crossbows - we shouls apply 1h animations a fallback only for lower body @@ -465,226 +555,181 @@ std::string CharacterController::fallbackShortWeaponGroup(const std::string& bas return groupName; } -void CharacterController::refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force) +void CharacterController::refreshMovementAnims(CharacterState movement, bool force) { - if (movement == mMovementState && idle == mIdleState && !force) + if (movement == mMovementState && !force) return; - // Reset idle if we actually play movement animations excepts of these cases: - // 1. When we play turning animations - // 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting) - bool resetIdle = (movement != CharState_None && !isTurning()); + std::string movementAnimName = movementStateToAnimGroup(movement); - std::string movementAnimName; - MWRender::Animation::BlendMask movemask; - const StateInfo *movestate; + if (movementAnimName.empty()) + { + if (!mCurrentMovement.empty()) + resetCurrentIdleState(); + resetCurrentMovementState(); + return; + } - movemask = MWRender::Animation::BlendMask_All; - movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(movement)); - if(movestate != sMovementListEnd) + mMovementState = movement; + std::string::size_type swimpos = movementAnimName.find("swim"); + if (!mAnimation->hasAnimation(movementAnimName)) { - movementAnimName = movestate->groupname; - if(!weapShortGroup.empty()) + if (swimpos != std::string::npos) { - std::string::size_type swimpos = movementAnimName.find("swim"); - if (swimpos == std::string::npos) - { - if (mWeaponType == ESM::Weapon::Spell && (movement == CharState_TurnLeft || movement == CharState_TurnRight)) // Spellcasting stance turning is a special case - movementAnimName = weapShortGroup + movementAnimName; - else - movementAnimName += weapShortGroup; - } + movementAnimName.erase(swimpos, 4); + swimpos = std::string::npos; + } + } - if(!mAnimation->hasAnimation(movementAnimName)) - { - movementAnimName = movestate->groupname; - if (swimpos == std::string::npos) - { - movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); + MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; - // If we apply movement only for lower body, do not reset idle animations. - // For upper body there will be idle animation. - if (movemask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) - idle = CharState_Idle; + std::string weapShortGroup = getWeaponShortGroup(mWeaponType); + if (swimpos == std::string::npos && !weapShortGroup.empty()) + { + std::string weapMovementAnimName; + // Spellcasting stance turning is a special case + if (mWeaponType == ESM::Weapon::Spell && isTurning()) + weapMovementAnimName = weapShortGroup + movementAnimName; + else + weapMovementAnimName = movementAnimName + weapShortGroup; - if (movemask == MWRender::Animation::BlendMask_LowerBody) - resetIdle = false; - } - } - } + if (!mAnimation->hasAnimation(weapMovementAnimName)) + weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); + + movementAnimName = weapMovementAnimName; } - if(force || movement != mMovementState) + if (!mAnimation->hasAnimation(movementAnimName)) { - mMovementState = movement; - if(movestate != sMovementListEnd) - { - if(!mAnimation->hasAnimation(movementAnimName)) - { - std::string::size_type swimpos = movementAnimName.find("swim"); - if (swimpos != std::string::npos) - { - movementAnimName.erase(swimpos, 4); - if (!weapShortGroup.empty()) - { - std::string weapMovementAnimName = movementAnimName + weapShortGroup; - if(mAnimation->hasAnimation(weapMovementAnimName)) - movementAnimName = weapMovementAnimName; - else - { - movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - if (movemask == MWRender::Animation::BlendMask_LowerBody) - resetIdle = false; - } - } - } + std::string::size_type runpos = movementAnimName.find("run"); + if (runpos != std::string::npos) + movementAnimName.replace(runpos, 3, "walk"); - if (swimpos == std::string::npos || !mAnimation->hasAnimation(movementAnimName)) - { - std::string::size_type runpos = movementAnimName.find("run"); - if (runpos != std::string::npos) - { - movementAnimName.replace(runpos, runpos+3, "walk"); - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); - } - else - movementAnimName.clear(); - } - } + if (!mAnimation->hasAnimation(movementAnimName)) + { + if (!mCurrentMovement.empty()) + resetCurrentIdleState(); + resetCurrentMovementState(); + return; } + } - // If we're playing the same animation, start it from the point it ended - float startpoint = 0.f; - if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) - mAnimation->getInfo(mCurrentMovement, &startpoint); + // If we're playing the same animation, start it from the point it ended + float startpoint = 0.f; + if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) + mAnimation->getInfo(mCurrentMovement, &startpoint); - mMovementAnimationControlled = true; + mMovementAnimationControlled = true; - mAnimation->disable(mCurrentMovement); + clearStateAnimation(mCurrentMovement); + mCurrentMovement = movementAnimName; - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); + // 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. + mAdjustMovementAnimSpeed = true; + if (mPtr.getClass().getType() == ESM::Creature::sRecordId && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) + { + CharacterState walkState = runStateToWalkState(mMovementState); + std::string anim = movementStateToAnimGroup(walkState); - mCurrentMovement = movementAnimName; - if(!mCurrentMovement.empty()) + mMovementAnimSpeed = mAnimation->getVelocity(anim); + if (mMovementAnimSpeed <= 1.0f) { - if (resetIdle) - { - mAnimation->disable(mCurrentIdle); - mIdleState = CharState_None; - idle = CharState_None; - } - - // 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. - std::string anim = mCurrentMovement; - mAdjustMovementAnimSpeed = true; - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() - && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) - { - CharacterState walkState = runStateToWalkState(mMovementState); - const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); - anim = stateinfo->groupname; - - mMovementAnimSpeed = mAnimation->getVelocity(anim); - if (mMovementAnimSpeed <= 1.0f) - { - // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), - // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist - // we will play without any scaling. - // Makes the speed attribute of most water creatures totally useless. - // And again, this can not be fixed without patching game data. - mAdjustMovementAnimSpeed = false; - mMovementAnimSpeed = 1.f; - } - } - else - { - mMovementAnimSpeed = mAnimation->getVelocity(anim); - - if (mMovementAnimSpeed <= 1.0f) - { - // The first person anims don't have any velocity to calculate a speed multiplier from. - // We use the third person velocities instead. - // FIXME: should be pulled from the actual animation, but it is not presently loaded. - 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; - } - } + // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), + // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist + // we will play without any scaling. + // Makes the speed attribute of most water creatures totally useless. + // And again, this can not be fixed without patching game data. + mAdjustMovementAnimSpeed = false; + mMovementAnimSpeed = 1.f; + } + } + else + { + mMovementAnimSpeed = mAnimation->getVelocity(mCurrentMovement); - mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, - 1.f, "start", "stop", startpoint, ~0ul, true); + if (mMovementAnimSpeed <= 1.0f) + { + // The first person anims don't have any velocity to calculate a speed multiplier from. + // We use the third person velocities instead. + // FIXME: should be pulled from the actual animation, but it is not presently loaded. + 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; } - else - mMovementState = CharState_None; } + + mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); } -void CharacterController::refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force) +void CharacterController::refreshIdleAnims(CharacterState idle, bool force) { // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), // the idle animation should be displayed if (((mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped) - || (mMovementState != CharState_None && !isTurning()) - || mHitState != CharState_None) - && !mPtr.getClass().isBipedal(mPtr)) - idle = CharState_None; + || mMovementState != CharState_None || mHitState != CharState_None) && !mPtr.getClass().isBipedal(mPtr)) + { + resetCurrentIdleState(); + return; + } + + if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty())) + return; - if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty())) + mIdleState = idle; + + std::string idleGroup = idleStateToAnimGroup(mIdleState); + if (idleGroup.empty()) { - mIdleState = idle; - size_t numLoops = ~0ul; + resetCurrentIdleState(); + return; + } - std::string idleGroup; - MWRender::Animation::AnimPriority idlePriority (Priority_Default); - // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to - // "idle"+weapon or "idle". - if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) - { - idleGroup = "idleswim"; - idlePriority = Priority_SwimIdle; - } - else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) - { - idleGroup = "idlesneak"; - idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; - } - else if(mIdleState != CharState_None) - { - idleGroup = "idle"; - if(!weapShortGroup.empty()) - { - idleGroup += weapShortGroup; - if(!mAnimation->hasAnimation(idleGroup)) - { - idleGroup = fallbackShortWeaponGroup("idle"); - } + MWRender::Animation::AnimPriority priority = getIdlePriority(mIdleState); + size_t numLoops = std::numeric_limits::max(); - // 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 - numLoops = 1 + Misc::Rng::rollDice(4); - } - } + // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to + // "idle"+weapon or "idle". + bool fallback = mIdleState != CharState_Idle && !mAnimation->hasAnimation(idleGroup); + if (fallback) + { + priority = getIdlePriority(CharState_Idle); + idleGroup = idleStateToAnimGroup(CharState_Idle); + } - // There is no need to restart anim if the new and old anims are the same. - // Just update a number of loops. - float startPoint = 0; - if (!mCurrentIdle.empty() && mCurrentIdle == idleGroup) + if (fallback || mIdleState == CharState_Idle || mIdleState == CharState_SpecialIdle) + { + std::string weapShortGroup = getWeaponShortGroup(mWeaponType); + if (!weapShortGroup.empty()) { - mAnimation->getInfo(mCurrentIdle, &startPoint); - } + std::string weapIdleGroup = idleGroup + weapShortGroup; + if (!mAnimation->hasAnimation(weapIdleGroup)) + weapIdleGroup = fallbackShortWeaponGroup(idleGroup); + idleGroup = weapIdleGroup; - if(!mCurrentIdle.empty()) - mAnimation->disable(mCurrentIdle); + // 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 + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + numLoops = 1 + Misc::Rng::rollDice(4, prng); + } + } - mCurrentIdle = idleGroup; - if(!mCurrentIdle.empty()) - mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", startPoint, numLoops, true); + if (!mAnimation->hasAnimation(idleGroup)) + { + resetCurrentIdleState(); + return; } + + float startPoint = 0.f; + // There is no need to restart anim if the new and old anims are the same. + // Just update the number of loops. + if (mCurrentIdle == idleGroup) + mAnimation->getInfo(mCurrentIdle, &startPoint); + + clearStateAnimation(mCurrentIdle); + mCurrentIdle = idleGroup; + mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) @@ -693,67 +738,38 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat if (isPersistentAnimPlaying()) return; - if (mPtr.getClass().isActor()) - refreshHitRecoilAnims(idle); - - std::string weap; - if (mPtr.getClass().hasInventoryStore(mPtr)) - weap = getWeaponType(mWeaponType)->mShortGroup; - - refreshJumpAnims(weap, jump, idle, force); - refreshMovementAnims(weap, movement, idle, force); + refreshHitRecoilAnims(); + refreshJumpAnims(jump, force); + refreshMovementAnims(movement, force); // idle handled last as it can depend on the other states - refreshIdleAnims(weap, idle, force); + refreshIdleAnims(idle, force); } void CharacterController::playDeath(float startpoint, CharacterState death) { + mDeathState = death; + mCurrentDeath = deathStateToAnimGroup(mDeathState); + // Make sure the character was swimming upon death for forward-compatibility - const bool wasSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - - switch (death) - { - case CharState_SwimDeath: - mCurrentDeath = "swimdeath"; - break; - case CharState_SwimDeathKnockDown: - mCurrentDeath = (wasSwimming ? "swimdeathknockdown" : "deathknockdown"); - break; - case CharState_SwimDeathKnockOut: - mCurrentDeath = (wasSwimming ? "swimdeathknockout" : "deathknockout"); - break; - case CharState_DeathKnockDown: - mCurrentDeath = "deathknockdown"; - break; - case CharState_DeathKnockOut: - mCurrentDeath = "deathknockout"; - break; - default: - mCurrentDeath = "death" + std::to_string(death - CharState_Death1 + 1); + if (!MWBase::Environment::get().getWorld()->isSwimming(mPtr)) + { + if (mDeathState == CharState_SwimDeathKnockDown) + mCurrentDeath = "deathknockdown"; + else if (mDeathState == CharState_SwimDeathKnockOut) + mCurrentDeath = "deathknockout"; } - mDeathState = death; mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). // However, they could still trigger text keys, such as Hit events, or sounds. - mMovementState = CharState_None; - mAnimation->disable(mCurrentMovement); - mCurrentMovement = ""; - mUpperBodyState = UpperCharState_Nothing; - mAnimation->disable(mCurrentWeapon); - mCurrentWeapon = ""; - mHitState = CharState_None; - mAnimation->disable(mCurrentHit); - mCurrentHit = ""; - mIdleState = CharState_None; - mAnimation->disable(mCurrentIdle); - mCurrentIdle = ""; - mJumpState = JumpState_None; - mAnimation->disable(mCurrentJump); - mCurrentJump = ""; + resetCurrentMovementState(); + resetCurrentWeaponState(); + resetCurrentHitState(); + resetCurrentIdleState(); + resetCurrentJumpState(); mMovementAnimationControlled = true; mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, @@ -776,30 +792,12 @@ void CharacterController::playRandomDeath(float startpoint) MWBase::Environment::get().getWorld()->useDeathCamera(); } - if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) - { - mDeathState = CharState_SwimDeathKnockDown; - } - else if(mHitState == CharState_SwimKnockOut && mAnimation->hasAnimation("swimdeathknockout")) - { - mDeathState = CharState_SwimDeathKnockOut; - } - else if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) - { + mDeathState = hitStateToDeathState(mHitState); + if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr)) mDeathState = CharState_SwimDeath; - } - else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) - { - mDeathState = CharState_DeathKnockDown; - } - else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) - { - mDeathState = CharState_DeathKnockOut; - } - else - { + + if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState))) mDeathState = chooseRandomDeathState(); - } // Do not interrupt scripted animation by death if (isPersistentAnimPlaying()) @@ -824,29 +822,7 @@ std::string CharacterController::chooseRandomAttackAnimation() const CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) : mPtr(ptr) - , mWeapon(MWWorld::Ptr()) , mAnimation(anim) - , mIdleState(CharState_None) - , mMovementState(CharState_None) - , mMovementAnimSpeed(0.f) - , mAdjustMovementAnimSpeed(false) - , mHasMovedInXY(false) - , mMovementAnimationControlled(true) - , mDeathState(CharState_None) - , mFloatToSurface(true) - , mHitState(CharState_None) - , mUpperBodyState(UpperCharState_Nothing) - , mJumpState(JumpState_None) - , mWeaponType(ESM::Weapon::None) - , mAttackStrength(0.f) - , mSkipAnim(false) - , mSecondsOfSwimming(0) - , mSecondsOfRunning(0) - , mTurnAnimationThreshold(0) - , mAttackingOrSpell(false) - , mCastingManualSpell(false) - , mTimeUntilWake(0.f) - , mIsMovingBackward(false) { if(!mAnimation) return; @@ -883,7 +859,11 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim } if(!cls.getCreatureStats(mPtr).isDead()) + { mIdleState = CharState_Idle; + if (cls.getCreatureStats(mPtr).getFallHeight() > 0) + mJumpState = JumpState_InAir; + } else { const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); @@ -906,12 +886,10 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim { /* Don't accumulate with non-actors. */ mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); - - mIdleState = CharState_Idle; } - // Do not update animation status for dead actors - if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) + // Update animation status for living actors + if (mDeathState == CharState_None && cls.isActor() && !cls.getCreatureStats(mPtr).isDead()) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); @@ -928,34 +906,28 @@ CharacterController::~CharacterController() } } -void split(const std::string &s, char delim, std::vector &elems) { - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, delim)) { - elems.push_back(item); - } -} - -void CharacterController::handleTextKey(const std::string &groupname, NifOsg::TextKeyMap::ConstIterator key, const NifOsg::TextKeyMap& map) +void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { - const std::string &evt = key->second; + std::string_view evt = key->second; - if(evt.compare(0, 7, "sound: ") == 0) + if (evt.substr(0, 7) == "sound: ") { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f); return; } - if(evt.compare(0, 10, "soundgen: ") == 0) + + auto& charClass = mPtr.getClass(); + if (evt.substr(0, 10) == "soundgen: ") { - std::string soundgen = evt.substr(10); + std::string soundgen = std::string(evt.substr(10)); // The event can optionally contain volume and pitch modifiers float volume=1.f, pitch=1.f; - if (soundgen.find(" ") != std::string::npos) + if (soundgen.find(' ') != std::string::npos) { std::vector tokens; - split(soundgen, ' ', tokens); + Misc::StringUtils::split(soundgen, tokens); soundgen = tokens[0]; if (tokens.size() >= 2) { @@ -971,12 +943,11 @@ void CharacterController::handleTextKey(const std::string &groupname, NifOsg::Te } } - std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen); + std::string sound = charClass.getSoundIdFromSndGen(mPtr, soundgen); if(!sound.empty()) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - // NB: landing sound is not played for NPCs here - if(soundgen == "left" || soundgen == "right" || soundgen == "land") + if (soundgen == "left" || soundgen == "right") { sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); @@ -989,88 +960,89 @@ void CharacterController::handleTextKey(const std::string &groupname, NifOsg::Te return; } - if(evt.compare(0, groupname.size(), groupname) != 0 || - evt.compare(groupname.size(), 2, ": ") != 0) + if (evt.substr(0, groupname.size()) != groupname || evt.substr(groupname.size(), 2) != ": ") { // Not ours, skip it return; } - size_t off = groupname.size()+2; - size_t len = evt.size() - off; - if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0) - mAnimation->showCarriedLeft(true); - else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0) - mAnimation->showCarriedLeft(false); - else if(evt.compare(off, len, "equip attach") == 0) - mAnimation->showWeapons(true); - else if(evt.compare(off, len, "unequip detach") == 0) - mAnimation->showWeapons(false); - else if(evt.compare(off, len, "chop hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if(evt.compare(off, len, "slash hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if(evt.compare(off, len, "thrust hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - else if(evt.compare(off, len, "hit") == 0) + std::string_view action = evt.substr(groupname.size() + 2); + if (action == "equip attach") + { + 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); + } + else if (action == "chop hit") + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + else if (action == "slash hit") + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + else if (action == "thrust hit") + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + else if (action == "hit") { if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); else - mPtr.getClass().hit(mPtr, mAttackStrength); + charClass.hit(mPtr, mAttackStrength); } - else if (!groupname.empty() - && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) - && evt.compare(off, len, "start") == 0) + else if (isRandomAttackAnimation(groupname) && action == "start") { std::multimap::const_iterator hitKey = key; + std::string hitKeyName = std::string(groupname) + ": hit"; + std::string stopKeyName = std::string(groupname) + ": stop"; // Not all animations have a hit key defined. If there is none, the hit happens with the start key. bool hasHitKey = false; while (hitKey != map.end()) { - if (hitKey->second == groupname + ": hit") + if (hitKey->second == hitKeyName) { hasHitKey = true; break; } - if (hitKey->second == groupname + ": stop") + if (hitKey->second == stopKeyName) break; ++hitKey; } if (!hasHitKey) { if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); } } - else if (evt.compare(off, len, "shoot attach") == 0) + else if (action == "shoot attach") mAnimation->attachArrow(); - else if (evt.compare(off, len, "shoot release") == 0) + else if (action == "shoot release") mAnimation->releaseArrow(mAttackStrength); - else if (evt.compare(off, len, "shoot follow attach") == 0) + else if (action == "shoot follow attach") mAnimation->attachArrow(); - - else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release" - // Make sure this key is actually for the RangeType we are casting. The flame atronach has - // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. - && evt.compare(off, len, mAttackType + " release") == 0) + // Make sure this key is actually for the RangeType we are casting. The flame atronach has + // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. + else if (groupname == "spellcast" && action == mAttackType + " release") { MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); mCastingManualSpell = false; } - - else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) - mPtr.getClass().block(mPtr); - else if (groupname == "containeropen" && evt.compare(off, len, "loot") == 0) + else if (groupname == "shield" && action == "block hit") + charClass.block(mPtr); + else if (groupname == "containeropen" && action == "loot") MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr); } @@ -1079,7 +1051,7 @@ void CharacterController::updatePtr(const MWWorld::Ptr &ptr) mPtr = ptr; } -void CharacterController::updateIdleStormState(bool inwater) +void CharacterController::updateIdleStormState(bool inwater) const { if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperCharState_Nothing || inwater) { @@ -1087,9 +1059,10 @@ void CharacterController::updateIdleStormState(bool inwater) return; } - if (MWBase::Environment::get().getWorld()->isInStorm()) + const auto world = MWBase::Environment::get().getWorld(); + if (world->isInStorm()) { - osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); + osg::Vec3f stormDirection = world->getStormDirection(); osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); stormDirection.normalize(); characterDirection.normalize(); @@ -1114,97 +1087,6 @@ void CharacterController::updateIdleStormState(bool inwater) } } -bool CharacterController::updateCreatureState() -{ - const MWWorld::Class &cls = mPtr.getClass(); - CreatureStats &stats = cls.getCreatureStats(mPtr); - - int weapType = ESM::Weapon::None; - if(stats.getDrawState() == DrawState_Weapon) - weapType = ESM::Weapon::HandToHand; - else if (stats.getDrawState() == DrawState_Spell) - weapType = ESM::Weapon::Spell; - - if (weapType != mWeaponType) - { - mWeaponType = weapType; - if (mAnimation->isPlaying(mCurrentWeapon)) - mAnimation->disable(mCurrentWeapon); - } - - if(mAttackingOrSpell) - { - if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) - { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - - std::string startKey = "start"; - std::string stopKey = "stop"; - if (weapType == ESM::Weapon::Spell) - { - const std::string spellid = stats.getSpells().getSelectedSpell(); - bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); - - if (!spellid.empty() && canCast) - { - MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell); - cast.playSpellCastingEffects(spellid, false); - - if (!mAnimation->hasAnimation("spellcast")) - { - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; - } - else - { - const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); - const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); - - switch(effectentry.mRange) - { - case 0: mAttackType = "self"; break; - case 1: mAttackType = "touch"; break; - case 2: mAttackType = "target"; break; - } - - startKey = mAttackType + " " + startKey; - stopKey = mAttackType + " " + stopKey; - mCurrentWeapon = "spellcast"; - } - } - else - mCurrentWeapon = ""; - } - - if (weapType != ESM::Weapon::Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation - { - mCurrentWeapon = chooseRandomAttackAnimation(); - } - - if (!mCurrentWeapon.empty()) - { - mAnimation->play(mCurrentWeapon, Priority_Weapon, - MWRender::Animation::BlendMask_All, true, - 1, startKey, stopKey, - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; - - mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); - - if (weapType == ESM::Weapon::HandToHand) - playSwishSound(0.0f); - } - } - - mAttackingOrSpell = false; - } - - bool animPlaying = mAnimation->getInfo(mCurrentWeapon); - if (!animPlaying) - mUpperBodyState = UpperCharState_Nothing; - return false; -} - bool CharacterController::updateCarriedLeftVisible(const int weaptype) const { // Shields/torches shouldn't be visible during any operation involving two hands @@ -1213,14 +1095,18 @@ bool CharacterController::updateCarriedLeftVisible(const int weaptype) const return mAnimation->updateCarriedLeftVisible(weaptype); } -bool CharacterController::updateWeaponState(CharacterState& idle) +bool CharacterController::updateState(CharacterState idle) { + const auto world = MWBase::Environment::get().getWorld(); + auto& prng = world->getPrng(); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); int weaptype = ESM::Weapon::None; - if(stats.getDrawState() == DrawState_Weapon) + if(stats.getDrawState() == DrawState::Weapon) weaptype = ESM::Weapon::HandToHand; - else if (stats.getDrawState() == DrawState_Spell) + else if (stats.getDrawState() == DrawState::Spell) weaptype = ESM::Weapon::Spell; const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); @@ -1228,11 +1114,11 @@ bool CharacterController::updateWeaponState(CharacterState& idle) std::string upSoundId; std::string downSoundId; bool weaponChanged = false; - if (mPtr.getClass().hasInventoryStore(mPtr)) + if (cls.hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::ContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); - if(stats.getDrawState() == DrawState_Spell) + if(stats.getDrawState() == DrawState::Spell) weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None) @@ -1256,13 +1142,13 @@ bool CharacterController::updateWeaponState(CharacterState& idle) // For biped actors, blend weapon animations with lower body animations with higher priority MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); - if (mPtr.getClass().isBipedal(mPtr)) + if (cls.isBipedal(mPtr)) priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; bool forcestateupdate = false; // We should not play equipping animation and sound during weapon->weapon transition - bool isStillWeapon = weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None && + const bool isStillWeapon = weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; // If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires), @@ -1270,12 +1156,12 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped) { forcestateupdate = true; + if (!mCurrentWeapon.empty()) + mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; - mAttackingOrSpell = false; - mAnimation->disable(mCurrentWeapon); + setAttackingOrSpell(false); mAnimation->showWeapons(true); - if (mPtr == getPlayer()) - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); + stats.setAttackingOrSpell(false); } if(!isKnockedOut() && !isKnockedDown() && !isRecovery()) @@ -1316,7 +1202,6 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if(!downSoundId.empty()) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f); } } @@ -1344,7 +1229,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (!isStillWeapon) { - mAnimation->disable(mCurrentWeapon); + clearStateAnimation(mCurrentWeapon); if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); @@ -1372,21 +1257,19 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if(isWerewolf) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfEquip"); + const MWWorld::ESMStore &store = world->getStore(); + const ESM::Sound *sound = store.get().searchRandom("WolfEquip", prng); if(sound) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } } mWeaponType = weaptype; - mCurrentWeapon = getWeaponAnimation(mWeaponType); + mCurrentWeapon = weapgroup; if(!upSoundId.empty() && !isStillWeapon) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); } } @@ -1394,8 +1277,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) // Make sure that we disabled unequipping animation if (mUpperBodyState == UpperCharState_UnEquipingWeap) { - mUpperBodyState = UpperCharState_Nothing; - mAnimation->disable(mCurrentWeapon); + resetCurrentWeaponState(); mWeaponType = ESM::Weapon::None; mCurrentWeapon = getWeaponAnimation(mWeaponType); } @@ -1404,10 +1286,9 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if(isWerewolf) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + if(stats.getStance(MWMechanics::CreatureStats::Stance_Run) && mHasMovedInXY - && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) + && !world->isSwimming(mPtr) && mWeaponType == ESM::Weapon::None) { if(!sndMgr->getSoundPlaying(mPtr, "WolfRun")) @@ -1422,11 +1303,11 @@ bool CharacterController::updateWeaponState(CharacterState& idle) bool ammunition = true; bool isWeapon = false; float weapSpeed = 1.f; - if (mPtr.getClass().hasInventoryStore(mPtr)) + if (cls.hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); - isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); + isWeapon = (weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId); if (isWeapon) { weapSpeed = weapon->get()->mBase->mData.mSpeed; @@ -1438,7 +1319,8 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) { - mAnimation->disable(mCurrentWeapon); + if (!mCurrentWeapon.empty()) + mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; } } @@ -1450,19 +1332,17 @@ bool CharacterController::updateWeaponState(CharacterState& idle) float complete; bool animPlaying; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; - if(mAttackingOrSpell) + if(getAttackingOrSpell()) { - MWWorld::Ptr player = getPlayer(); - bool resetIdle = ammunition; if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); + world->breakInvisibility(mPtr); mAttackStrength = 0; - // Randomize attacks for non-bipedal creatures with Weapon flag - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() && - !mPtr.getClass().isBipedal(mPtr) && + // Randomize attacks for non-bipedal creatures + if (cls.getType() == ESM::Creature::sRecordId && + !cls.isBipedal(mPtr) && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) { mCurrentWeapon = chooseRandomAttackAnimation(); @@ -1472,11 +1352,9 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { // Unset casting flag, otherwise pressing the mouse button down would // continue casting every frame if there is no animation - mAttackingOrSpell = false; - if (mPtr == player) + setAttackingOrSpell(false); + if (mPtr == getPlayer()) { - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); - // For the player, set the spell we want to cast // This has to be done at the start of the casting animation, // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) @@ -1485,13 +1363,13 @@ bool CharacterController::updateWeaponState(CharacterState& idle) } std::string spellid = stats.getSpells().getSelectedSpell(); bool isMagicItem = false; - bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); + bool canCast = mCastingManualSpell || world->startSpellCast(mPtr); if (spellid.empty()) { - if (mPtr.getClass().hasInventoryStore(mPtr)) + if (cls.hasInventoryStore(mPtr)) { - MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); if (inv.getSelectedEnchantItem() != inv.end()) { const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem(); @@ -1505,7 +1383,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (isMagicItem && !useCastingAnimations) { // Enchanted items by default do not use casting animations - MWBase::Environment::get().getWorld()->castSpell(mPtr); + world->castSpell(mPtr); resetIdle = false; } else if(!spellid.empty() && canCast) @@ -1514,7 +1392,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) cast.playSpellCastingEffects(spellid, isMagicItem); std::vector effects; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore &store = world->getStore(); if (isMagicItem) { const ESM::Enchantment *enchantment = store.get().find(spellid); @@ -1528,15 +1406,21 @@ bool CharacterController::updateWeaponState(CharacterState& idle) const ESM::MagicEffect *effect = store.get().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + const ESM::Static* castStatic = world->getStore().get().find ("VFX_Hands"); + + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect { if (mAnimation->getNode("Bip01 L Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); + mAnimation->addEffect( + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), + -1, false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); + mAnimation->addEffect( + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), + -1, false, "Bip01 R Hand", effect->mParticle); } const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation @@ -1547,7 +1431,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { startKey = "start"; stopKey = "stop"; - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately + world->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately mCastingManualSpell = false; } else @@ -1576,17 +1460,17 @@ bool CharacterController::updateWeaponState(CharacterState& idle) } else if(mWeaponType == ESM::Weapon::PickProbe) { - MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWWorld::ContainerStoreIterator weapon = cls.getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr item = *weapon; // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); + MWWorld::Ptr target = world->getFacedObject(); std::string resultMessage, resultSound; if(!target.isEmpty()) { - if(item.getTypeName() == typeid(ESM::Lockpick).name()) + if(item.getType() == ESM::Lockpick::sRecordId) Security(mPtr).pickLock(target, item, resultMessage, resultSound); - else if(item.getTypeName() == typeid(ESM::Probe).name()) + else if(item.getType() == ESM::Probe::sRecordId) Security(mPtr).probeTrap(target, item, resultMessage, resultSound); } mAnimation->play(mCurrentWeapon, priorityWeapon, @@ -1597,8 +1481,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if(!resultMessage.empty()) MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); if(!resultSound.empty()) - MWBase::Environment::get().getSoundManager()->playSound3D(target, resultSound, - 1.0f, 1.0f); + sndMgr->playSound3D(target, resultSound, 1.0f, 1.0f); } else if (ammunition) { @@ -1624,18 +1507,18 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { if (isWeapon) { - MWWorld::ConstContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWWorld::ConstContainerStoreIterator weapon = cls.getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); mAttackType = getBestAttack(weapon->get()->mBase); } else { // There is no "best attack" for Hand-to-Hand - setAttackTypeRandomly(mAttackType); + mAttackType = getRandomAttackType(); } } else { - setAttackTypeBasedOnMovement(); + mAttackType = getMovementBasedAttackType(); } } // else if (mPtr != getPlayer()) use mAttackType set by AiCombat @@ -1647,7 +1530,15 @@ bool CharacterController::updateWeaponState(CharacterState& idle) MWRender::Animation::BlendMask_All, false, weapSpeed, startKey, stopKey, 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; + if(mAnimation->getCurrentTime(mCurrentWeapon) != -1.f) + { + mUpperBodyState = UpperCharState_StartToMinAttack; + if (isRandomAttackAnimation(mCurrentWeapon)) + { + mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng)); + playSwishSound(0.0f); + } + } } } @@ -1656,8 +1547,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) idle != CharState_IdleSneak && idle != CharState_IdleSwim && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim) { - mAnimation->disable(mCurrentIdle); - mIdleState = CharState_None; + resetCurrentIdleState(); } animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); @@ -1677,17 +1567,15 @@ bool CharacterController::updateWeaponState(CharacterState& idle) // most creatures don't actually have an attack wind-up animation, so use a uniform random value // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. - attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); + attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng)); } if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(isWerewolf) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfSwing"); + const MWWorld::ESMStore &store = world->getStore(); + const ESM::Sound *sound = store.get().searchRandom("WolfSwing", prng); if(sound) sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } @@ -1715,7 +1603,8 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (mWeaponType > ESM::Weapon::None) mAnimation->showWeapons(true); } - mAnimation->disable(mCurrentWeapon); + if (!mCurrentWeapon.empty()) + mAnimation->disable(mCurrentWeapon); } } @@ -1784,7 +1673,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) // Note: if the "min attack"->"max attack" is a stub, "play" it anyway. Attack strength will be random. float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); - if (mAttackingOrSpell || minAttackTime == maxAttackTime) + if (getAttackingOrSpell() || minAttackTime == maxAttackTime) { start = mAttackType+" min attack"; stop = mAttackType+" max attack"; @@ -1852,18 +1741,20 @@ bool CharacterController::updateWeaponState(CharacterState& idle) } else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon)) { - mAnimation->disable(mCurrentWeapon); + clearStateAnimation(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; } - if (mPtr.getClass().hasInventoryStore(mPtr)) + if (cls.hasInventoryStore(mPtr)) { - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() + if(torch != inv.end() && torch->getType() == ESM::Light::sRecordId && updateCarriedLeftVisible(mWeaponType)) - { + if (mAnimation->isPlaying("shield")) + mAnimation->disable("shield"); + mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); } @@ -1898,7 +1789,7 @@ void CharacterController::updateAnimQueue() mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); } -void CharacterController::update(float duration, bool animationOnly) +void CharacterController::update(float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Class &cls = mPtr.getClass(); @@ -1907,9 +1798,6 @@ void CharacterController::update(float duration, bool animationOnly) updateMagicEffects(); - if (isKnockedOut()) - mTimeUntilWake -= duration; - bool isPlayer = mPtr == MWMechanics::getPlayer(); bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); @@ -1935,7 +1823,7 @@ void CharacterController::update(float duration, bool animationOnly) bool flying = world->isFlying(mPtr); bool solid = world->isActorCollisionEnabled(mPtr); // Can't run and sneak while flying (see speed formula in Npc/Creature::getSpeed) - bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying; + bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying && !inwater; bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; CreatureStats &stats = cls.getCreatureStats(mPtr); Movement& movementSettings = cls.getMovementSettings(mPtr); @@ -1965,7 +1853,7 @@ void CharacterController::update(float duration, bool animationOnly) movementSettings.mSpeedFactor *= 2.f; static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); - if (smoothMovement && !isFirstPersonPlayer) + if (smoothMovement) { static const float playerTurningCoef = 1.0 / std::max(0.01f, Settings::Manager::getFloat("smooth movement player turning delay", "Game")); float angle = mPtr.getRefData().getPosition().rot[2]; @@ -1975,7 +1863,9 @@ void CharacterController::update(float duration, bool animationOnly) float deltaLen = delta.length(); float maxDelta; - if (std::abs(speedDelta) < deltaLen / 2) + if (isFirstPersonPlayer) + maxDelta = 1; + else if (std::abs(speedDelta) < deltaLen / 2) // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f); else if (isPlayer && speedDelta < -deltaLen / 2) @@ -2013,11 +1903,14 @@ void CharacterController::update(float duration, bool animationOnly) bool canMove = cls.getMaxSpeed(mPtr) > 0; static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); if (!turnToMovementDirection || isFirstPersonPlayer) + { movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; + stats.setSideMovementAngle(0); + } else if (canMove) { float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); - movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater) + movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState::Nothing || inwater) && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f); if (movementSettings.mIsStrafing) targetMovementAngle = 0; @@ -2030,13 +1923,13 @@ void CharacterController::update(float duration, bool animationOnly) mIsMovingBackward = vec.y() < 0; float maxDelta = osg::PI * duration * (2.5f - cosDelta); - delta = osg::clampBetween(delta, -maxDelta, maxDelta); + delta = std::clamp(delta, -maxDelta, maxDelta); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); - if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater) + if (stats.getDrawState() == MWMechanics::DrawState::Nothing || inwater) mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); else mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); @@ -2047,15 +1940,13 @@ void CharacterController::update(float duration, bool animationOnly) vec.x() *= speed; vec.y() *= speed; - if(mHitState != CharState_None && mJumpState == JumpState_None) + if(mHitState != CharState_None && mHitState != CharState_Block && mJumpState == JumpState_None) vec = osg::Vec3f(); CharacterState movestate = CharState_None; - CharacterState idlestate = CharState_SpecialIdle; + CharacterState idlestate = CharState_None; JumpingState jumpstate = JumpState_None; - bool forcestateupdate = false; - mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; isrunning = isrunning && mHasMovedInXY; @@ -2131,8 +2022,6 @@ void CharacterController::update(float duration, bool animationOnly) if(!onground && !flying && !inwater && solid) { // In the air (either getting up —ascending part of jump— or falling). - - forcestateupdate = (mJumpState != JumpState_InAir); jumpstate = JumpState_InAir; static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->mValue.getFloat(); @@ -2160,13 +2049,9 @@ void CharacterController::update(float duration, bool animationOnly) } else if(mJumpState == JumpState_InAir && !inwater && !flying && solid) { - forcestateupdate = true; jumpstate = JumpState_Landing; vec.z() = 0.0f; - // We should reset idle animation during landing - mAnimation->disable(mCurrentIdle); - float height = cls.getCreatureStats(mPtr).land(isPlayer); float healthLost = getFallDamage(mPtr, height); @@ -2265,18 +2150,19 @@ void CharacterController::update(float duration, bool animationOnly) sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); } - if (turnToMovementDirection) + if (turnToMovementDirection && !isFirstPersonPlayer && + (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward || + movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) { - float targetSwimmingPitch; - if (inwater && vec.y() != 0 && !isFirstPersonPlayer && !movementSettings.mIsStrafing) - targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; - else - targetSwimmingPitch = 0; - float maxSwimPitchDelta = 3.0f * duration; float swimmingPitch = mAnimation->getBodyPitchRadians(); - swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); + float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; + float maxSwimPitchDelta = 3.0f * duration; + swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } + else + mAnimation->setBodyPitchRadians(0); + static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game"); if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection) { @@ -2318,8 +2204,11 @@ void CharacterController::update(float duration, bool animationOnly) } } - if(movestate != CharState_None && !isTurning()) + if (movestate != CharState_None) + { clearAnimQueue(); + jumpstate = JumpState_None; + } if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) { @@ -2335,13 +2224,7 @@ void CharacterController::update(float duration, bool animationOnly) if (!mSkipAnim) { - // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used. - if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr)) - forcestateupdate = updateWeaponState(idlestate) || forcestateupdate; - else - forcestateupdate = updateCreatureState() || forcestateupdate; - - refreshCurrentAnims(idlestate, movestate, jumpstate, forcestateupdate); + refreshCurrentAnims(idlestate, movestate, jumpstate, updateState(idlestate)); updateIdleStormState(inwater); } @@ -2372,20 +2255,20 @@ void CharacterController::update(float duration, bool animationOnly) if(!isKnockedDown() && !isKnockedOut()) { if (rot != osg::Vec3f()) - world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); + world->rotateObject(mPtr, rot, true); } else //avoid z-rotating for knockdown { if (rot.x() != 0 && rot.y() != 0) - world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); + { + rot.z() = 0.0f; + world->rotateObject(mPtr, rot, true); + } } - if (!animationOnly && !mMovementAnimationControlled) + if (!mMovementAnimationControlled) world->queueMovement(mPtr, vec); } - else if (!animationOnly) - // We must always queue movement, even if there is none, to apply gravity. - world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); movement = vec; movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; @@ -2407,9 +2290,6 @@ void CharacterController::update(float duration, bool animationOnly) if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) playDeath(1.f, mDeathState); } - // We must always queue movement, even if there is none, to apply gravity. - if (!animationOnly) - world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); } bool isPersist = isPersistentAnimPlaying(); @@ -2423,7 +2303,7 @@ void CharacterController::update(float duration, bool animationOnly) moved.y() *= scale; // Ensure we're moving in generally the right direction... - if(speed > 0.f) + if (speed > 0.f && moved != osg::Vec3f()) { float l = moved.length(); if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 || @@ -2439,11 +2319,17 @@ void CharacterController::update(float duration, bool animationOnly) } } - if (mFloatToSurface && cls.isActor() && cls.getCreatureStats(mPtr).isDead() && cls.canSwim(mPtr)) - moved.z() = 1.0; + if (mFloatToSurface && cls.isActor()) + { + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode && cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0)) + { + moved.z() = 1.0; + } + } // Update movement - if(!animationOnly && mMovementAnimationControlled && mPtr.getClass().isActor()) + if(mMovementAnimationControlled && mPtr.getClass().isActor()) world->queueMovement(mPtr, moved); mSkipAnim = false; @@ -2451,7 +2337,7 @@ void CharacterController::update(float duration, bool animationOnly) mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } -void CharacterController::persistAnimationState() +void CharacterController::persistAnimationState() const { ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); @@ -2504,12 +2390,11 @@ void CharacterController::unpersistAnimationState() { float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); + float time = std::clamp(anim.mTime, start, stop); complete = (time - start) / (stop - start); } - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); + clearStateAnimation(mCurrentIdle); mIdleState = CharState_SpecialIdle; bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); @@ -2526,7 +2411,7 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int // We should not interrupt persistent animations by non-persistent ones if (isPersistentAnimPlaying() && !persist) - return false; + return true; // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count @@ -2559,8 +2444,7 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int { clearAnimQueue(persist); - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); + clearStateAnimation(mCurrentIdle); mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); @@ -2587,18 +2471,18 @@ void CharacterController::skipAnim() mSkipAnim = true; } -bool CharacterController::isPersistentAnimPlaying() +bool CharacterController::isPersistentAnimPlaying() const { if (!mAnimQueue.empty()) { - AnimationQueueEntry& first = mAnimQueue.front(); + const AnimationQueueEntry& first = mAnimQueue.front(); return first.mPersist && isAnimPlaying(first.mGroup); } return false; } -bool CharacterController::isAnimPlaying(const std::string &groupName) +bool CharacterController::isAnimPlaying(const std::string &groupName) const { if(mAnimation == nullptr) return false; @@ -2611,9 +2495,15 @@ void CharacterController::clearAnimQueue(bool clearPersistAnims) if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); + if (clearPersistAnims) + { + mAnimQueue.clear(); + return; + } + for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { - if (clearPersistAnims || !it->mPersist) + if (!it->mPersist) it = mAnimQueue.erase(it); else ++it; @@ -2629,7 +2519,7 @@ void CharacterController::forceStateUpdate() // Make sure we canceled the current attack or spellcasting, // because we disabled attack animations anyway. mCastingManualSpell = false; - mAttackingOrSpell = false; + setAttackingOrSpell(false); if (mUpperBodyState != UpperCharState_Nothing) mUpperBodyState = UpperCharState_WeapEquiped; @@ -2648,11 +2538,7 @@ CharacterController::KillResult CharacterController::kill() if (mDeathState == CharState_None) { playRandomDeath(); - - mAnimation->disable(mCurrentIdle); - - mIdleState = CharState_None; - mCurrentIdle.clear(); + resetCurrentIdleState(); return Result_DeathAnimStarted; } @@ -2672,14 +2558,11 @@ void CharacterController::resurrect() if(mDeathState == CharState_None) return; - if(mAnimation) - mAnimation->disable(mCurrentDeath); - mCurrentDeath.clear(); - mDeathState = CharState_None; + resetCurrentDeathState(); mWeaponType = ESM::Weapon::None; } -void CharacterController::updateContinuousVfx() +void CharacterController::updateContinuousVfx() const { // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. @@ -2696,7 +2579,7 @@ void CharacterController::updateContinuousVfx() } } -void CharacterController::updateMagicEffects() +void CharacterController::updateMagicEffects() const { if (!mPtr.getClass().isActor()) return; @@ -2712,7 +2595,7 @@ void CharacterController::updateMagicEffects() mAnimation->setVampire(vampire); } -void CharacterController::setVisibility(float visibility) +void CharacterController::setVisibility(float visibility) const { // We should take actor's invisibility in account if (mPtr.getClass().isActor()) @@ -2728,7 +2611,7 @@ void CharacterController::setVisibility(float visibility) float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { - alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f)); + alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f); } visibility = std::min(visibility, alpha); @@ -2738,18 +2621,17 @@ void CharacterController::setVisibility(float visibility) mAnimation->setAlpha(visibility); } -void CharacterController::setAttackTypeBasedOnMovement() +std::string CharacterController::getMovementBasedAttackType() const { float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward - mAttackType = "thrust"; - else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway - mAttackType = "slash"; - else - mAttackType = "chop"; + return "thrust"; + if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway + return "slash"; + return "chop"; } -bool CharacterController::isRandomAttackAnimation(const std::string& group) const +bool CharacterController::isRandomAttackAnimation(std::string_view group) { return (group == "attack1" || group == "swimattack1" || group == "attack2" || group == "swimattack2" || @@ -2825,14 +2707,14 @@ bool CharacterController::isRunning() const mMovementState == CharState_SwimRunRight; } -void CharacterController::setAttackingOrSpell(bool attackingOrSpell) +void CharacterController::setAttackingOrSpell(bool attackingOrSpell) const { - mAttackingOrSpell = attackingOrSpell; + mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); } -void CharacterController::castSpell(const std::string spellId, bool manualSpell) +void CharacterController::castSpell(const std::string& spellId, bool manualSpell) { - mAttackingOrSpell = true; + setAttackingOrSpell(true); mCastingManualSpell = manualSpell; ActionSpell action = ActionSpell(spellId); action.prepare(mPtr); @@ -2843,15 +2725,15 @@ void CharacterController::setAIAttackType(const std::string& attackType) mAttackType = attackType; } -void CharacterController::setAttackTypeRandomly(std::string& attackType) +std::string CharacterController::getRandomAttackType() { - float random = Misc::Rng::rollProbability(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + float random = Misc::Rng::rollProbability(world->getPrng()); if (random >= 2/3.f) - attackType = "thrust"; - else if (random >= 1/3.f) - attackType = "slash"; - else - attackType = "chop"; + return "thrust"; + if (random >= 1/3.f) + return "slash"; + return "chop"; } bool CharacterController::readyToPrepareAttack() const @@ -2865,10 +2747,7 @@ bool CharacterController::readyToStartAttack() const if (mHitState != CharState_None && mHitState != CharState_Block) return false; - if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr)) - return mUpperBodyState == UpperCharState_WeapEquiped; - else - return mUpperBodyState == UpperCharState_Nothing; + return mUpperBodyState == UpperCharState_WeapEquiped; } float CharacterController::getAttackStrength() const @@ -2876,7 +2755,12 @@ float CharacterController::getAttackStrength() const return mAttackStrength; } -void CharacterController::setActive(int active) +bool CharacterController::getAttackingOrSpell() const +{ + return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell(); +} + +void CharacterController::setActive(int active) const { mAnimation->setActive(active); } @@ -2886,7 +2770,7 @@ void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target) mHeadTrackTarget = target; } -void CharacterController::playSwishSound(float attackStrength) +void CharacterController::playSwishSound(float attackStrength) const { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -2905,8 +2789,8 @@ void CharacterController::updateHeadTracking(float duration) if (!head) return; - float zAngleRadians = 0.f; - float xAngleRadians = 0.f; + double zAngleRadians = 0.f; + double xAngleRadians = 0.f; if (!mHeadTrackTarget.isEmpty()) { @@ -2930,7 +2814,7 @@ void CharacterController::updateHeadTracking(float duration) } else // no head node to look at, fall back to look at center of collision box - direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget); + direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false); } direction.normalize(); @@ -2939,15 +2823,16 @@ void CharacterController::updateHeadTracking(float duration) const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); + zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw(); + zAngleRadians *= (1 - direction.z() * direction.z()); xAngleRadians = std::asin(direction.z()); } const double xLimit = osg::DegreesToRadians(40.0); const double zLimit = osg::DegreesToRadians(30.0); double zLimitOffset = mAnimation->getUpperBodyYawRadians(); - xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -xLimit, xLimit); - zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), - -zLimit + zLimitOffset, zLimit + zLimitOffset); + xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit); + zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); float factor = duration*5; factor = std::min(factor, 1.f); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 949affcfde..2de6713396 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -3,8 +3,6 @@ #include -#include - #include "../mwworld/ptr.hpp" #include "../mwworld/containerstore.hpp" @@ -52,14 +50,6 @@ enum CharacterState { CharState_SpecialIdle, CharState_Idle, - CharState_Idle2, - CharState_Idle3, - CharState_Idle4, - CharState_Idle5, - CharState_Idle6, - CharState_Idle7, - CharState_Idle8, - CharState_Idle9, CharState_IdleSwim, CharState_IdleSneak, @@ -93,8 +83,6 @@ enum CharacterState { CharState_SwimTurnLeft, CharState_SwimTurnRight, - CharState_Jump, - CharState_Death1, CharState_Death2, CharState_Death3, @@ -151,77 +139,81 @@ class CharacterController : public MWRender::Animation::TextKeyListener typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; - CharacterState mIdleState; + CharacterState mIdleState{CharState_None}; std::string mCurrentIdle; - CharacterState mMovementState; + CharacterState mMovementState{CharState_None}; std::string mCurrentMovement; - float mMovementAnimSpeed; - bool mAdjustMovementAnimSpeed; - bool mHasMovedInXY; - bool mMovementAnimationControlled; + float mMovementAnimSpeed{0.f}; + bool mAdjustMovementAnimSpeed{false}; + bool mHasMovedInXY{false}; + bool mMovementAnimationControlled{true}; - CharacterState mDeathState; + CharacterState mDeathState{CharState_None}; std::string mCurrentDeath; - bool mFloatToSurface; + bool mFloatToSurface{true}; - CharacterState mHitState; + CharacterState mHitState{CharState_None}; std::string mCurrentHit; - UpperBodyCharacterState mUpperBodyState; + UpperBodyCharacterState mUpperBodyState{UpperCharState_Nothing}; - JumpingState mJumpState; + JumpingState mJumpState{JumpState_None}; std::string mCurrentJump; - int mWeaponType; + int mWeaponType{ESM::Weapon::None}; std::string mCurrentWeapon; - float mAttackStrength; + float mAttackStrength{0.f}; - bool mSkipAnim; + bool mSkipAnim{false}; // counted for skill increase - float mSecondsOfSwimming; - float mSecondsOfRunning; + float mSecondsOfSwimming{0.f}; + float mSecondsOfRunning{0.f}; MWWorld::ConstPtr mHeadTrackTarget; - float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning + float mTurnAnimationThreshold{0.f}; // how long to continue playing turning animation after actor stopped turning std::string mAttackType; // slash, chop or thrust - bool mAttackingOrSpell; - bool mCastingManualSpell; + bool mCastingManualSpell{false}; - float mTimeUntilWake; - - bool mIsMovingBackward; + bool mIsMovingBackward{false}; osg::Vec2f mSmoothedSpeed; - void setAttackTypeBasedOnMovement(); + std::string getMovementBasedAttackType() const; + + void clearStateAnimation(std::string &anim) const; + void resetCurrentJumpState(); + void resetCurrentMovementState(); + void resetCurrentIdleState(); + void resetCurrentHitState(); + void resetCurrentWeaponState(); + void resetCurrentDeathState(); void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false); - void refreshHitRecoilAnims(CharacterState& idle); - void refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force=false); - void refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force=false); - void refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force=false); + void refreshHitRecoilAnims(); + void refreshJumpAnims(JumpingState jump, bool force=false); + void refreshMovementAnims(CharacterState movement, bool force=false); + void refreshIdleAnims(CharacterState idle, bool force=false); void clearAnimQueue(bool clearPersistAnims = false); - bool updateWeaponState(CharacterState& idle); - bool updateCreatureState(); - void updateIdleStormState(bool inwater); + bool updateState(CharacterState idle); + void updateIdleStormState(bool inwater) const; std::string chooseRandomAttackAnimation() const; - bool isRandomAttackAnimation(const std::string& group) const; + static bool isRandomAttackAnimation(std::string_view group); - bool isPersistentAnimPlaying(); + bool isPersistentAnimPlaying() const; void updateAnimQueue(); void updateHeadTracking(float duration); - void updateMagicEffects(); + void updateMagicEffects() const; void playDeath(float startpoint, CharacterState death); CharacterState chooseRandomDeathState() const; @@ -233,32 +225,41 @@ class CharacterController : public MWRender::Animation::TextKeyListener bool updateCarriedLeftVisible(int weaptype) const; - std::string fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr); + std::string fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr) const; std::string getWeaponAnimation(int weaponType) const; + std::string getWeaponShortGroup(int weaponType) const; + + bool getAttackingOrSpell() const; + void setAttackingOrSpell(bool attackingOrSpell) const; public: CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim); virtual ~CharacterController(); - void handleTextKey(const std::string &groupname, NifOsg::TextKeyMap::ConstIterator key, const NifOsg::TextKeyMap& map) override; + CharacterController(const CharacterController&) = delete; + CharacterController(CharacterController&&) = delete; + + const MWWorld::Ptr& getPtr() const { return mPtr; } + + void handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) override; // Be careful when to call this, see comment in Actors - void updateContinuousVfx(); + void updateContinuousVfx() const; void updatePtr(const MWWorld::Ptr &ptr); - void update(float duration, bool animationOnly=false); + void update(float duration); - bool onOpen(); - void onClose(); + bool onOpen() const; + void onClose() const; - void persistAnimationState(); + void persistAnimationState() const; void unpersistAnimationState(); bool playGroup(const std::string &groupname, int mode, int count, bool persist=false); void skipAnim(); - bool isAnimPlaying(const std::string &groupName); + bool isAnimPlaying(const std::string &groupName) const; enum KillResult { @@ -286,11 +287,10 @@ public: bool isTurning() const; bool isAttackingOrSpell() const; - void setVisibility(float visibility); - void setAttackingOrSpell(bool attackingOrSpell); - void castSpell(const std::string spellId, bool manualSpell=false); + void setVisibility(float visibility) const; + void castSpell(const std::string& spellId, bool manualSpell=false); void setAIAttackType(const std::string& attackType); - static void setAttackTypeRandomly(std::string& attackType); + static std::string getRandomAttackType(); bool readyToPrepareAttack() const; bool readyToStartAttack() const; @@ -298,12 +298,12 @@ public: float getAttackStrength() const; /// @see Animation::setActive - void setActive(int active); + void setActive(int active) const; /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. void setHeadTrackTarget(const MWWorld::ConstPtr& target); - void playSwishSound(float attackStrength); + void playSwishSound(float attackStrength) const; }; } diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 183845b8c1..e18fb892dc 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -1,3 +1,4 @@ + #include "combat.hpp" #include @@ -47,7 +48,10 @@ namespace MWMechanics { MWMechanics::CastSpell cast(attacker, victim, fromProjectile); cast.mHitPosition = hitPosition; - cast.cast(object, false); + cast.cast(object, 0, false); + // Apply magic effects directly instead of waiting a frame to allow soul trap to work on one-hit kills + if(!victim.isEmpty() && victim.getClass().isActor()) + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(victim); return true; } } @@ -71,7 +75,7 @@ namespace MWMechanics MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) return false; if (!blocker.getRefData().getBaseNode()) @@ -84,9 +88,11 @@ namespace MWMechanics osg::Vec3f(0,0,1))); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - if (angleDegrees < gmst.find("fCombatBlockLeftAngle")->mValue.getFloat()) + static const float fCombatBlockLeftAngle = gmst.find("fCombatBlockLeftAngle")->mValue.getFloat(); + if (angleDegrees < fCombatBlockLeftAngle) return false; - if (angleDegrees > gmst.find("fCombatBlockRightAngle")->mValue.getFloat()) + static const float fCombatBlockRightAngle = gmst.find("fCombatBlockRightAngle")->mValue.getFloat(); + if (angleDegrees > fCombatBlockRightAngle) return false; MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); @@ -94,11 +100,16 @@ namespace MWMechanics float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + 0.2f * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() + 0.1f * blockerStats.getAttribute(ESM::Attribute::Luck).getModified(); float enemySwing = attackStrength; - float swingTerm = enemySwing * gmst.find("fSwingBlockMult")->mValue.getFloat() + gmst.find("fSwingBlockBase")->mValue.getFloat(); + static const float fSwingBlockMult = gmst.find("fSwingBlockMult")->mValue.getFloat(); + static const float fSwingBlockBase = gmst.find("fSwingBlockBase")->mValue.getFloat(); + float swingTerm = enemySwing * fSwingBlockMult + fSwingBlockBase; float blockerTerm = blockTerm * swingTerm; if (blocker.getClass().getMovementSettings(blocker).mPosition[1] <= 0) - blockerTerm *= gmst.find("fBlockStillBonus")->mValue.getFloat(); + { + static const float fBlockStillBonus = gmst.find("fBlockStillBonus")->mValue.getFloat(); + blockerTerm *= fBlockStillBonus; + } blockerTerm *= blockerStats.getFatigueTerm(); float attackerSkill = 0; @@ -110,12 +121,12 @@ namespace MWMechanics + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); - int x = int(blockerTerm - attackerTerm); - int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); - int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); - x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + static const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); + static const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); + int x = std::clamp(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance); - if (Misc::Rng::roll0to99() < x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) < x) { // Reduce shield durability by incoming damage int shieldhealth = shield->getClass().getItemHealth(*shield); @@ -125,9 +136,9 @@ namespace MWMechanics if (shieldhealth == 0) inv.unequipItem(*shield, blocker); // Reduce blocker fatigue - const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->mValue.getFloat(); - const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->mValue.getFloat(); - const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->mValue.getFloat(); + static const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->mValue.getFloat(); + static const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->mValue.getFloat(); + static const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->mValue.getFloat(); MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); float normalizedEncumbrance = blocker.getClass().getNormalizedEncumbrance(blocker); normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); @@ -198,19 +209,19 @@ namespace MWMechanics bool validVictim = !victim.isEmpty() && victim.getClass().isActor(); + int weaponSkill = ESM::Skill::Marksman; + if (!weapon.isEmpty()) + weaponSkill = weapon.getClass().getEquipmentSkill(weapon); + float damage = 0.f; if (validVictim) { if (attacker == getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); - int weaponSkill = ESM::Skill::Marksman; - if (!weapon.isEmpty()) - weaponSkill = weapon.getClass().getEquipmentSkill(weapon); + int skillValue = attacker.getClass().getSkill(attacker, weaponSkill); - int skillValue = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); - - if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue)) + if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); @@ -226,6 +237,12 @@ namespace MWMechanics damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); adjustWeaponDamage(damage, weapon, attacker); + } + + reduceWeaponCondition(damage, validVictim, weapon, attacker); + + if (validVictim) + { if (weapon == projectile || Settings::Manager::getBool("only appropriate ammunition bypasses resistance", "Game") || isNormalWeapon(weapon)) resistNormalWeapon(victim, attacker, projectile, damage); applyWerewolfDamageMult(victim, projectile, damage); @@ -239,14 +256,13 @@ namespace MWMechanics bool knockedDown = victim.getClass().getCreatureStats(victim).getKnockedDown(); if (knockedDown || unaware) { - damage *= gmst.find("fCombatKODamageMult")->mValue.getFloat(); + static const float fCombatKODamageMult = gmst.find("fCombatKODamageMult")->mValue.getFloat(); + damage *= fCombatKODamageMult; if (!knockedDown) MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); } } - reduceWeaponCondition(damage, validVictim, weapon, attacker); - // Apply "On hit" effect of the projectile bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); @@ -255,8 +271,8 @@ namespace MWMechanics // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory if (victim != getPlayer() && !appliedEnchantment) { - float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->mValue.getFloat(); - if (Misc::Rng::rollProbability() < fProjectileThrownStoreChance / 100.f) + static const float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->mValue.getFloat(); + if (Misc::Rng::rollProbability(world->getPrng()) < fProjectileThrownStoreChance / 100.f) victim.getClass().getContainerStore(victim).add(projectile, 1, victim); } @@ -286,11 +302,12 @@ namespace MWMechanics { defenseTerm = victimStats.getEvasion(); } + static const float fCombatInvisoMult = gmst.find("fCombatInvisoMult")->mValue.getFloat(); defenseTerm += std::min(100.f, - gmst.find("fCombatInvisoMult")->mValue.getFloat() * + fCombatInvisoMult * victimStats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude()); defenseTerm += std::min(100.f, - gmst.find("fCombatInvisoMult")->mValue.getFloat() * + fCombatInvisoMult * victimStats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude()); } float attackTerm = skillValue + @@ -305,6 +322,11 @@ namespace MWMechanics void applyElementalShields(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim) { + // Don't let elemental shields harm the player in god mode. + bool godmode = attacker == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + if (godmode) + return; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); for (int i=0; i<3; ++i) { float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude(); @@ -324,7 +346,7 @@ namespace MWMechanics saveTerm *= 1.25f * normalisedFatigue; - float x = std::max(0.f, saveTerm - Misc::Rng::roll0to99()); + float x = std::max(0.f, saveTerm - Misc::Rng::roll0to99(prng)); int element = ESM::MagicEffect::FireDamage; if (i == 1) @@ -345,6 +367,8 @@ namespace MWMechanics MWMechanics::DynamicStat health = attackerStats.getHealth(); health.setCurrent(health.getCurrent() - x); attackerStats.setHealth(health); + + MWBase::Environment::get().getSoundManager()->playSound3D(attacker, "Health Damage", 1.0f, 1.0f); } } @@ -401,8 +425,8 @@ namespace MWMechanics void getHandToHandDamage(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, float &damage, bool &healthdmg, float attackStrength) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - float minstrike = store.get().find("fMinHandToHandMult")->mValue.getFloat(); - float maxstrike = store.get().find("fMaxHandToHandMult")->mValue.getFloat(); + static const float minstrike = store.get().find("fMinHandToHandMult")->mValue.getFloat(); + static const float maxstrike = store.get().find("fMaxHandToHandMult")->mValue.getFloat(); damage = static_cast(attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand)); damage *= minstrike + ((maxstrike-minstrike)*attackStrength); @@ -426,13 +450,17 @@ namespace MWMechanics // GLOB instead of GMST because it gets updated during a quest damage *= MWBase::Environment::get().getWorld()->getGlobalFloat("werewolfclawmult"); } - if(healthdmg) - damage *= store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); + if (healthdmg) + { + static const float fHandtoHandHealthPer = store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); + damage *= fHandtoHandHealthPer; + } MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(isWerewolf) { - const ESM::Sound *sound = store.get().searchRandom("WolfHit"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfHit", prng); if(sound) sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); } @@ -444,9 +472,9 @@ namespace MWMechanics { // somewhat of a guess, but using the weapon weight makes sense const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); - const float fFatigueAttackBase = store.find("fFatigueAttackBase")->mValue.getFloat(); - const float fFatigueAttackMult = store.find("fFatigueAttackMult")->mValue.getFloat(); - const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->mValue.getFloat(); + static const float fFatigueAttackBase = store.find("fFatigueAttackBase")->mValue.getFloat(); + static const float fFatigueAttackMult = store.find("fFatigueAttackMult")->mValue.getFloat(); + static const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->mValue.getFloat(); CreatureStats& stats = attacker.getClass().getCreatureStats(attacker); MWMechanics::DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = attacker.getClass().getNormalizedEncumbrance(attacker); diff --git a/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp b/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp new file mode 100644 index 0000000000..667a0b9c45 --- /dev/null +++ b/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp @@ -0,0 +1,20 @@ +#ifndef OPENMW_MWMECHANICS_CREATURECUSTOMDATARESETTER_H +#define OPENMW_MWMECHANICS_CREATURECUSTOMDATARESETTER_H + +#include "../mwworld/ptr.hpp" + +namespace MWMechanics +{ + struct CreatureCustomDataResetter + { + MWWorld::Ptr mPtr; + + ~CreatureCustomDataResetter() + { + if (!mPtr.isEmpty()) + mPtr.getRefData().setCustomData({}); + } + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 1d5fe8347e..ae4ebbce49 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -1,32 +1,32 @@ #include "creaturestats.hpp" #include +#include -#include -#include -#include +#include +#include +#include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/mechanicsmanager.hpp" namespace MWMechanics { int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() - : mDrawState (DrawState_Nothing), mDead (false), mDeathAnimationFinished(false), mDied (false), mMurdered(false), mFriendlyHits (0), + : 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), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), + mFallHeight(0), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) + , mAttackingOrSpell(false) { - for (int i=0; i<4; ++i) - mAiSettings[i] = 0; } const AiSequence& CreatureStats::getAiSequence() const @@ -44,7 +44,7 @@ namespace MWMechanics float max = getFatigue().getModified(); float current = getFatigue().getCurrent(); - float normalised = floor(max) == 0 ? 1 : std::max (0.0f, current / max); + float normalised = std::floor(max) == 0 ? 1 : std::max (0.0f, current / max); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -100,7 +100,7 @@ namespace MWMechanics Stat CreatureStats::getAiSetting (AiSetting index) const { - return mAiSettings[index]; + return mAiSettings[static_cast>(index)]; } const DynamicStat &CreatureStats::getDynamic(int index) const @@ -146,7 +146,7 @@ namespace MWMechanics mAttributes[index] = value; if (index == ESM::Attribute::Intelligence) - mRecalcMagicka = true; + recalculateMagicka(); else if (index == ESM::Attribute::Strength || index == ESM::Attribute::Willpower || index == ESM::Attribute::Agility || @@ -157,10 +157,9 @@ namespace MWMechanics float agility = getAttribute(ESM::Attribute::Agility).getModified(); float endurance = getAttribute(ESM::Attribute::Endurance).getModified(); DynamicStat fatigue = getFatigue(); - float diff = (strength+willpower+agility+endurance) - fatigue.getBase(); float currentToBaseRatio = fatigue.getBase() > 0 ? (fatigue.getCurrent() / fatigue.getBase()) : 0; - fatigue.setModified(fatigue.getModified() + diff, 0); - fatigue.setCurrent(fatigue.getBase() * currentToBaseRatio); + fatigue.setBase(std::max(0.f, strength + willpower + agility + endurance)); + fatigue.setCurrent(fatigue.getBase() * currentToBaseRatio, false, true); setFatigue(fatigue); } } @@ -195,8 +194,6 @@ namespace MWMechanics mDead = true; - mDynamic[index].setModifier(0); - mDynamic[index].setCurrentModifier(0); mDynamic[index].setCurrent(0); } } @@ -208,16 +205,15 @@ namespace MWMechanics void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcMagicka = true; - + bool recalc = effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier(); mMagicEffects.setModifiers(effects); + if(recalc) + recalculateMagicka(); } void CreatureStats::setAiSetting (AiSetting index, Stat value) { - mAiSettings[index] = value; + mAiSettings[static_cast>(index)] = value; } void CreatureStats::setAiSetting (AiSetting index, int base) @@ -281,10 +277,7 @@ namespace MWMechanics { if (mDead) { - if (mDynamic[0].getModified() < 1) - mDynamic[0].setModified(1, 0); - - mDynamic[0].setCurrent(mDynamic[0].getModified()); + mDynamic[0].setCurrent(mDynamic[0].getBase()); mDead = false; mDeathAnimationFinished = false; } @@ -355,6 +348,11 @@ namespace MWMechanics mLastHitObject = objectid; } + void CreatureStats::clearLastHitObject() + { + mLastHitObject.clear(); + } + const std::string &CreatureStats::getLastHitObject() const { return mLastHitObject; @@ -365,6 +363,11 @@ namespace MWMechanics mLastHitAttemptObject = objectid; } + void CreatureStats::clearLastHitAttemptObject() + { + mLastHitAttemptObject.clear(); + } + const std::string &CreatureStats::getLastHitAttemptObject() const { return mLastHitAttemptObject; @@ -400,19 +403,25 @@ namespace MWMechanics return height; } - bool CreatureStats::needToRecalcDynamicStats() + void CreatureStats::recalculateMagicka() { - if (mRecalcMagicka) - { - mRecalcMagicka = false; - return true; - } - return false; - } + auto world = MWBase::Environment::get().getWorld(); + float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); - void CreatureStats::setNeedRecalcDynamicStats(bool val) - { - mRecalcMagicka = val; + float base = 1.f; + const auto& player = world->getPlayerPtr(); + if (this == &player.getClass().getCreatureStats(player)) + base = world->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); + else + base = world->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + + double magickaFactor = base + mMagicEffects.get(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + + DynamicStat magicka = getMagicka(); + float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; + magicka.setBase(magickaFactor * intelligence); + magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); + setMagicka(magicka); } void CreatureStats::setKnockedDown(bool value) @@ -490,12 +499,12 @@ namespace MWMechanics } } - DrawState_ CreatureStats::getDrawState() const + DrawState CreatureStats::getDrawState() const { return mDrawState; } - void CreatureStats::setDrawState(DrawState_ state) + void CreatureStats::setDrawState(DrawState state) { mDrawState = state; } @@ -532,8 +541,8 @@ namespace MWMechanics state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; - state.mRecalcDynamicStats = mRecalcMagicka; - state.mDrawState = mDrawState; + state.mRecalcDynamicStats = false; + state.mDrawState = static_cast(mDrawState); state.mLevel = mLevel; state.mActorId = mActorId; state.mDeathAnimation = mDeathAnimation; @@ -545,40 +554,38 @@ namespace MWMechanics mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); - state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; for (int i=0; i<4; ++i) mAiSettings[i].writeState (state.mAiSettings[i]); - for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; - - state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); - } + state.mMissingACDT = false; } void CreatureStats::readState (const ESM::CreatureStats& state) { - for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; - - mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - } + if(state.mRecalcDynamicStats) + recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -683,7 +682,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -692,23 +691,4 @@ namespace MWMechanics { return mSummonGraveyard; } - - std::map &CreatureStats::getCorprusSpells() - { - return mCorprusSpells; - } - - void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) - { - mCorprusSpells[sourceId] = stats; - } - - void CreatureStats::removeCorprusSpell(const std::string& sourceId) - { - auto corprusIt = mCorprusSpells.find(sourceId); - if (corprusIt != mCorprusSpells.end()) - { - mCorprusSpells.erase(corprusIt); - } - } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index b2c0aec98e..a2370015e1 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H +#include #include #include #include @@ -11,9 +12,10 @@ #include "activespells.hpp" #include "aisequence.hpp" #include "drawstate.hpp" +#include "aisetting.hpp" #include -#include +#include namespace ESM { @@ -24,7 +26,7 @@ namespace MWMechanics { struct CorprusStats { - static const int sWorseningPeriod = 24; + static constexpr int sWorseningPeriod = 24; int mWorsenings[ESM::Attribute::Length]; MWWorld::TimeStamp mNextWorsening; @@ -36,7 +38,7 @@ namespace MWMechanics class CreatureStats { static int sActorId; - DrawState_ mDrawState; + DrawState mDrawState; AttributeValue mAttributes[ESM::Attribute::Length]; DynamicStat mDynamic[3]; // health, magicka, fatigue Spells mSpells; @@ -64,8 +66,6 @@ namespace MWMechanics std::string mLastHitObject; // The last object to hit this actor std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - bool mRecalcMagicka; - // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -85,25 +85,23 @@ namespace MWMechanics float mSideMovementAngle; private: - std::map mSummonedCreatures; // + std::multimap mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; - std::map mCorprusSpells; - protected: int mLevel; + bool mAttackingOrSpell; public: CreatureStats(); - DrawState_ getDrawState() const; - void setDrawState(DrawState_ state); + DrawState getDrawState() const; + void setDrawState(DrawState state); - bool needToRecalcDynamicStats(); - void setNeedRecalcDynamicStats(bool val); + void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); @@ -128,7 +126,7 @@ namespace MWMechanics const MagicEffects & getMagicEffects() const; - bool getAttackingOrSpell() const; + bool getAttackingOrSpell() const { return mAttackingOrSpell; } int getLevel() const; @@ -153,17 +151,10 @@ namespace MWMechanics /// Set Modifier for each magic effect according to \a effects. Does not touch Base values. void modifyMagicEffects(const MagicEffects &effects); - void setAttackingOrSpell(bool attackingOrSpell); + void setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } void setLevel(int level); - enum AiSetting - { - AI_Hello = 0, - AI_Fight = 1, - AI_Flee = 2, - AI_Alarm = 3 - }; void setAiSetting (AiSetting index, Stat value); void setAiSetting (AiSetting index, int base); Stat getAiSetting (AiSetting index) const; @@ -234,7 +225,7 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag @@ -258,16 +249,14 @@ namespace MWMechanics bool getStance (Stance flag) const; void setLastHitObject(const std::string &objectid); + void clearLastHitObject(); const std::string &getLastHitObject() const; void setLastHitAttemptObject(const std::string &objectid); + void clearLastHitAttemptObject(); const std::string &getLastHitAttemptObject() const; void setHitAttemptActorId(const int actorId); int getHitAttemptActorId() const; - // Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves. - // TODO: Put it somewhere else? - std::set mBoundItems; - void writeState (ESM::CreatureStats& state) const; void readState (const ESM::CreatureStats& state); @@ -295,12 +284,6 @@ namespace MWMechanics static void cleanup(); - std::map & getCorprusSpells(); - - void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); - - void removeCorprusSpell(const std::string& sourceId); - float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp index 2376989745..e973e0ed52 100644 --- a/apps/openmw/mwmechanics/difficultyscaling.cpp +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr const MWWorld::Ptr& player = MWMechanics::getPlayer(); // [-500, 500] - int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); - difficultySetting = std::min(difficultySetting, 500); - difficultySetting = std::max(difficultySetting, -500); + const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500); static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 7933c927e5..82c5236ca4 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -21,7 +21,7 @@ namespace MWMechanics /// Call when \a actor has got in contact with \a carrier (e.g. hit by him, or loots him) /// @param actor The actor that will potentially catch diseases. Currently only the player can catch diseases. /// @param carrier The disease carrier. - inline void diseaseContact (MWWorld::Ptr actor, MWWorld::Ptr carrier) + inline void diseaseContact (const MWWorld::Ptr& actor, const MWWorld::Ptr& carrier) { if (!carrier.getClass().isActor() || actor != getPlayer()) return; @@ -30,12 +30,11 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->mValue.getFloat(); - MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; @@ -53,10 +52,11 @@ namespace MWMechanics continue; int x = static_cast(fDiseaseXferChance * 100 * resist); - if (Misc::Rng::rollDice(10000) < x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::rollDice(10000, prng) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); std::string msg = "sMagicContractDisease"; diff --git a/apps/openmw/mwmechanics/drawstate.hpp b/apps/openmw/mwmechanics/drawstate.hpp index 7f59d8d782..1373d59ef8 100644 --- a/apps/openmw/mwmechanics/drawstate.hpp +++ b/apps/openmw/mwmechanics/drawstate.hpp @@ -3,12 +3,12 @@ namespace MWMechanics { - /// \note The _ suffix is required to avoid a collision with a Windoze macro. Die, Microsoft! Die! - enum DrawState_ + + enum class DrawState { - DrawState_Nothing = 0, - DrawState_Weapon = 1, - DrawState_Spell = 2 + Nothing = 0, + Weapon = 1, + Spell = 2 }; } diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 1717ba06fe..7ed58f0d25 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -22,6 +22,7 @@ namespace MWMechanics Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) + , mObjectType(0) , mWeaponType(-1) {} @@ -29,11 +30,11 @@ namespace MWMechanics { mOldItemPtr=oldItem; mWeaponType = -1; - mObjectType.clear(); + mObjectType = 0; if(!itemEmpty()) { - mObjectType = mOldItemPtr.getTypeName(); - if (mObjectType == typeid(ESM::Weapon).name()) + mObjectType = mOldItemPtr.getType(); + if (mObjectType == ESM::Weapon::sRecordId) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } @@ -75,7 +76,8 @@ namespace MWMechanics if(mSelfEnchanting) { - if(getEnchantChance() <= (Misc::Rng::roll0to99())) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if(getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; mEnchanter.getClass().skillUsageSucceeded (mEnchanter, ESM::Skill::Enchant, 2); @@ -115,7 +117,7 @@ namespace MWMechanics const bool powerfulSoul = getGemCharge() >= \ MWBase::Environment::get().getWorld()->getStore().get().find ("iSoulAmountForConstantEffect")->mValue.getInteger(); - if ((mObjectType == typeid(ESM::Armor).name()) || (mObjectType == typeid(ESM::Clothing).name())) + if ((mObjectType == ESM::Armor::sRecordId) || (mObjectType == ESM::Clothing::sRecordId)) { // Armor or Clothing switch(mCastStyle) { @@ -150,7 +152,7 @@ namespace MWMechanics return; } } - else if(mObjectType == typeid(ESM::Book).name()) + else if(mObjectType == ESM::Book::sRecordId) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; @@ -355,10 +357,10 @@ namespace MWMechanics ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { - static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game"))); + static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f); MWWorld::Ptr player = getPlayer(); - int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); - count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); + count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); + count = std::clamp(getGemCharge() * multiplier / enchantPoints, 1, count); } } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 33a2820938..5e1a6fa239 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -3,8 +3,8 @@ #include -#include -#include +#include +#include #include "../mwworld/ptr.hpp" @@ -23,7 +23,7 @@ namespace MWMechanics ESM::EffectList mEffectList; std::string mNewItemName; - std::string mObjectType; + unsigned int mObjectType; int mWeaponType; const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const; diff --git a/apps/openmw/mwmechanics/greetingstate.hpp b/apps/openmw/mwmechanics/greetingstate.hpp new file mode 100644 index 0000000000..9b37096322 --- /dev/null +++ b/apps/openmw/mwmechanics/greetingstate.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_MWMECHANICS_GREETINGSTATE_H +#define OPENMW_MWMECHANICS_GREETINGSTATE_H + +namespace MWMechanics +{ + enum GreetingState + { + Greet_None, + Greet_InProgress, + Greet_Done + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/inventory.hpp b/apps/openmw/mwmechanics/inventory.hpp new file mode 100644 index 0000000000..ddd4b3351a --- /dev/null +++ b/apps/openmw/mwmechanics/inventory.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_MWMECHANICS_INVENTORY_H +#define OPENMW_MWMECHANICS_INVENTORY_H + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/esmstore.hpp" + +#include +#include + +#include +#include + +namespace MWMechanics +{ + template + void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) + { + T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); + for (auto& it : copy.mInventory.mList) + { + if (Misc::StringUtils::ciEqual(it.mItem, itemId)) + { + const int sign = it.mCount < 1 ? -1 : 1; + it.mCount = sign * std::max(it.mCount * sign + amount, 0); + MWBase::Environment::get().getWorld()->createOverrideRecord(copy); + return; + } + } + if (amount > 0) + { + ESM::ContItem cont; + cont.mItem = itemId; + cont.mCount = amount; + copy.mInventory.mList.push_back(cont); + MWBase::Environment::get().getWorld()->createOverrideRecord(copy); + } + } +} + +#endif diff --git a/apps/openmw/mwmechanics/levelledlist.hpp b/apps/openmw/mwmechanics/levelledlist.hpp index c8368101a7..be5c5962bb 100644 --- a/apps/openmw/mwmechanics/levelledlist.hpp +++ b/apps/openmw/mwmechanics/levelledlist.hpp @@ -19,14 +19,14 @@ namespace MWMechanics { /// @return ID of resulting item, or empty if none - inline std::string getLevelledItem (const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Seed& seed = Misc::Rng::getSeed()) + inline std::string getLevelledItem (const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Generator& prng) { const std::vector& items = levItem->mList; const MWWorld::Ptr& player = getPlayer(); int playerLevel = player.getClass().getCreatureStats(player).getLevel(); - if (Misc::Rng::roll0to99(seed) < levItem->mChanceNone) + if (Misc::Rng::roll0to99(prng) < levItem->mChanceNone) return std::string(); std::vector candidates; @@ -55,7 +55,7 @@ namespace MWMechanics } if (candidates.empty()) return std::string(); - std::string item = candidates[Misc::Rng::rollDice(candidates.size(), seed)]; + std::string item = candidates[Misc::Rng::rollDice(candidates.size(), prng)]; // Vanilla doesn't fail on nonexistent items in levelled lists if (!MWBase::Environment::get().getWorld()->getStore().find(Misc::StringUtils::lowerCase(item))) @@ -66,17 +66,17 @@ namespace MWMechanics // Is this another levelled item or a real item? MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), item, 1); - if (ref.getPtr().getTypeName() != typeid(ESM::ItemLevList).name() - && ref.getPtr().getTypeName() != typeid(ESM::CreatureLevList).name()) + if (ref.getPtr().getType() != ESM::ItemLevList::sRecordId + && ref.getPtr().getType() != ESM::CreatureLevList::sRecordId) { return item; } else { - if (ref.getPtr().getTypeName() == typeid(ESM::ItemLevList).name()) - return getLevelledItem(ref.getPtr().get()->mBase, false, seed); + if (ref.getPtr().getType() == ESM::ItemLevList::sRecordId) + return getLevelledItem(ref.getPtr().get()->mBase, false, prng); else - return getLevelledItem(ref.getPtr().get()->mBase, true, seed); + return getLevelledItem(ref.getPtr().get()->mBase, true, prng); } } diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp deleted file mode 100644 index b0defac7d2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "linkedeffects.hpp" - -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } - - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) - { - if (caster.isEmpty() || caster == target) - return; - - if (!target.getClass().isActor() || !caster.getClass().isActor()) - return; - - // Make sure callers don't do something weird - if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) - throw std::runtime_error("invalid absorb stat effect"); - - if (appliedEffect.mMagnitude == 0) - return; - - std::vector absorbEffects; - ActiveSpells::ActiveEffect absorbEffect = appliedEffect; - absorbEffect.mMagnitude *= -1; - absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; - absorbEffects.emplace_back(absorbEffect); - - // Morrowind negates reflected Absorb spells so the original caster won't be harmed. - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - { - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); - return; - } - - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); - } -} diff --git a/apps/openmw/mwmechanics/linkedeffects.hpp b/apps/openmw/mwmechanics/linkedeffects.hpp deleted file mode 100644 index a6dea2a3a2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MWMECHANICS_LINKEDEFFECTS_H -#define MWMECHANICS_LINKEDEFFECTS_H - -#include - -namespace ESM -{ - struct ActiveEffect; - struct EffectList; - struct ENAMstruct; - struct MagicEffect; - struct Spell; -} - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - - // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); - - // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); -} - -#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 1a1a44f638..d4b89c56d0 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -1,9 +1,19 @@ #include "magiceffects.hpp" +#include #include -#include -#include +#include +#include + +namespace +{ + // Round value to prevent precision issues + void truncate(float& value) + { + value = static_cast(value * 1024.f) / 1024.f; + } +} namespace MWMechanics { @@ -74,6 +84,7 @@ namespace MWMechanics { mModifier += param.mModifier; mBase += param.mBase; + truncate(mModifier); return *this; } @@ -81,6 +92,7 @@ namespace MWMechanics { mModifier -= param.mModifier; mBase -= param.mBase; + truncate(mModifier); return *this; } @@ -121,28 +133,6 @@ namespace MWMechanics } } - MagicEffects& MagicEffects::operator+= (const MagicEffects& effects) - { - if (this==&effects) - { - MagicEffects temp (effects); - *this += temp; - return *this; - } - - for (Collection::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) - { - Collection::iterator result = mCollection.find (iter->first); - - if (result!=mCollection.end()) - result->second += iter->second; - else - mCollection.insert (*iter); - } - - return *this; - } - EffectParam MagicEffects::get (const EffectKey& key) const { Collection::const_iterator iter = mCollection.find (key); @@ -193,22 +183,22 @@ namespace MWMechanics void MagicEffects::writeState(ESM::MagicEffects &state) const { - // Don't need to save Modifiers, they are recalculated every frame anyway. - for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + for (const auto& [key, params] : mCollection) { - if (iter->second.getBase() != 0) + if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions - state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + state.mEffects[key.mId] = {params.getBase(), params.getModifier()}; } } } void MagicEffects::readState(const ESM::MagicEffects &state) { - for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + for (const auto& [key, params] : state.mEffects) { - mCollection[EffectKey(it->first)].setBase(it->second); + mCollection[EffectKey(key)].setBase(params.first); + mCollection[EffectKey(key)].setModifier(params.second); } } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 12735a87fc..e8175f6a78 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -69,16 +69,6 @@ namespace MWMechanics return param -= right; } - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual ~EffectSourceVisitor() { } - - virtual void visit (EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) = 0; - }; - /// \brief Effects currently affecting a NPC or creature class MagicEffects { @@ -107,8 +97,6 @@ namespace MWMechanics /// Copy Modifier values from \a effects, but keep original mBase values. void setModifiers(const MagicEffects& effects); - MagicEffects& operator+= (const MagicEffects& effects); - EffectParam get (const EffectKey& key) const; ///< This function can safely be used for keys that are not present. diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index b1db2562b2..1f953898c8 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -4,8 +4,8 @@ #include -#include -#include +#include +#include #include @@ -30,6 +30,7 @@ #include "npcstats.hpp" #include "actorutil.hpp" #include "combat.hpp" +#include "actor.hpp" namespace { @@ -66,6 +67,35 @@ namespace } } + bool isOwned(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) + { + const MWWorld::CellRef& cellref = target.getCellRef(); + + const std::string& owner = cellref.getOwner(); + bool isOwned = !owner.empty() && owner != "player"; + + const std::string& faction = cellref.getFaction(); + bool isFactionOwned = false; + if (!faction.empty() && ptr.getClass().isNpc()) + { + const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); + auto found = factions.find(Misc::StringUtils::lowerCase(faction)); + if (found == factions.end() || found->second < cellref.getFactionRank()) + isFactionOwned = true; + } + + const std::string& globalVariable = cellref.getGlobalVariable(); + if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(globalVariable)) + { + isOwned = false; + isFactionOwned = false; + } + + if (!cellref.getOwner().empty()) + victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false); + + return isOwned || isFactionOwned; + } } namespace MWMechanics @@ -77,14 +107,14 @@ namespace MWMechanics MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); - npcStats.setNeedRecalcDynamicStats(true); + npcStats.recalculateMagicka(); const ESM::NPC *player = ptr.get()->mBase; // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); - creatureStats.modifyMagicEffects(MagicEffects()); + creatureStats.getActiveSpells().clear(ptr); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); @@ -213,6 +243,7 @@ namespace MWMechanics int attributes[ESM::Attribute::Length]; for (int i=0; i selectedSpells = autoCalcPlayerSpells(skills, attributes, race); @@ -251,17 +282,17 @@ namespace MWMechanics mObjects.addObject(ptr); } - void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const std::string& spellId, bool manualSpell) { if(ptr.getClass().isActor()) mActors.castSpell(ptr, spellId, manualSpell); } - void MechanicsManager::remove(const MWWorld::Ptr& ptr) + void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) { if(ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); - mActors.removeActor(ptr); + mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } @@ -282,37 +313,14 @@ namespace MWMechanics mObjects.dropObjects(cellStore); } - void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) - { - auto& stats = actor.getClass().getCreatureStats (actor); - auto& corprusSpells = stats.getCorprusSpells(); - - auto corprusIt = corprusSpells.find(sourceId); - - if (corprusIt != corprusSpells.end()) - { - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - MWMechanics::AttributeValue attr = stats.getAttribute(i); - attr.restore(corprusIt->second.mWorsenings[i]); - actor.getClass().getCreatureStats(actor).setAttribute(i, attr); - } - } - } - void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values MWWorld::Ptr ptr = getPlayer(); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); - // Update the equipped weapon icon MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end()) - winMgr->unsetSelectedWeapon(); - else - winMgr->setSelectedWeapon(*weapon); // Update the selected spell icon MWWorld::ContainerStoreIterator enchantItem = inv.getSelectedEnchantItem(); @@ -327,13 +335,19 @@ namespace MWMechanics winMgr->unsetSelectedSpell(); } + // Update the equipped weapon icon + if (weapon == inv.end()) + winMgr->unsetSelectedWeapon(); + else + winMgr->setSelectedWeapon(*weapon); + if (mUpdatePlayer) { mUpdatePlayer = false; // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. - mActors.removeActor(ptr); + mActors.removeActor(ptr, true); mActors.addActor(ptr, true); } @@ -402,7 +416,7 @@ namespace MWMechanics mActors.rest(hours, sleep); } - void MechanicsManager::restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) + void MechanicsManager::restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) { mActors.restoreDynamicStats(actor, hours, sleep); } @@ -483,7 +497,7 @@ namespace MWMechanics mUpdatePlayer = true; } - int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange) + int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); float x = static_cast(npcSkill.getBaseDisposition()); @@ -557,23 +571,21 @@ namespace MWMechanics x += fDispDiseaseMod; static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->mValue.getFloat(); - if (playerStats.getDrawState() == MWMechanics::DrawState_Weapon) + if (playerStats.getDrawState() == MWMechanics::DrawState::Weapon) x += fDispWeaponDrawn; x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - if(addTemporaryDispositionChange) - x += MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange(); - - int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return effective_disposition; + if (clamp) + return std::clamp(x, 0, 100);//, normally clamped to [0..100] when used + return static_cast(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) { // Make sure zero base price items/services can't be bought/sold for 1 gold // and return the intended base price for creature merchants - if (basePrice == 0 || ptr.getTypeName() == typeid(ESM::Creature).name()) + if (basePrice == 0 || ptr.getType() == ESM::Creature::sRecordId) return basePrice; const MWMechanics::NpcStats &sellerStats = ptr.getClass().getNpcStats(ptr); @@ -603,7 +615,7 @@ namespace MWMechanics return mActors.countDeaths (id); } - void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) + void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -640,7 +652,8 @@ namespace MWMechanics float x = 0; float y = 0; - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (type == PT_Admire) { @@ -665,12 +678,12 @@ namespace MWMechanics { float s = floor(r * fPerDieRollMult * fPerTempMult); - int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase(); - int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s))))); - npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); + const int flee = npcStats.getAiSetting(MWMechanics::AiSetting::Flee).getBase(); + const int fight = npcStats.getAiSetting(MWMechanics::AiSetting::Fight).getBase(); + npcStats.setAiSetting (MWMechanics::AiSetting::Flee, + std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100)); + npcStats.setAiSetting (MWMechanics::AiSetting::Fight, + std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100)); } float c = -std::abs(floor(r * fPerDieRollMult)); @@ -681,7 +694,7 @@ namespace MWMechanics // Deviating from Morrowind here: it doesn't increase disposition on marginal wins, // which seems to be a bug (MCP fixes it too). // Original logic: x = 0, y = -iPerMinChange - x = -iPerMinChange; + x = iPerMinChange; y = x; // This goes unused. } else @@ -706,12 +719,12 @@ namespace MWMechanics if (success) { float s = c * fPerDieRollMult * fPerTempMult; - int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); - int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); - npcStats.setAiSetting (CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); + const int flee = npcStats.getAiSetting(AiSetting::Flee).getBase(); + const int fight = npcStats.getAiSetting(AiSetting::Fight).getBase(); + npcStats.setAiSetting(AiSetting::Flee, + std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100)); + npcStats.setAiSetting(AiSetting::Fight, + std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100)); } x = floor(-c * fPerDieRollMult); @@ -727,19 +740,22 @@ namespace MWMechanics x = success ? std::max(iPerMinChange, c) : c; } - tempChange = type == PT_Intimidate ? x : int(x * fPerTempMult); + tempChange = type == PT_Intimidate ? int(x) : int(x * fPerTempMult); - float cappedDispositionChange = tempChange; - if (currentDisposition + tempChange > 100.f) - cappedDispositionChange = static_cast(100 - currentDisposition); - if (currentDisposition + tempChange < 0.f) - cappedDispositionChange = static_cast(-currentDisposition); + int cappedDispositionChange = tempChange; + if (currentDisposition + tempChange > 100) + cappedDispositionChange = 100 - currentDisposition; + if (currentDisposition + tempChange < 0) + { + cappedDispositionChange = -currentDisposition; + tempChange = cappedDispositionChange; + } permChange = floor(cappedDispositionChange / fPerTempMult); if (type == PT_Intimidate) { - permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : y; + permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : int(y); } } @@ -803,7 +819,7 @@ namespace MWMechanics MWBase::World* world = MWBase::Environment::get().getWorld(); world->getNavigator()->setUpdatesEnabled(mAI); if (mAI) - world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3()); + world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3()); return mAI; } @@ -869,7 +885,7 @@ namespace MWMechanics int lockLevel = cellref.getLockLevel(); if (target.getClass().isDoor() && (lockLevel <= 0 || lockLevel == ESM::UnbreakableLock) && - ptr.getCellRef().getTrap().empty()) + cellref.getTrap().empty()) { return true; } @@ -896,35 +912,11 @@ namespace MWMechanics return true; } - const std::string& owner = cellref.getOwner(); - bool isOwned = !owner.empty() && owner != "player"; - - const std::string& faction = cellref.getFaction(); - bool isFactionOwned = false; - if (!faction.empty() && ptr.getClass().isNpc()) - { - const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); - std::map::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction)); - if (found == factions.end() - || found->second < cellref.getFactionRank()) - isFactionOwned = true; - } - - const std::string& globalVariable = cellref.getGlobalVariable(); - if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1) - { - isOwned = false; - isFactionOwned = false; - } - - if (!cellref.getOwner().empty()) - victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false); - - // A special case for evidence chest - we should not allow to take items even if it is technically permitted - if (Misc::StringUtils::ciEqual(cellref.getRefId(), "stolen_goods")) + if (isOwned(ptr, target, victim)) return false; - return (!isOwned && !isFactionOwned); + // A special case for evidence chest - we should not allow to take items even if it is technically permitted + return !Misc::StringUtils::ciEqual(cellref.getRefId(), "stolen_goods"); } bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed) @@ -956,9 +948,14 @@ namespace MWMechanics void MechanicsManager::unlockAttempted(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item) { MWWorld::Ptr victim; - if (isAllowedToUse(ptr, item, victim)) - return; - commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction()); + if (isOwned(ptr, item, victim)) + { + // Note that attempting to unlock something that has ever been locked (even ESM::UnbreakableLock) is a crime even if it's already unlocked. + // Likewise, it's illegal to unlock something that has a trap but isn't otherwise locked. + const auto& cellref = item.getCellRef(); + if(cellref.getLockLevel() || !cellref.getTrap().empty()) + commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction()); + } } std::vector > MechanicsManager::getStolenItemOwners(const std::string& itemid) @@ -1300,7 +1297,7 @@ namespace MWMechanics continue; // Will the witness report the crime? - if (actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) + if (actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase() >= 100) { reported = true; @@ -1323,7 +1320,7 @@ namespace MWMechanics // once the bounty has been paid. actor.getClass().getNpcStats(actor).setCrimeId(id); - if (!actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!actor.getClass().getCreatureStats(actor).getAiSequence().isInPursuit()) { actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); } @@ -1332,7 +1329,7 @@ namespace MWMechanics { float dispTerm = (actor == victim) ? dispVictim : disp; - float alarmTerm = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase(); + float alarmTerm = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase(); if (type == OT_Pickpocket && alarmTerm <= 0) alarmTerm = 1.0; @@ -1344,7 +1341,7 @@ namespace MWMechanics fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; - int observerFightRating = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Fight).getBase(); + const int observerFightRating = actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); @@ -1356,7 +1353,7 @@ namespace MWMechanics 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(CreatureStats::AI_Fight, observerFightRating + static_cast(fightTerm)); + observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); @@ -1401,7 +1398,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().hasPackage(AiPackageTypeId::Pursue)) + if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit()) startCombat(victim, player); // Set the crime ID, which we will use to calm down participants @@ -1447,7 +1444,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 (!target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!target.getClass().getCreatureStats(target).getAiSequence().isInPursuit()) { // If an actor has OnPCHitMe declared in his script, his Fight = 0 and the attacker is player, // he will attack the player only if we will force him (e.g. via StartCombat console command) @@ -1455,12 +1452,22 @@ namespace MWMechanics std::string script = target.getClass().getScript(target); if (!script.empty() && target.getRefData().getLocals().hasVar(script, "onpchitme") && attacker == player) { - int fight = std::max(0, target.getClass().getCreatureStats(target).getAiSetting(CreatureStats::AI_Fight).getModified()); + const int fight = target.getClass().getCreatureStats(target).getAiSetting(AiSetting::Fight).getModified(); peaceful = (fight == 0); } if (!peaceful) + { startCombat(target, attacker); + // Force friendly actors into combat to prevent infighting between followers + std::set followersTarget; + getActorsSidingWith(target, followersTarget); + for(const auto& follower : followersTarget) + { + if(follower != attacker && follower != player) + startCombat(follower, attacker); + } + } } } @@ -1472,7 +1479,7 @@ namespace MWMechanics 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().hasPackage(AiPackageTypeId::Pursue); + && !target.getClass().getCreatureStats(target).getAiSequence().isInPursuit(); } void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) @@ -1575,8 +1582,8 @@ namespace MWMechanics } float target = x - y; - - return (Misc::Rng::roll0to99() >= target); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return (Misc::Rng::roll0to99(prng) >= target); } void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) @@ -1603,16 +1610,16 @@ namespace MWMechanics if (ptr.getClass().isClass(ptr, "Guard")) { stats.setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable - for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if (iter->first.getClass().isClass(iter->first, "Guard")) + if (actor.getPtr().getClass().isClass(actor.getPtr(), "Guard")) { - MWMechanics::AiSequence& aiSeq = iter->first.getClass().getCreatureStats(iter->first).getAiSequence(); + MWMechanics::AiSequence& aiSeq = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getAiSequence(); if (aiSeq.getTypeId() == MWMechanics::AiPackageTypeId::Pursue) { aiSeq.stopPursuit(); aiSeq.stack(MWMechanics::AiCombat(target), ptr); - iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable + actor.getPtr().getClass().getCreatureStats(actor.getPtr()).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable } } } @@ -1623,6 +1630,11 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); } + void MechanicsManager::stopCombat(const MWWorld::Ptr& actor) + { + mActors.stopCombat(actor); + } + void MechanicsManager::getObjectsInRange(const osg::Vec3f &position, float radius, std::vector &objects) { mActors.getObjectsInRange(position, radius, objects); @@ -1639,26 +1651,31 @@ namespace MWMechanics return mActors.isAnyObjectInRange(position, radius); } - std::list MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) { return mActors.getActorsSidingWith(actor); } - std::list MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) { return mActors.getActorsFollowing(actor); } - std::list MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingIndices(actor); } - std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { + std::map MechanicsManager::getActorsFollowingByIndex(const MWWorld::Ptr& actor) + { + return mActors.getActorsFollowingByIndex(actor); + } + + std::vector MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } - std::list MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) { + std::vector MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) { return mActors.getEnemiesNearby(actor); } @@ -1717,9 +1734,9 @@ namespace MWMechanics int disposition = 50; if (ptr.getClass().isNpc()) - disposition = getDerivedDisposition(ptr, true); + disposition = getDerivedDisposition(ptr); - int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() + int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(AiSetting::Fight).getModified() + static_cast(getFightDistanceBias(ptr, target) + getFightDispositionBias(static_cast(disposition))); if (ptr.getClass().isNpc() && target.getClass().isNpc()) @@ -1771,20 +1788,9 @@ namespace MWMechanics MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - if (actor == player->getPlayer()) - { - if (werewolf) - { - player->saveStats(); - player->setWerewolfStats(); - } - else - player->restoreStats(); - } - // Werewolfs can not cast spells, so we need to unset the prepared spell if there is one. - if (npcStats.getDrawState() == MWMechanics::DrawState_Spell) - npcStats.setDrawState(MWMechanics::DrawState_Nothing); + if (npcStats.getDrawState() == MWMechanics::DrawState::Spell) + npcStats.setDrawState(MWMechanics::DrawState::Nothing); npcStats.setWerewolf(werewolf); @@ -1808,13 +1814,23 @@ namespace MWMechanics // Update the GUI only when called on the player MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + // 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); + mActors.updateActor(actor, 0.f); + if (werewolf) { + player->saveStats(); + player->setWerewolfStats(); windowManager->forceHide(MWGui::GW_Inventory); windowManager->forceHide(MWGui::GW_Magic); } else { + player->restoreStats(); windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } @@ -1835,7 +1851,7 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && awarenessCheck(actor, neighbor)) { detected = true; - if (neighbor.getClass().getCreatureStats(neighbor).getAiSetting(MWMechanics::CreatureStats::AI_Alarm).getModified() > 0) + if (neighbor.getClass().getCreatureStats(neighbor).getAiSetting(MWMechanics::AiSetting::Alarm).getModified() > 0) { reported = true; break; @@ -1861,8 +1877,8 @@ namespace MWMechanics { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::NpcStats &stats = actor.getClass().getNpcStats(actor); - - stats.getSkill(ESM::Skill::Acrobatics).setBase(gmst.find("fWerewolfAcrobatics")->mValue.getInteger()); + auto& skill = stats.getSkill(ESM::Skill::Acrobatics); + skill.setModifier(gmst.find("fWerewolfAcrobatics")->mValue.getFloat() - skill.getModified()); } void MechanicsManager::cleanupSummonedCreature(const MWWorld::Ptr &caster, int creatureActorId) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 28f62b7774..4dd8790d2a 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -45,7 +45,7 @@ namespace MWMechanics void add (const MWWorld::Ptr& ptr) override; ///< Register an object for management - void remove (const MWWorld::Ptr& ptr) override; + void remove (const MWWorld::Ptr& ptr, bool keepActive) override; ///< Deregister an object for management void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; @@ -54,7 +54,7 @@ namespace MWMechanics void drop(const MWWorld::CellStore *cellStore) override; ///< Deregister all objects in the given cell. - void update (float duration, bool paused) override; + void update(float duration, bool paused); ///< Update objects /// /// \param paused In game type does not currently advance (this usually means some GUI @@ -75,7 +75,7 @@ namespace MWMechanics void setPlayerClass (const ESM::Class& class_) override; ///< Set player class to custom class. - void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) override; + void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) override; void rest(double hours, bool sleep) override; ///< If the player is sleeping or waiting, this should be called every hour. @@ -87,13 +87,13 @@ namespace MWMechanics int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) override; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. - int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) override; + int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) override; ///< Calculate the diposition of an NPC toward the player. int countDeaths (const std::string& id) const override; ///< Return the number of deaths for actors with the given ID. - void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) override; + void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) override; ///< Perform a persuasion action on NPC /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! @@ -102,6 +102,8 @@ namespace MWMechanics /// Makes \a ptr fight \a target. Also shouts a combat taunt. void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; + void stopCombat(const MWWorld::Ptr& ptr) override; + /** * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. @@ -147,12 +149,13 @@ namespace MWMechanics /// Check if there are actors in selected range bool isAnyActorInRange(const osg::Vec3f &position, float radius) override; - std::list getActorsSidingWith(const MWWorld::Ptr& actor) override; - std::list getActorsFollowing(const MWWorld::Ptr& actor) override; - std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) override; + std::vector getActorsSidingWith(const MWWorld::Ptr& actor) override; + std::vector getActorsFollowing(const MWWorld::Ptr& actor) override; + std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) override; + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) override; - std::list getActorsFighting(const MWWorld::Ptr& actor) override; - std::list getEnemiesNearby(const MWWorld::Ptr& actor) override; + std::vector getActorsFighting(const MWWorld::Ptr& actor) override; + std::vector getEnemiesNearby(const MWWorld::Ptr& actor) override; /// Recursive version of getActorsFollowing void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) override; @@ -185,7 +188,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 std::string spellId, bool manualSpell=false) override; + void castSpell(const MWWorld::Ptr& ptr, const std::string& spellId, bool manualSpell=false) override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; @@ -229,8 +232,6 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; - void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; - private: bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 5d19368bf6..34dcbb79a4 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -1,11 +1,12 @@ #include "npcstats.hpp" #include +#include -#include -#include -#include -#include +#include +#include +#include +#include #include "../mwworld/esmstore.hpp" @@ -371,7 +372,7 @@ int MWMechanics::NpcStats::getReputation() const void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine - mReputation = std::min(255, std::max(0, reputation)); + mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index 5b18fc2c30..5a474abf1c 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -1,7 +1,7 @@ #include "objects.hpp" #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,59 +13,43 @@ namespace MWMechanics { -Objects::Objects() -{ -} - -Objects::~Objects() -{ - for(auto& object : mObjects) - { - delete object.second; - object.second = nullptr; - } -} - void Objects::addObject(const MWWorld::Ptr& ptr) { removeObject(ptr); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if(anim) mObjects.insert(std::make_pair(ptr, new CharacterController(ptr, anim))); + if (anim == nullptr) + return; + + const auto it = mObjects.emplace(mObjects.end(), ptr, anim); + mIndex.emplace(ptr.mRef, it); } void Objects::removeObject(const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - delete iter->second; - mObjects.erase(iter); + mObjects.erase(iter->second); + mIndex.erase(iter); } } void Objects::updateObject(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { - PtrControllerMap::iterator iter = mObjects.find(old); - if(iter != mObjects.end()) - { - CharacterController *ctrl = iter->second; - mObjects.erase(iter); - - ctrl->updatePtr(ptr); - mObjects.insert(std::make_pair(ptr, ctrl)); - } + const auto iter = mIndex.find(old.mRef); + if (iter != mIndex.end()) + iter->second->updatePtr(ptr); } void Objects::dropObjects (const MWWorld::CellStore *cellStore) { - PtrControllerMap::iterator iter = mObjects.begin(); - while(iter != mObjects.end()) + for (auto iter = mObjects.begin(); iter != mObjects.end();) { - if(iter->first.getCell()==cellStore) + if (iter->getPtr().getCell() == cellStore) { - delete iter->second; - mObjects.erase(iter++); + mIndex.erase(iter->getPtr().mRef); + iter = mObjects.erase(iter); } else ++iter; @@ -76,8 +60,8 @@ void Objects::update(float duration, bool paused) { if(!paused) { - for(auto& object : mObjects) - object.second->update(duration); + for (CharacterController& object : mObjects) + object.update(duration); } else { @@ -86,15 +70,15 @@ void Objects::update(float duration, bool paused) if(mode != MWGui::GM_Container) return; - for(auto& object : mObjects) + for (CharacterController& object : mObjects) { - if (object.first.getTypeName() != typeid(ESM::Container).name()) + if (object.getPtr().getType() != ESM::Container::sRecordId) continue; - if (object.second->isAnimPlaying("containeropen")) + if (object.isAnimPlaying("containeropen")) { - object.second->update(duration); - MWBase::Environment::get().getWorld()->updateAnimatedCollisionShape(object.first); + object.update(duration); + MWBase::Environment::get().getWorld()->updateAnimatedCollisionShape(object.getPtr()); } } } @@ -102,23 +86,23 @@ void Objects::update(float duration, bool paused) bool Objects::onOpen(const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) return iter->second->onOpen(); return true; } void Objects::onClose(const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) iter->second->onClose(); } bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) { - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { return iter->second->playGroup(groupName, mode, number, persist); } @@ -130,24 +114,22 @@ bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& gro } void Objects::skipAnimation(const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) iter->second->skipAnim(); } void Objects::persistAnimationStates() { - for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) - iter->second->persistAnimationState(); + for (CharacterController& object : mObjects) + object.persistAnimationState(); } -void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) +void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { - for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) - { - if ((position - iter->first.getRefData().getPosition().asVec3()).length2() <= radius*radius) - out.push_back(iter->first); - } + for (const CharacterController& object : mObjects) + if ((position - object.getPtr().getRefData().getPosition().asVec3()).length2() <= radius * radius) + out.push_back(object.getPtr()); } } diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 5160114a3f..a6b2d7c675 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -1,9 +1,12 @@ #ifndef GAME_MWMECHANICS_ACTIVATORS_H #define GAME_MWMECHANICS_ACTIVATORS_H +#include "character.hpp" + #include #include #include +#include namespace osg { @@ -18,17 +21,12 @@ namespace MWWorld namespace MWMechanics { - class CharacterController; - class Objects { - typedef std::map PtrControllerMap; - PtrControllerMap mObjects; + std::list mObjects; + std::map::iterator> mIndex; public: - Objects(); - ~Objects(); - void addObject (const MWWorld::Ptr& ptr); ///< Register an animated object @@ -51,7 +49,7 @@ namespace MWMechanics void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); - void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& out); + void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; std::size_t size() const { diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 88325ee7c7..0f73890eb7 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -1,9 +1,14 @@ #include "obstacle.hpp" +#include + #include +#include #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "movement.hpp" @@ -72,6 +77,21 @@ namespace MWMechanics return MWWorld::Ptr(); // none found } + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, + std::vector* occupyingActors) + { + const auto world = MWBase::Environment::get().getWorld(); + const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; + const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + if (ignorePlayer) + { + const std::array ignore {actor, world->getPlayerConstPtr()}; + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + } + const std::array ignore {actor}; + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + } + ObstacleCheck::ObstacleCheck() : mWalkState(WalkState::Initial) , mStateDuration(0) diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 6c2197d811..2026f22129 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -3,16 +3,19 @@ #include +#include + namespace MWWorld { class Ptr; + class ConstPtr; } namespace MWMechanics { struct Movement; - static const int NUM_EVADE_DIRECTIONS = 4; + static constexpr int NUM_EVADE_DIRECTIONS = 4; /// tests actor's proximity to a closed door by default bool proximityToDoor(const MWWorld::Ptr& actor, float minDist); @@ -21,6 +24,9 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer = false, + std::vector* occupyingActors = nullptr); + class ObstacleCheck { public: diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index a82dcf7173..9a75585cc1 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -3,11 +3,13 @@ #include #include -#include +#include + +#include #include -#include #include #include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -97,15 +99,28 @@ namespace float dotProduct = v1.x() * v3.x() + v1.y() * v3.y(); float crossProduct = v1.x() * v3.y() - v1.y() * v3.x(); - // Check that the angle between v1 and v3 is less or equal than 10 degrees. - static const float cos170 = std::cos(osg::PI / 180 * 170); - bool checkAngle = dotProduct <= cos170 * v1.length() * v3.length(); + // Check that the angle between v1 and v3 is less or equal than 5 degrees. + static const float cos175 = std::cos(osg::PI * (175.0 / 180)); + bool checkAngle = dotProduct <= cos175 * v1.length() * v3.length(); // Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`. - bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length() * 2; + bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length(); return checkAngle && checkDist; } + + struct IsValidShortcut + { + const DetourNavigator::Navigator* mNavigator; + const DetourNavigator::AgentBounds mAgentBounds; + const DetourNavigator::Flags mFlags; + + bool operator()(const osg::Vec3f& start, const osg::Vec3f& end) const + { + const auto position = DetourNavigator::raycast(*mNavigator, mAgentBounds, start, end, mFlags); + return position.has_value() && std::abs((position.value() - start).length2() - (end - start).length2()) <= 1; + } + }; } namespace MWMechanics @@ -194,9 +209,6 @@ namespace MWMechanics endPointInLocalCoords, startNode); - if (!endNode.second) - return; - // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); @@ -267,7 +279,8 @@ namespace MWMechanics // unreachable pathgrid point. // // The AI routines will have to deal with such situations. - *out++ = endPoint; + if (endNode.second) + *out++ = endPoint; } float PathFinder::getZAngleToNext(float x, float y) const @@ -296,7 +309,9 @@ namespace MWMechanics return getXAngleToDir(dir); } - void PathFinder::update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance) + void PathFinder::update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, + bool shortenIfAlmostStraight, bool canMoveByZ, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags) { if (mPath.empty()) return; @@ -304,13 +319,44 @@ namespace MWMechanics while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); - while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance)) - mPath.erase(mPath.begin() + 1); - if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance)) - mPath.pop_front(); + const IsValidShortcut isValidShortcut { + MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, flags + }; - if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance) - mPath.pop_front(); + if (shortenIfAlmostStraight) + { + while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance) + && isValidShortcut(mPath[0], mPath[2])) + mPath.erase(mPath.begin() + 1); + if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance) + && isValidShortcut(position, mPath[1])) + mPath.pop_front(); + } + + if (mPath.size() > 1) + { + std::size_t begin = 0; + for (std::size_t i = 1; i < mPath.size(); ++i) + { + const float sqrDistance = Misc::getVectorToLine(position, mPath[i - 1], mPath[i]).length2(); + if (sqrDistance < pointTolerance * pointTolerance && isValidShortcut(position, mPath[i])) + begin = i; + } + for (std::size_t i = 0; i < begin; ++i) + mPath.pop_front(); + } + + if (mPath.size() == 1) + { + float distSqr; + if (canMoveByZ) + distSqr = (mPath.front() - position).length2(); + else + distSqr = sqrDistanceIgnoreZ(mPath.front(), position); + if (distSqr < destinationTolerance * destinationTolerance) + mPath.pop_front(); + } } void PathFinder::buildStraightPath(const osg::Vec3f& endPoint) @@ -328,58 +374,76 @@ namespace MWMechanics buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - mConstructed = true; + mConstructed = !mPath.empty(); } void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts) + const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path - if (!buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath))) + DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, + areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + + if (status != DetourNavigator::Status::Success) + mPath.clear(); + + if (status == DetourNavigator::Status::NavMeshNotFound) mPath.push_back(endPoint); - mConstructed = true; + mConstructed = !mPath.empty(); } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { mPath.clear(); mCell = cell; - bool hasNavMesh = false; + DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) - hasNavMesh = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)); + { + status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, + endTolerance, pathType, std::back_inserter(mPath)); + if (status != DetourNavigator::Status::Success) + mPath.clear(); + } - if (hasNavMesh && mPath.empty()) - buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, std::back_inserter(mPath)); + if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty() && (flags & DetourNavigator::Flag_usePathgrid) == 0) + { + status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + if (status != DetourNavigator::Status::Success) + mPath.clear(); + } if (mPath.empty()) buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - if (!hasNavMesh && mPath.empty()) + if (status == DetourNavigator::Status::NavMeshNotFound && mPath.empty()) mPath.push_back(endPoint); - mConstructed = true; + mConstructed = !mPath.empty(); } - bool PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out) + DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, + const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::back_insert_iterator> out) { const auto world = MWBase::Environment::get().getWorld(); const auto stepSize = getPathStepSize(actor); const auto navigator = world->getNavigator(); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts, out); + const auto status = DetourNavigator::findPath(*navigator, agentBounds, stepSize, + startPoint, endPoint, flags, areaCosts, endTolerance, out); - if (status == DetourNavigator::Status::NavMeshNotFound) - return false; + if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) + return DetourNavigator::Status::Success; if (status != DetourNavigator::Status::Success) { @@ -389,11 +453,12 @@ namespace MWMechanics << DetourNavigator::WriteFlags {flags} << ")"; } - return true; + return status; } - void PathFinder::buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + void PathFinder::buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts) { if (mPath.empty()) return; @@ -407,8 +472,9 @@ namespace MWMechanics const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); std::deque prePath; auto prePathInserter = std::back_inserter(prePath); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, mPath.front(), flags, areaCosts, - prePathInserter); + const float endTolerance = 0; + const auto status = DetourNavigator::findPath(*navigator, agentBounds, stepSize, + startPoint, mPath.front(), flags, areaCosts, endTolerance, prePathInserter); if (status == DetourNavigator::Status::NavMeshNotFound) return; @@ -430,4 +496,23 @@ namespace MWMechanics std::copy(prePath.rbegin(), prePath.rend(), std::front_inserter(mPath)); } + + void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + { + const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); + const auto maxDistance = std::min( + navigator->getMaxNavmeshAreaRealRadius(), + static_cast(Constants::CellSizeInUnits) + ); + const auto startToEnd = endPoint - startPoint; + const auto distance = startToEnd.length(); + if (distance <= maxDistance) + return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, pathType); + const auto end = startPoint + startToEnd * maxDistance / distance; + buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); + } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 5af822fee7..c07b085e5a 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -7,8 +7,9 @@ #include #include +#include #include -#include +#include namespace MWWorld { @@ -17,6 +18,11 @@ namespace MWWorld class Ptr; } +namespace DetourNavigator +{ + struct AgentBounds; +} + namespace MWMechanics { class PathgridGraph; @@ -69,6 +75,12 @@ namespace MWMechanics // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); + enum class PathType + { + Full, + Partial, + }; + class PathFinder { public: @@ -91,18 +103,27 @@ namespace MWMechanics const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts); + const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); - void buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, + void buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); + void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); + /// Remove front point if exist and within tolerance - void update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance); + void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, + bool shortenIfAlmostStraight, bool canMoveByZ, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags); bool checkPathCompleted() const { @@ -161,7 +182,7 @@ namespace MWMechanics // Caller needs to be careful for very short distances (i.e. less than 1) // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 // - static float distanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos) + static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) { return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); } @@ -203,9 +224,10 @@ namespace MWMechanics void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, std::back_insert_iterator> out); - bool buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out); + [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, + const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::back_insert_iterator> out); }; } diff --git a/apps/openmw/mwmechanics/pathgrid.hpp b/apps/openmw/mwmechanics/pathgrid.hpp index 050504617e..dfe958e745 100644 --- a/apps/openmw/mwmechanics/pathgrid.hpp +++ b/apps/openmw/mwmechanics/pathgrid.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/pickpocket.cpp b/apps/openmw/mwmechanics/pickpocket.cpp index 05e8a03930..1b638eadd2 100644 --- a/apps/openmw/mwmechanics/pickpocket.cpp +++ b/apps/openmw/mwmechanics/pickpocket.cpp @@ -41,7 +41,8 @@ namespace MWMechanics int iPickMaxChance = MWBase::Environment::get().getWorld()->getStore().get() .find("iPickMaxChance")->mValue.getInteger(); - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (t < pcSneak / iPickMinChance) { return (roll > int(pcSneak / iPickMinChance)); @@ -53,7 +54,7 @@ namespace MWMechanics } } - bool Pickpocket::pick(MWWorld::Ptr item, int count) + bool Pickpocket::pick(const MWWorld::Ptr& item, int count) { float stackValue = static_cast(item.getClass().getValue(item) * count); float fPickPocketMod = MWBase::Environment::get().getWorld()->getStore().get() diff --git a/apps/openmw/mwmechanics/pickpocket.hpp b/apps/openmw/mwmechanics/pickpocket.hpp index 4de1e37f84..0957b7a680 100644 --- a/apps/openmw/mwmechanics/pickpocket.hpp +++ b/apps/openmw/mwmechanics/pickpocket.hpp @@ -13,7 +13,7 @@ namespace MWMechanics /// Steal some items /// @return Was the thief detected? - bool pick (MWWorld::Ptr item, int count); + bool pick (const MWWorld::Ptr& item, int count); /// End the pickpocketing process /// @return Was the thief detected? bool finish (); diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index 51c78e1e3b..1f92466e04 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -49,7 +49,8 @@ bool rechargeItem(const MWWorld::Ptr &item, const MWWorld::Ptr &gem) intelligenceTerm = 1; float x = (player.getClass().getSkill(player, ESM::Skill::Enchant) + intelligenceTerm + luckTerm) * stats.getFatigueTerm(); - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (roll < x) { std::string soul = gem.getCellRef().getSoul(); diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 81886ed9b0..ac6cc5b414 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -44,7 +44,8 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair) float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm; - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (roll <= x) { int y = static_cast(fRepairAmountMult * toolQuality * roll); diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index e642a7bb4b..bfb48dd754 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -54,7 +54,8 @@ namespace MWMechanics resultMessage = "#{sLockImpossible}"; else { - if (Misc::Rng::roll0to99() <= x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) <= x) { lock.getCellRef().unlock(); resultMessage = "#{sLockSuccess}"; @@ -98,7 +99,8 @@ namespace MWMechanics resultMessage = "#{sTrapImpossible}"; else { - if (Misc::Rng::roll0to99() <= x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) <= x) { trap.getCellRef().setTrap(""); diff --git a/apps/openmw/mwmechanics/setbaseaisetting.hpp b/apps/openmw/mwmechanics/setbaseaisetting.hpp new file mode 100644 index 0000000000..523161f731 --- /dev/null +++ b/apps/openmw/mwmechanics/setbaseaisetting.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_MWMECHANICS_SETBASEAISETTING_H +#define OPENMW_MWMECHANICS_SETBASEAISETTING_H + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "aisetting.hpp" +#include "creaturestats.hpp" + +namespace MWMechanics +{ + template + void setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) + { + T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); + switch (setting) + { + case MWMechanics::AiSetting::Hello: + copy.mAiData.mHello = value; + break; + case MWMechanics::AiSetting::Fight: + copy.mAiData.mFight = value; + break; + case MWMechanics::AiSetting::Flee: + copy.mAiData.mFlee = value; + break; + case MWMechanics::AiSetting::Alarm: + copy.mAiData.mAlarm = value; + break; + default: + assert(false); + } + MWBase::Environment::get().getWorld()->createOverrideRecord(copy); + } +} + +#endif diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp deleted file mode 100644 index bab290fdab..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "spellabsorption.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "creaturestats.hpp" -#include "spellutil.hpp" - -namespace MWMechanics -{ - - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor - { - public: - float mProbability{0.f}; - - GetAbsorptionProbability() = default; - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float /*remainingTime*/, float /*totalTime*/) override - { - if (key.mId == ESM::MagicEffect::SpellAbsorption) - { - if (mProbability == 0.f) - mProbability = magnitude / 100; - else - { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - mProbability; - failProbability *= 1.f - magnitude / 100; - mProbability = 1.f - failProbability; - } - } - } - }; - - bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) - { - if (spellId.empty() || target.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - CreatureStats& stats = target.getClass().getCreatureStats(target); - if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) - return false; - - GetAbsorptionProbability check; - stats.getActiveSpells().visitEffectSources(check); - stats.getSpells().visitEffectSources(check); - if (target.getClass().hasInventoryStore(target)) - target.getClass().getInventoryStore(target).visitEffectSources(check); - - int chance = check.mProbability * 100; - if (Misc::Rng::roll0to99() >= chance) - return false; - - const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); - const ESM::Spell* spell = esmStore.get().search(spellId); - int spellCost = 0; - if (spell) - { - spellCost = spell->mData.mCost; - } - else - { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); - if (enchantment) - spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); - } - - // Magicka is increased by the cost of the spell - DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spellCost); - stats.setMagicka(magicka); - return true; - } - -} diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp deleted file mode 100644 index 0fe501df91..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MWMECHANICS_SPELLABSORPTION_H -#define MWMECHANICS_SPELLABSORPTION_H - -#include - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - // Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. - bool absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); -} - -#endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 81b3a353df..3852d3f61a 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -22,12 +23,9 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "linkedeffects.hpp" -#include "spellabsorption.hpp" -#include "spellresistance.hpp" +#include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" -#include "tickableeffects.hpp" #include "weapontype.hpp" namespace MWMechanics @@ -54,13 +52,14 @@ namespace MWMechanics (mTarget.getRefData().getPosition().asVec3() + offset) - (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mSlot); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, - const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) + const ESM::EffectList &effects, ESM::RangeType range, bool exploded) { - if (!target.isEmpty() && target.getClass().isActor()) + const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); + if (targetIsActor) { // Early-out for characters that have departed. const auto& stats = target.getClass().getCreatureStats(target); @@ -82,14 +81,15 @@ namespace MWMechanics return; const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search (mId); - if (spell && !target.isEmpty() && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) + if (spell && targetIsActor && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) { int requiredResistance = (spell->mData.mType == ESM::Spell::ST_Disease) ? ESM::MagicEffect::ResistCommonDisease : ESM::MagicEffect::ResistBlightDisease; float x = target.getClass().getCreatureStats(target).getMagicEffects().get(requiredResistance).getMagnitude(); - if (Misc::Rng::roll0to99() <= x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) <= x) { // Fully resisted, show message if (target == getPlayer()) @@ -98,29 +98,18 @@ namespace MWMechanics } } - ESM::EffectList reflectedEffects; - std::vector appliedLastingEffects; - - // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. - // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. - // Otherwise, they'd only apply after the whole spell was added. - MagicEffects targetEffects; - if (!target.isEmpty() && target.getClass().isActor()) - targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); + ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); - ActiveSpells targetSpells; - if (!target.isEmpty() && target.getClass().isActor()) - targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); + const ActiveSpells* targetSpells = nullptr; + if (targetIsActor) + targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); bool canCastAnEffect = false; // For bound equipment.If this remains false // throughout the iteration of this spell's // effects, we display a "can't re-cast" message - // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(mId, caster, target); - int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) @@ -133,7 +122,7 @@ namespace MWMechanics effectIt->mEffectID); // Re-casting a bound equipment effect has no effect if the spell is still active - if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) + if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells && targetSpells->isSpellActive(mId)) { if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); @@ -141,308 +130,68 @@ namespace MWMechanics } canCastAnEffect = true; - if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer)) - continue; - // caster needs to be an actor for linked effects (e.g. Absorb) if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked && (caster.isEmpty() || !caster.getClass().isActor())) continue; - // 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); - - // Avoid proceeding further for absorbed spells. - if (absorbed) - continue; - - // Reflect harmful effects - if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) - continue; - - // Try resisting. - float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); - if (magnitudeMult == 0) - { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); - } - else - { - float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); - magnitude *= magnitudeMult; - - if (!target.getClass().isActor()) - { - // non-actor objects have no list of active magic effects, so have to apply instantly - if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) - continue; - } - else // target.getClass().isActor() == true - { - ActiveSpells::ActiveEffect effect; - effect.mEffectId = effectIt->mEffectID; - effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; - effect.mMagnitude = magnitude; - effect.mTimeLeft = 0.f; - effect.mEffectIndex = currentEffectIndex; - - // Avoid applying absorb effects if the caster is the target - // We still need the spell to be added - if (caster == target - && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute - && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - { - effect.mMagnitude = 0; - } - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMagnitude = 0; - } - - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) - { - // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. - MWBase::Environment::get().getWindowManager()->setEnemy(target); - } - - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; + ActiveSpells::ActiveEffect effect; + effect.mEffectId = effectIt->mEffectID; + effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = effectIt->mMagnMin; + effect.mMaxMagnitude = effectIt->mMagnMax; + effect.mTimeLeft = 0.f; + effect.mEffectIndex = currentEffectIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if(mManualSpell) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; - if (effect.mDuration == 0) - { - // We still should add effect to list to allow GetSpellEffects to detect this spell - appliedLastingEffects.push_back(effect); - - // duration 0 means apply full magnitude instantly - bool wasDead = target.getClass().getCreatureStats(target).isDead(); - effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); - bool isDead = target.getClass().getCreatureStats(target).isDead(); + bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); - if (!wasDead && isDead) - MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); - } - else - { - effect.mTimeLeft = effect.mDuration; - - targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); - - // add to list of active effects, to apply in next frame - appliedLastingEffects.push_back(effect); - - // Unequip all items, if a spell with the ExtraSpell effect was casted - if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) - { - MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); - store.unequipAll(target); - } - - // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target - if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) - || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) - && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) - { - MWMechanics::AiFollow package(caster, true); - target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); - } - - // For absorb effects, also apply the effect to the caster - but with a negative - // magnitude, since we're transferring stats from the target to the caster - if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); - } - } + effect.mTimeLeft = effect.mDuration; - // Re-casting a summon effect will remove the creature from previous castings of that effect. - if (isSummoningEffect(effectIt->mEffectID) && !target.isEmpty() && target.getClass().isActor()) - { - CreatureStats& targetStats = target.getClass().getCreatureStats(target); - ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); - auto findCreature = targetStats.getSummonedCreatureMap().find(key); - if (findCreature != targetStats.getSummonedCreatureMap().end()) - { - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); - targetStats.getSummonedCreatureMap().erase(findCreature); - } - } - - if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + // add to list of active effects, to apply in next frame + params.getEffects().emplace_back(effect); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - - // Add VFX - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Note: in case of non actor, a free effect should be fine as well - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } - } - } - - if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); - - if (!target.isEmpty()) { - if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true, exploded); - - if (!appliedLastingEffects.empty()) + bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { - int casterActorId = -1; - if (!caster.isEmpty() && caster.getClass().isActor()) - casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, - mSourceName, casterActorId); + // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. + MWBase::Environment::get().getWindowManager()->setEnemy(target); } - } - } - bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) - { - short effectId = effect.mId; - if (target.getClass().canLock(target)) - { - if (effectId == ESM::MagicEffect::Lock) + if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); - target.getCellRef().lock(static_cast(magnitude)); - } - return true; + playEffects(target, *magicEffect); } - else if (effectId == ESM::MagicEffect::Open) - { - if (!caster.isEmpty()) - { - MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); - // Use the player instead of the caster for vanilla crime compatibility - } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() <= magnitude) - { - if (target.getCellRef().getLockLevel() > 0) - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + } - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); - } - target.getCellRef().unlock(); - } - else - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); - } + if (!exploded) + MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile, mSlot); - return true; - } - } - else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) + if (!target.isEmpty()) { - target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); - return true; - } - else if (target.getClass().isActor() && target == getPlayer()) - { - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); - - if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; - MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); - anim->removeEffect(effectId); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_end"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1); - return true; - } - else if (effectId == ESM::MagicEffect::Mark) - { - if (teleportingEnabled) - { - MWBase::Environment::get().getWorld()->getPlayer().markPosition( - target.getCell(), target.getRefData().getPosition()); - } - else if (caster == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - } - return true; - } - else if (effectId == ESM::MagicEffect::Recall) + if (!params.getEffects().empty()) { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - - MWWorld::CellStore* markedCell = nullptr; - ESM::Position markedPosition; - - MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell) + if(targetIsActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); + else { - MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, - markedPosition, false); - action.execute(target); - anim->removeEffect(effectId); + // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + // and we can ignore reflection since non-actors cannot reflect spells + for(auto& effect : params.getEffects()) + applyMagicEffect(target, caster, params, effect, 0.f); } - return true; } } - return false; } - bool CastSpell::cast(const std::string &id) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -458,7 +207,7 @@ namespace MWMechanics throw std::runtime_error("ID type cannot be casted"); } - bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) + bool CastSpell::cast(const MWWorld::Ptr &item, int slot, bool launchProjectile) { std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) @@ -469,11 +218,11 @@ namespace MWMechanics const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); - mStack = false; + mSlot = slot; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; - if (item.getTypeName() == typeid(ESM::Weapon).name()) + if (item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; @@ -532,7 +281,10 @@ namespace MWMechanics mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 3); } - inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); + if (isProjectile) + inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Self); + else + inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); @@ -549,7 +301,7 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); @@ -560,7 +312,6 @@ namespace MWMechanics { mSourceName = spell->mName; mId = spell->mId; - mStack = false; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -582,7 +333,7 @@ namespace MWMechanics DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); - float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); + float fatigueLoss = MWMechanics::calcSpellCost(*spell) * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); @@ -590,7 +341,8 @@ namespace MWMechanics // Check success float successChance = getSpellSuccessChance(spell, mCaster, nullptr, true, false); - if (Misc::Rng::roll0to99() >= successChance) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) >= successChance) { if (mCaster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); @@ -635,7 +387,7 @@ namespace MWMechanics bool CastSpell::cast (const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; mSourceName = ingredient->mName; ESM::ENAMstruct effect; @@ -654,7 +406,8 @@ namespace MWMechanics + 0.1f * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()) * creatureStats.getFatigueTerm(); - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (roll > x) { // "X has no effect on you" @@ -712,6 +465,8 @@ namespace MWMechanics { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); std::vector addedEffects; + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + for (const ESM::ENAMstruct& effectData : effects) { const auto effect = store.get().find(effectData.mEffectID); @@ -724,23 +479,48 @@ namespace MWMechanics castStatic = store.get().find ("VFX_DefaultCast"); // check if the effect was already added - if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end()) + if (std::find(addedEffects.begin(), addedEffects.end(), + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)) + != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", effect->mParticle); + animation->addEffect( + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), + effect->mIndex, false, "", effect->mParticle); } else { // If the caster has no animation, add the effect directly to the effectManager - // We should scale it manually - osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f / Constants::UnitsPerFoot); - float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); - float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f; + // We must scale and position it manually + float scale = mCaster.getCellRef().getScale(); osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale); + if (!mCaster.getClass().isNpc()) + { + osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f); + scale *= std::max({bounds.x(), bounds.y(), bounds.z() / 2.f}) / 64.f; + float offset = 0.f; + if (bounds.z() < 128.f) + offset = bounds.z() - 128.f; + else if (bounds.z() < bounds.x() + bounds.y()) + offset = 128.f - bounds.z(); + if (MWBase::Environment::get().getWorld()->isFlying(mCaster)) + offset /= 20.f; + pos.z() += offset * scale; + } + else + { + // Additionally use the NPC's height + osg::Vec3f npcScaleVec (1.f, 1.f, 1.f); + mCaster.getClass().adjustScale(mCaster, npcScaleVec, true); + scale *= npcScaleVec.z(); + } + scale = std::max(scale, 1.f); + MWBase::Environment::get().getWorld()->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), + effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) @@ -750,7 +530,7 @@ namespace MWMechanics "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; - addedEffects.push_back("meshes\\" + castStatic->mModel); + addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!effect->mCastSound.empty()) @@ -759,4 +539,41 @@ namespace MWMechanics sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } } + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping) + { + if (playNonLooping) + { + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!magicEffect.mHitSound.empty()) + sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(target, schools[magicEffect.mData.mSchool]+" hit", 1.0f, 1.0f); + } + + // Add VFX + const ESM::Static* castStatic; + if (!magicEffect.mHit.empty()) + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect.mHit); + else + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); + + bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim && !castStatic->mModel.empty()) + { + // 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); + } + } + } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 45431bbc6a..8dfd2b3f0c 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -1,8 +1,8 @@ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H -#include -#include +#include +#include #include "../mwworld/ptr.hpp" @@ -12,6 +12,7 @@ namespace ESM struct Ingredient; struct Potion; struct EffectList; + struct MagicEffect; } namespace MWMechanics @@ -27,13 +28,14 @@ namespace MWMechanics void playSpellCastingEffects(const std::vector& effects); public: - bool mStack{false}; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb bool mAlwaysSucceed{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.) + int mSlot{0}; + ESM::ActiveSpells::EffectType mType{ESM::ActiveSpells::Type_Temporary}; public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); @@ -42,7 +44,7 @@ namespace MWMechanics /// @note mCaster must be an actor /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. - bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); + bool cast (const MWWorld::Ptr& item, int slot, bool launchProjectile=true); /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); @@ -60,12 +62,10 @@ namespace MWMechanics /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); - - /// @note \a caster can be any type of object, or even an empty object. - /// @return was the target suitable for the effect? - bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); + const ESM::EffectList& effects, ESM::RangeType range, bool exploded=false); }; + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); } #endif diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp new file mode 100644 index 0000000000..81a3dc0921 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -0,0 +1,1186 @@ +#include "spelleffects.hpp" + +#include + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" +#include "../mwmechanics/summoning.hpp" + +#include "../mwrender/animation.hpp" + +#include "../mwworld/actionequip.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace +{ + float roll(const ESM::ActiveEffect& effect) + { + if(effect.mMinMagnitude == effect.mMaxMagnitude) + return effect.mMinMagnitude; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return effect.mMinMagnitude + Misc::Rng::rollDice(effect.mMaxMagnitude - effect.mMinMagnitude + 1, prng); + } + + void modifyAiSetting(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, ESM::MagicEffect::Effects creatureEffect, MWMechanics::AiSetting setting, float magnitude, bool& invalid) + { + if(target == MWMechanics::getPlayer() || (effect.mEffectId == creatureEffect) == target.getClass().isNpc()) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getAiSetting(setting); + stat.setModifier(static_cast(stat.getModifier() + magnitude)); + creatureStats.setAiSetting(setting, stat); + } + } + + void adjustDynamicStat(const MWWorld::Ptr& target, int index, float magnitude, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero, allowIncreaseAboveModified); + creatureStats.setDynamic(index, stat); + } + + void modDynamicStat(const MWWorld::Ptr& target, int index, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + float current = stat.getCurrent(); + stat.setBase(std::max(0.f, stat.getBase() + magnitude)); + stat.setCurrent(current + magnitude); + creatureStats.setDynamic(index, stat); + } + + void damageAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + if(effect.mEffectId == ESM::MagicEffect::DamageAttribute) + magnitude = std::min(attr.getModified(), magnitude); + attr.damage(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void restoreAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.restore(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void fortifyAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.setModifier(attr.getModifier() + magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void damageSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + if(effect.mEffectId == ESM::MagicEffect::DamageSkill) + magnitude = std::min(skill.getModified(), magnitude); + skill.damage(magnitude); + } + + void restoreSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.restore(magnitude); + } + + void fortifySkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setModifier(skill.getModifier() + magnitude); + } + + bool disintegrateSlot(const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != MWMechanics::getPlayer()) + inv.autoEquip(ptr); + else + inv.unequipItem(*item, ptr); + } + + return true; + } + + return false; + } + + int getBoundItemSlot(const MWWorld::Ptr& boundPtr) + { + const auto [slots, _] = boundPtr.getClass().getEquipmentSlots(boundPtr); + if(!slots.empty()) + return slots[0]; + return -1; + } + + void addBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + + int slot = getBoundItemSlot(boundPtr); + auto prevItem = slot >= 0 ? store.getSlot(slot) : store.end(); + + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem; + auto it = slot >= 0 ? store.getSlot(slot) : store.end(); + // Equip can fail because beast races cannot equip boots/helmets + if(it != store.end()) + newItem = *it; + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState::Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void removeBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + auto item = std::find_if(store.begin(), store.end(), [&] (const auto& it) + { + return Misc::StringUtils::ciEqual(it.getCellRef().getRefId(), itemId); + }); + if(item == store.end()) + return; + int slot = getBoundItemSlot(*item); + + auto currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); + + if (actor != MWMechanics::getPlayer()) + { + store.remove(itemId, 1, actor); + + // Equip a replacement + if (!wasEquipped) + return; + + auto type = currentItem->getType(); + if (type != ESM::Weapon::sRecordId && type != ESM::Armor::sRecordId && type != ESM::Clothing::sRecordId) + return; + + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + + if (!actor.getClass().hasInventoryStore(actor)) + return; + + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return; + + actor.getClass().getInventoryStore(actor).autoEquip(actor); + + return; + } + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (!prevItemId.empty() && wasEquipped) + { + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr prevItem = store.findReplacement(prevItemId); + if (!prevItem.isEmpty()) + { + MWWorld::ActionEquip action(prevItem); + action.execute(actor); + } + } + + store.remove(itemId, 1, actor); + } + + bool isCorprusEffect(const MWMechanics::ActiveSpells::ActiveEffect& effect, bool harmfulOnly = false) + { + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId != ESM::MagicEffect::Corprus) + { + const auto* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce && (!harmfulOnly || magicEffect->mData.mFlags & ESM::MagicEffect::Flags::Harmful)) + return true; + } + return false; + } + + void absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + { + const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Static* absorbStatic = esmStore.get().find("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, std::string()); + } + const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; + if (spell) + { + spellCost = MWMechanics::calcSpellCost(*spell); + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + if (enchantment) + spellCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); + } + + // Magicka is increased by the cost of the spell + auto& stats = target.getClass().getCreatureStats(target); + auto magicka = stats.getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spellCost); + stats.setMagicka(magicka); + } + + MWMechanics::MagicApplicationResult applyProtections(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, const ESM::MagicEffect* magicEffect) + { + 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) + { + bool canReflect = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && + !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) && magnitudes.get(ESM::MagicEffect::Reflect).getMagnitude() > 0.f; + bool canAbsorb = !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_SpellAbsorption) && magnitudes.get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f; + if(canReflect || canAbsorb) + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + for(const auto& activeParam : stats.getActiveSpells()) + { + for(const auto& activeEffect : activeParam.getEffects()) + { + if(!(activeEffect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + if(activeEffect.mEffectId == ESM::MagicEffect::Reflect) + { + if(canReflect && Misc::Rng::roll0to99(prng) < activeEffect.mMagnitude) + { + return MWMechanics::MagicApplicationResult::REFLECTED; + } + } + else if(activeEffect.mEffectId == ESM::MagicEffect::SpellAbsorption) + { + if(canAbsorb && Misc::Rng::roll0to99(prng) < activeEffect.mMagnitude) + { + absorbSpell(spellParams.getId(), caster, target); + return MWMechanics::MagicApplicationResult::REMOVED; + } + } + } + } + } + } + // 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); + // 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().getWorld()->getStore().get().search(spellParams.getId()); + float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return MWMechanics::MagicApplicationResult::REMOVED; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + return MWMechanics::MagicApplicationResult::APPLIED; + } + + static const std::map sBoundItemsMap{ + {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, + {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, + {ESM::MagicEffect::BoundCuirass, "sMagicBoundCuirassID"}, + {ESM::MagicEffect::BoundDagger, "sMagicBoundDaggerID"}, + {ESM::MagicEffect::BoundGloves, "sMagicBoundLeftGauntletID"}, + {ESM::MagicEffect::BoundHelm, "sMagicBoundHelmID"}, + {ESM::MagicEffect::BoundLongbow, "sMagicBoundLongbowID"}, + {ESM::MagicEffect::BoundLongsword, "sMagicBoundLongswordID"}, + {ESM::MagicEffect::BoundMace, "sMagicBoundMaceID"}, + {ESM::MagicEffect::BoundShield, "sMagicBoundShieldID"}, + {ESM::MagicEffect::BoundSpear, "sMagicBoundSpearID"} + }; +} + +namespace MWMechanics +{ + +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage, bool& recalculateMagicka) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool godmode = target == getPlayer() && world->getGodModeState(); + switch(effect.mEffectId) + { + case ESM::MagicEffect::CureCommonDisease: + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + break; + case ESM::MagicEffect::CureCorprusDisease: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Corprus); + break; + case ESM::MagicEffect::CurePoison: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::Dispel: + // 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) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(params.getId()); + if (spell && spell->mData.mType == ESM::Spell::ST_Spell) + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return Misc::Rng::roll0to99(prng) < magnitude; + } + } + return false; + }, target); + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + auto marker = (effect.mEffectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; + world->teleportToClosestMarker(target, marker); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_end"); + if (fx) + { + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Mark: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + world->getPlayer().markPosition(target.getCell(), target.getRefData().getPosition()); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Recall: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + MWWorld::CellStore* markedCell = nullptr; + ESM::Position markedPosition; + + world->getPlayer().getMarkedPosition(markedCell, markedPosition); + if (markedCell) + { + MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, markedPosition, false); + action.execute(target); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(caster.isEmpty() || !caster.getClass().isActor() || target == getPlayer() || (effect.mEffectId == ESM::MagicEffect::CommandCreature) == target.getClass().isNpc()) + invalid = true; + else if(effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) + { + MWMechanics::AiFollow package(caster, true); + target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(target.getClass().hasInventoryStore(target)) + { + auto& store = target.getClass().getInventoryStore(target); + store.unequipAll(target); + } + else + invalid = true; + break; + case ESM::MagicEffect::TurnUndead: + if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(AiSetting::Flee); + stat.setModifier(static_cast(stat.getModifier() + effect.mMagnitude)); + creatureStats.setAiSetting(AiSetting::Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, AiSetting::Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, AiSetting::Fight, -effect.mMagnitude, invalid); + if(!invalid && effect.mMagnitude > 0) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + creatureStats.getAiSequence().stopCombat(); + } + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, AiSetting::Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, AiSetting::Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + if(!target.isInCell()) + invalid = true; + else + effect.mArg = summonCreature(effect.mEffectId, target); + break; + case ESM::MagicEffect::BoundGloves: + if(!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + addBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + // left gauntlet added below + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + if(!target.getClass().hasInventoryStore(target)) + invalid = true; + else + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + addBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::DamageHealth: + case ESM::MagicEffect::Poison: + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + if(!godmode) + { + int index = 0; + if(effect.mEffectId == ESM::MagicEffect::DamageMagicka) + index = 1; + else if(effect.mEffectId == ESM::MagicEffect::DamageFatigue) + index = 2; + // Damage "Dynamic" abilities reduce the base value + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, index, -effect.mMagnitude); + else + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + adjustDynamicStat(target, index, -effect.mMagnitude, index == 2 && uncappedDamageFatigue); + if(index == 0) + receivedMagicDamage = true; + } + } + break; + case ESM::MagicEffect::DamageAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DamageSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + // Damage Skill abilities reduce base skill :todd: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + SkillValue& skill = npcStats.getSkill(effect.mArg); + // Damage Skill abilities reduce base skill :todd: + skill.setBase(std::max(skill.getBase() - effect.mMagnitude, 0.f)); + } + else + damageSkill(target, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::RestoreAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreSkill: + if(!target.getClass().isNpc()) + invalid = true; + else + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::RestoreHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::SunDamage: + { + // isInCell shouldn't be needed, but updateActor called during game start + if (!target.isInCell() || !target.getCell()->isExterior() || godmode) + break; + float time = world->getTimeStamp().getHour(); + float timeDiff = std::clamp(std::abs(time - 13.f), 0.f, 7.f); + float damageScale = 1.f - timeDiff / 7.f; + // When cloudy, the sun damage effect is halved + static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); + + int weather = world->getCurrentWeather(); + if (weather > 1) + damageScale *= fMagicSunBlockedMult; + float damage = effect.mMagnitude * damageScale; + adjustDynamicStat(target, 0, -damage); + if (damage > 0.f) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + if(!godmode) + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; + adjustDynamicStat(target, index, -effect.mMagnitude, uncappedDamageFatigue && index == 2); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude, false, true); + break; + case ESM::MagicEffect::DrainAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() + effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + damageSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + // Abilities affect base stats, but not for drain + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() + effect.mMagnitude); + } + else + fortifySkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + recalculateMagicka = true; + break; + case ESM::MagicEffect::AbsorbHealth: + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + if(!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::AbsorbHealth; + adjustDynamicStat(target, index, -effect.mMagnitude); + if(!caster.isEmpty()) + adjustDynamicStat(caster, index, effect.mMagnitude); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::AbsorbAttribute: + if(!godmode) + { + damageAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + damageSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::DisintegrateArmor: + { + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (godmode) + break; + static const std::array priorities + { + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots + }; + for (const int priority : priorities) + { + if (disintegrateSlot(target, priority, effect.mMagnitude)) + break; + } + break; + } + case ESM::MagicEffect::DisintegrateWeapon: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (!godmode) + disintegrateSlot(target, MWWorld::InventoryStore::Slot_CarriedRight, effect.mMagnitude); + break; + } +} + +bool shouldRemoveEffect(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect) +{ + const auto world = MWBase::Environment::get().getWorld(); + switch(effect.mEffectId) + { + case ESM::MagicEffect::Levitate: + { + if(!world->isLevitationEnabled()) + { + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + return true; + } + break; + } + case ESM::MagicEffect::Recall: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::AlmsiviIntervention: + { + return effect.mFlags & ESM::ActiveEffect::Flag_Applied; + } + case ESM::MagicEffect::WaterWalking: + { + if (target.getClass().isPureWaterCreature(target) && world->isSwimming(target)) + return true; + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied) + break; + if (!world->isWaterWalkingCastableOnTarget(target)) + { + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidEffect}"); + return true; + } + break; + } + } + return false; +} + +MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool invalid = false; + bool receivedMagicDamage = false; + bool recalculateMagicka = false; + if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) + { + spellParams.worsen(); + for(auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect)) + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, recalculateMagicka); + } + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + return MagicApplicationResult::APPLIED; + } + else if(shouldRemoveEffect(target, effect)) + { + onMagicEffectRemoved(target, spellParams, effect); + return MagicApplicationResult::REMOVED; + } + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + { + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return MagicApplicationResult::APPLIED; + } + else if(!dt) + return MagicApplicationResult::APPLIED; + } + if(effect.mEffectId == ESM::MagicEffect::Lock) + { + if(target.getClass().canLock(target)) + { + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + 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 + { + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); + target.getCellRef().lock(magnitude); + } + } + else + invalid = true; + } + else if(effect.mEffectId == ESM::MagicEffect::Open) + { + if(target.getClass().canLock(target)) + { + // Use the player instead of the caster for vanilla crime compatibility + MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); + + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() <= magnitude) + { + if(target.getCellRef().getLockLevel() > 0) + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); + } + target.getCellRef().unlock(); + } + else + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + } + } + else + invalid = true; + } + else if(!target.getClass().isActor()) + { + invalid = true; + } + else + { + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + { + MagicApplicationResult result = applyProtections(target, caster, spellParams, effect, magicEffect); + if(result != MagicApplicationResult::APPLIED) + return result; + } + float oldMagnitude = 0.f; + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + 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(effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() && target.getType() == ESM::Creature::sRecordId && target.get()->mBase->mData.mSoul == 0 && caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); + } + float magnitude = roll(effect); + //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here + effect.mMagnitude = magnitude; + if(!(magicEffect->mData.mFlags & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) + { + if(effect.mDuration != 0) + { + float mult = dt; + if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + mult = std::min(effect.mTimeLeft, dt); + effect.mMagnitude *= mult; + } + if(effect.mMagnitude == 0) + { + effect.mMagnitude = oldMagnitude; + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + effect.mTimeLeft -= dt; + return MagicApplicationResult::APPLIED; + } + } + if(effect.mEffectId == ESM::MagicEffect::Corprus) + spellParams.worsen(); + else + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, recalculateMagicka); + effect.mMagnitude = magnitude; + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); + } + effect.mTimeLeft -= dt; + if(invalid) + { + effect.mTimeLeft = 0; + effect.mFlags |= ESM::ActiveEffect::Flag_Remove; + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } + else + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + if (receivedMagicDamage && target == getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + if(recalculateMagicka) + target.getClass().getCreatureStats(target).recalculateMagicka(); + return MagicApplicationResult::APPLIED; +} + +void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + bool invalid; + switch(effect.mEffectId) + { + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); + seq.erasePackageIf([&](const auto& package) + { + return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(package.get())->isCommanded(); + }); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + target.getClass().getInventoryStore(target).autoEquip(target); + break; + case ESM::MagicEffect::TurnUndead: + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(AiSetting::Flee); + stat.setModifier(static_cast(stat.getModifier() - effect.mMagnitude)); + creatureStats.setAiSetting(AiSetting::Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, AiSetting::Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, AiSetting::Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, AiSetting::Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, AiSetting::Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + { + if(effect.mArg != -1) + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, effect.mArg); + auto& summons = target.getClass().getCreatureStats(target).getSummonedCreatureMap(); + auto [begin, end] = summons.equal_range(effect.mEffectId); + for(auto it = begin; it != end; ++it) + { + if(it->second == effect.mArg) + { + summons.erase(it); + break; + } + } + } + break; + case ESM::MagicEffect::BoundGloves: + removeBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + removeBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::DrainHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude, true); + break; + case ESM::MagicEffect::DrainAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() - effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() - effect.mMagnitude); + } + else + fortifySkill(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).recalculateMagicka(); + break; + case ESM::MagicEffect::AbsorbAttribute: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::Corprus: + { + int worsenings = spellParams.getWorsenings(); + spellParams.resetWorsenings(); + if(worsenings > 0) + { + for(const auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect, true)) + { + for(int i = 0; i < worsenings; i++) + removeMagicEffect(target, spellParams, otherEffect); + } + } + } + //Note that we remove the effects, but keep the params + target.getClass().getCreatureStats(target).getActiveSpells().purge([&spellParams] (const ActiveSpells::ActiveSpellParams& params, const auto&) + { + return &spellParams == ¶ms; + }, target); + } + break; + } +} + +void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return; + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMagnitude)); + removeMagicEffect(target, spellParams, effect); + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } +} + +} diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp new file mode 100644 index 0000000000..2861d0d64a --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -0,0 +1,25 @@ +#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H +#define GAME_MWMECHANICS_SPELLEFFECTS_H + +#include "activespells.hpp" + +#include "../mwworld/ptr.hpp" + +// These functions should probably be split up into separate Lua functions for each magic effect when magic is dehardcoded. +// That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion. + +namespace MWMechanics +{ + enum class MagicApplicationResult + { + APPLIED, REMOVED, REFLECTED + }; + + // Applies a tick of a single effect. Returns true if the effect should be removed immediately + MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + + // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce + void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); +} + +#endif diff --git a/apps/openmw/mwmechanics/spelllist.cpp b/apps/openmw/mwmechanics/spelllist.cpp index 891b286196..f90bb22f05 100644 --- a/apps/openmw/mwmechanics/spelllist.cpp +++ b/apps/openmw/mwmechanics/spelllist.cpp @@ -2,8 +2,7 @@ #include -#include -#include +#include #include "spells.hpp" @@ -71,7 +70,7 @@ namespace MWMechanics auto& id = spell->mId; bool changed = withBaseRecord([&] (auto& spells) { - for(auto it : spells) + for(const auto& it : spells) { if(Misc::StringUtils::ciEqual(id, it)) return false; @@ -153,23 +152,23 @@ namespace MWMechanics void SpellList::addListener(Spells* spells) { - for(const auto ptr : mListeners) - { - if(ptr == spells) - return; - } + if (std::find(mListeners.begin(), mListeners.end(), spells) != mListeners.end()) + return; mListeners.push_back(spells); } void SpellList::removeListener(Spells* spells) { - for(auto it = mListeners.begin(); it != mListeners.end(); it++) - { - if(*it == spells) - { - mListeners.erase(it); - break; - } - } + const auto it = std::find(mListeners.begin(), mListeners.end(), spells); + if (it != mListeners.end()) + mListeners.erase(it); + } + + void SpellList::updateListener(Spells* before, Spells* after) + { + const auto it = std::find(mListeners.begin(), mListeners.end(), before); + if (it == mListeners.end()) + return mListeners.push_back(after); + *it = after; } } diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index b01722fe81..b43a0bf14f 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -7,9 +7,7 @@ #include #include -#include - -#include "magiceffects.hpp" +#include namespace ESM { @@ -18,12 +16,6 @@ namespace ESM namespace MWMechanics { - struct SpellParams - { - std::map mEffectRands; // - std::set mPurgedEffects; // indices of purged effects - }; - class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. @@ -61,6 +53,8 @@ namespace MWMechanics void removeListener(Spells* spells); + void updateListener(Spells* before, Spells* after); + const std::vector getSpells() const; }; } diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 81658193d6..245bd4fcdb 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -1,9 +1,9 @@ #include "spellpriority.hpp" #include "weaponpriority.hpp" -#include -#include -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -17,7 +17,6 @@ #include "creaturestats.hpp" #include "spellresistance.hpp" #include "weapontype.hpp" -#include "combat.hpp" #include "summoning.hpp" #include "spellutil.hpp" @@ -32,21 +31,20 @@ namespace // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. if (effectFilter == -1) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->getId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - int effectId = effectIt->mEffectId; + int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway + if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) @@ -65,19 +63,28 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->first != spellId) + if (it->getId() != spellId) continue; - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - if (effectIt->mDuration > duration) - duration = effectIt->mDuration; + if (effect.mDuration > duration) + duration = effect.mDuration; } } return duration; } + + bool isSpellActive(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const std::string& id) + { + 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 && Misc::StringUtils::ciEqual(spell.getId(), id); + }) != active.end(); + } } namespace MWMechanics @@ -99,7 +106,7 @@ namespace MWMechanics float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) { - if (item.getTypeName() != typeid(ESM::Potion).name()) + if (item.getType() != ESM::Potion::sRecordId) return 0.f; const ESM::Potion* potion = item.get()->mBase; @@ -108,8 +115,6 @@ namespace MWMechanics float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy) { - const CreatureStats& stats = actor.getClass().getCreatureStats(actor); - float successChance = MWMechanics::getSpellSuccessChance(spell, actor); if (successChance == 0.f) return 0.f; @@ -128,9 +133,9 @@ namespace MWMechanics // Spells don't stack, so early out if the spell is still active on the target int types = getRangeTypes(spell->mEffects); - if ((types & Self) && stats.getActiveSpells().isSpellActive(spell->mId)) + if ((types & Self) && isSpellActive(actor, actor, spell->mId)) return 0.f; - if ( ((types & Touch) || (types & Target)) && enemy.getClass().getCreatureStats(enemy).getActiveSpells().isSpellActive(spell->mId)) + if ( ((types & Touch) || (types & Target)) && isSpellActive(actor, enemy, spell->mId)) return 0.f; return rateEffects(spell->mEffects, actor, enemy) * (successChance / 100.f); @@ -244,7 +249,7 @@ namespace MWMechanics return 0.f; // Enemy doesn't attack - if (stats.getDrawState() != MWMechanics::DrawState_Weapon) + if (stats.getDrawState() != MWMechanics::DrawState::Weapon) return 0.f; break; @@ -265,7 +270,7 @@ namespace MWMechanics return 0.f; // Enemy doesn't cast spells - if (stats.getDrawState() != MWMechanics::DrawState_Spell) + if (stats.getDrawState() != MWMechanics::DrawState::Spell) return 0.f; break; @@ -283,7 +288,7 @@ namespace MWMechanics return 0.f; // Enemy doesn't cast spells - if (stats.getDrawState() != MWMechanics::DrawState_Spell) + if (stats.getDrawState() != MWMechanics::DrawState::Spell) return 0.f; break; } @@ -371,6 +376,24 @@ namespace MWMechanics return 0.f; break; + case ESM::MagicEffect::BoundShield: + if(!actor.getClass().hasInventoryStore(actor)) + return 0.f; + else if(!actor.getClass().isNpc()) + { + // If the actor is an NPC they can benefit from the armor rating, otherwise check if we've got a one-handed weapon to use with the shield + const auto& store = actor.getClass().getInventoryStore(actor); + auto oneHanded = std::find_if(store.cbegin(MWWorld::ContainerStore::Type_Weapon), store.cend(), [](const MWWorld::ConstPtr& weapon) + { + if(weapon.getClass().getItemHealth(weapon) <= 0.f) + return false; + short type = weapon.get()->mBase->mData.mType; + return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded); + }); + if(oneHanded == store.cend()) + return 0.f; + } + break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: @@ -555,6 +578,13 @@ namespace MWMechanics if (!creatureStats.getSummonedCreatureMap().empty()) return 0.f; } + if(effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves) + { + // While rateSpell prevents actors from recasting the same spell, it doesn't prevent them from casting different spells with the same effect. + // Multiple instances of the same bound item don't stack so if the effect is already active, rate it as useless. + if(actor.getClass().getCreatureStats(actor).getMagicEffects().get(effect.mEffectID).getMagnitude() > 0.f) + return 0.f; + } // Underwater casting not possible if (effect.mRange == ESM::RT_Target) diff --git a/apps/openmw/mwmechanics/spellresistance.cpp b/apps/openmw/mwmechanics/spellresistance.cpp index 1edf140915..bb39255b43 100644 --- a/apps/openmw/mwmechanics/spellresistance.cpp +++ b/apps/openmw/mwmechanics/spellresistance.cpp @@ -51,7 +51,8 @@ namespace MWMechanics if (castChance > 0) x *= 50 / castChance; - float roll = Misc::Rng::rollClosedProbability() * 100; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + float roll = Misc::Rng::rollClosedProbability(prng) * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) roll -= resistance; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index d292c015d6..e3e8e849e4 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -1,8 +1,8 @@ #include "spells.hpp" #include -#include -#include +#include +#include #include #include @@ -20,53 +20,31 @@ namespace MWMechanics { Spells::Spells() - : mSpellsChanged(false) { } - std::map::const_iterator Spells::begin() const + Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), + mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers) { - return mSpells.begin(); + if(mSpellList) + mSpellList->addListener(this); } - std::map::const_iterator Spells::end() const + Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)) { - return mSpells.end(); + if (mSpellList) + mSpellList->updateListener(&spells, this); } - void Spells::rebuildEffects() const + std::vector::const_iterator Spells::begin() const { - mEffects = MagicEffects(); - mSourcedEffects.clear(); - - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - - if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || - spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) - { - int i=0; - for (const auto& effect : spell->mEffects.mList) - { - if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) - { - ++i; - continue; // effect was purged - } - - float random = 1.f; - if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) - random = iter.second.mEffectRands.at(i); - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; - mEffects.add (effect, magnitude); - mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); + return mSpells.begin(); + } - ++i; - } - } - } + std::vector::const_iterator Spells::end() const + { + return mSpells.end(); } bool Spells::hasSpell(const std::string &spell) const @@ -76,7 +54,7 @@ namespace MWMechanics bool Spells::hasSpell(const ESM::Spell *spell) const { - return mSpells.find(spell) != mSpells.end(); + return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) @@ -91,29 +69,8 @@ namespace MWMechanics void Spells::addSpell(const ESM::Spell* spell) { - if (mSpells.find (spell)==mSpells.end()) - { - std::map random; - - // Determine the random magnitudes (unless this is a castable spell, in which case - // they will be determined when the spell is cast) - if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) - { - for (unsigned int i=0; imEffects.mList.size();++i) - { - if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) - { - int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; - random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - } - } - } - - SpellParams params; - params.mEffectRands = random; - mSpells.emplace(spell, params); - mSpellsChanged = true; - } + if (!hasSpell(spell)) + mSpells.emplace_back(spell); } void Spells::remove (const std::string& spellId) @@ -128,27 +85,14 @@ namespace MWMechanics void Spells::removeSpell(const ESM::Spell* spell) { - const auto it = mSpells.find(spell); + const auto it = std::find(mSpells.begin(), mSpells.end(), spell); if(it != mSpells.end()) - { mSpells.erase(it); - mSpellsChanged = true; - } - } - - MagicEffects Spells::getMagicEffects() const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); - mSpellsChanged = true; } void Spells::clear(bool modifyBase) @@ -168,41 +112,23 @@ namespace MWMechanics return mSelectedSpell; } - bool Spells::isSpellActive(const std::string &id) const + bool Spells::hasSpellType(const ESM::Spell::SpellType type) const { - if (id.empty()) - return false; - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if (spell && hasSpell(spell)) - { - auto type = spell->mData.mType; - return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); - } - - return false; - } - - bool Spells::hasDisease(const ESM::Spell::SpellType type) const - { - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - if (spell->mData.mType == type) - return true; - } - - return false; + auto it = std::find_if(std::begin(mSpells), std::end(mSpells), [=](const ESM::Spell* spell) + { + return spell->mData.mType == type; + }); + return it != std::end(mSpells); } bool Spells::hasCommonDisease() const { - return hasDisease(ESM::Spell::ST_Disease); + return hasSpellType(ESM::Spell::ST_Disease); } bool Spells::hasBlightDisease() const { - return hasDisease(ESM::Spell::ST_Blight); + return hasSpellType(ESM::Spell::ST_Blight); } void Spells::purge(const SpellFilter& filter) @@ -210,12 +136,11 @@ namespace MWMechanics std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { - const ESM::Spell *spell = iter->first; + const ESM::Spell *spell = *iter; if (filter(spell)) { - mSpells.erase(iter++); + iter = mSpells.erase(iter); purged.push_back(spell->mId); - mSpellsChanged = true; } else ++iter; @@ -244,43 +169,6 @@ namespace MWMechanics purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } - void Spells::removeEffects(const std::string &id) - { - if (isSpellActive(id)) - { - for (auto& spell : mSpells) - { - if (spell.first == SpellList::getSpell(id)) - { - for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) - { - spell.second.mPurgedEffects.insert(i); - } - } - } - - mSpellsChanged = true; - } - } - - void Spells::visitEffectSources(EffectSourceVisitor &visitor) const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - - for (const auto& it : mSourcedEffects) - { - const ESM::Spell * spell = it.first; - for (const auto& effectIt : it.second) - { - // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here - visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); - } - } - } - bool Spells::hasCorprusEffect(const ESM::Spell *spell) { for (const auto& effectIt : spell->mEffects.mList) @@ -293,72 +181,37 @@ namespace MWMechanics return false; } - void Spells::purgeEffect(int effectId) - { - for (auto& spellIt : mSpells) - { - int i = 0; - for (auto& effectIt : spellIt.first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt.second.mPurgedEffects.insert(i); - mSpellsChanged = true; - } - ++i; - } - } - } - - void Spells::purgeEffect(int effectId, const std::string & sourceId) - { - // Effect source may be not a spell - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); - if (spell == nullptr) - return; - - auto spellIt = mSpells.find(spell); - if (spellIt == mSpells.end()) - return; - - int index = 0; - for (auto& effectIt : spellIt->first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt->second.mPurgedEffects.insert(index); - mSpellsChanged = true; - } - ++index; - } - } - bool Spells::canUsePower(const ESM::Spell* spell) const { - const auto it = mUsedPowers.find(spell); + const auto it = std::find_if(std::begin(mUsedPowers), std::end(mUsedPowers), [&](auto& pair) { return pair.first == spell; }); return it == mUsedPowers.end() || it->second + 24 <= MWBase::Environment::get().getWorld()->getTimeStamp(); } void Spells::usePower(const ESM::Spell* spell) { - mUsedPowers[spell] = MWBase::Environment::get().getWorld()->getTimeStamp(); + // Updates or inserts a new entry with the current timestamp. + const auto it = std::find_if(std::begin(mUsedPowers), std::end(mUsedPowers), [&](auto& pair) { return pair.first == spell; }); + const auto timestamp = MWBase::Environment::get().getWorld()->getTimeStamp(); + if (it == mUsedPowers.end()) + mUsedPowers.emplace_back(spell, timestamp); + else + it->second = timestamp; } void Spells::readState(const ESM::SpellState &state, CreatureStats* creatureStats) { const auto& baseSpells = mSpellList->getSpells(); - for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const std::string& id : state.mSpells) { // Discard spells that are no longer available due to changed content files - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell) { - mSpells[spell].mEffectRands = it->second.mEffectRands; - mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; + addSpell(spell); - if (it->first == state.mSelectedSpell) - mSelectedSpell = it->first; + if (id == state.mSelectedSpell) + mSelectedSpell = id; } } // Add spells from the base record @@ -374,34 +227,9 @@ namespace MWMechanics const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; - mUsedPowers[spell] = MWWorld::TimeStamp(it->second); - } - - for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); - if (!spell) - continue; - - CorprusStats stats; - - int worsening = state.mCorprusSpells.at(it->first).mWorsenings; - - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = worsening; - } - stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - - creatureStats->addCorprusSpell(it->first, stats); + mUsedPowers.emplace_back(spell, MWWorld::TimeStamp(it->second)); } - mSpellsChanged = true; - // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. for (std::map >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) @@ -440,16 +268,13 @@ namespace MWMechanics void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); - for (const auto& it : mSpells) + for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record - if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || - std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) + if((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) || + std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end()) { - ESM::SpellState::SpellParams params; - params.mEffectRands = it.second.mEffectRands; - params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.emplace(it.first->mId, params); + state.mSpells.emplace_back(spell->mId); } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 2f4049d2e0..25dab0c9d4 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include "../mwworld/timestamp.hpp" @@ -30,19 +29,14 @@ namespace MWMechanics class Spells { std::shared_ptr mSpellList; - std::map mSpells; + std::vector mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; - std::map mUsedPowers; + std::vector> mUsedPowers; - mutable bool mSpellsChanged; - mutable MagicEffects mEffects; - mutable std::map mSourcedEffects; - void rebuildEffects() const; - - bool hasDisease(const ESM::Spell::SpellType type) const; + bool hasSpellType(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); void purge(const SpellFilter& filter); @@ -53,17 +47,16 @@ namespace MWMechanics friend class SpellList; public: - using TIterator = std::map::const_iterator; - Spells(); + Spells(const Spells&); + + Spells(Spells&& spells); + ~Spells(); static bool hasCorprusEffect(const ESM::Spell *spell); - void purgeEffect(int effectId); - void purgeEffect(int effectId, const std::string & sourceId); - bool canUsePower (const ESM::Spell* spell) const; void usePower (const ESM::Spell* spell); @@ -72,9 +65,9 @@ namespace MWMechanics void purgeCorprusDisease(); void purgeCurses(); - TIterator begin() const; + std::vector::const_iterator begin() const; - TIterator end() const; + std::vector::const_iterator end() const; bool hasSpell(const std::string& spell) const; bool hasSpell(const ESM::Spell* spell) const; @@ -89,9 +82,6 @@ namespace MWMechanics ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). - MagicEffects getMagicEffects() const; - ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. - void clear(bool modifyBase = false); ///< Remove all spells of al types. @@ -101,17 +91,10 @@ namespace MWMechanics const std::string getSelectedSpell() const; ///< May return an empty string. - bool isSpellActive(const std::string& id) const; - ///< Are we under the effects of the given spell ID? - bool hasCommonDisease() const; bool hasBlightDisease() const; - void removeEffects(const std::string& id); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - void readState (const ESM::SpellState& state, CreatureStats* creatureStats); void writeState (ESM::SpellState& state) const; diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 8b2f5c46c8..7fa9687189 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -3,7 +3,6 @@ #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" @@ -24,7 +23,7 @@ namespace MWMechanics return schoolSkillArray.at(school); } - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, const EffectCostMethod method) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (!magicEffect) @@ -39,14 +38,43 @@ namespace MWMechanics duration = std::max(1, duration); static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); + int durationOffset = 0; + int minArea = 0; + if (method == EffectCostMethod::PlayerSpell) { + durationOffset = 1; + minArea = 1; + } + 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 *= durationOffset + duration; + x += 0.05 * std::max(minArea, effect.mArea) * magicEffect->mData.mBaseCost; return x * fEffectCostMult; } + int calcSpellCost (const ESM::Spell& spell) + { + if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) + return spell.mData.mCost; + + float cost = 0; + + for (const ESM::ENAMstruct& effect : spell.mEffects.mList) + { + float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect)); + + // 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) + effectCost *= 1.5; + + cost += effectCost; + } + + return std::round(cost); + } + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) { /* @@ -97,7 +125,7 @@ namespace MWMechanics float actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); - float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); + float castChance = (lowestSkill - calcSpellCost(*spell) + 0.2f * actorWillpower + 0.1f * actorLuck); return castChance; } @@ -123,7 +151,7 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; - if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost) + if (checkMagicka && calcSpellCost(*spell) > 0 && stats.getMagicka().getCurrent() < calcSpellCost(*spell)) return 0; if (spell->mData.mFlags & ESM::Spell::F_Always) @@ -133,7 +161,10 @@ namespace MWMechanics float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + if (cap) + return std::clamp(castChance, 0.f, 100.f); + + return std::max(castChance, 0.f); } float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) @@ -167,48 +198,4 @@ namespace MWMechanics const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); return spell && spellIncreasesSkill(spell); } - - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer) - { - switch (effectId) - { - case ESM::MagicEffect::Levitate: - { - if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); - return false; - } - break; - } - case ESM::MagicEffect::Soultrap: - { - if (!target.getClass().isNpc() // no messagebox for NPCs - && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); - return true; // must still apply to get visual effect and have target regard it as attack - } - break; - } - case ESM::MagicEffect::WaterWalking: - { - if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target)) - return false; - - MWBase::World *world = MWBase::Environment::get().getWorld(); - - if (!world->isWaterWalkingCastableOnTarget(target)) - { - if (castByPlayer && caster == target) - MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}"); - return false; - } - break; - } - } - return true; - } } diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp index 865a9126e7..571e02d166 100644 --- a/apps/openmw/mwmechanics/spellutil.hpp +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -1,7 +1,7 @@ #ifndef MWMECHANICS_SPELLUTIL_H #define MWMECHANICS_SPELLUTIL_H -#include +#include namespace ESM { @@ -19,7 +19,13 @@ namespace MWMechanics { ESM::Skill::SkillEnum spellSchoolToSkill(int school); - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); + enum class EffectCostMethod { + GameSpell, + PlayerSpell, + }; + + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr, const EffectCostMethod method = EffectCostMethod::GameSpell); + int calcSpellCost (const ESM::Spell& spell); int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); @@ -42,9 +48,6 @@ namespace MWMechanics /// Get whether or not the given spell contributes to skill progress. bool spellIncreasesSkill(const ESM::Spell* spell); bool spellIncreasesSkill(const std::string& spellId); - - /// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure. - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer); } #endif diff --git a/apps/openmw/mwmechanics/stat.cpp b/apps/openmw/mwmechanics/stat.cpp index 7f71cf9b18..eacfca98ae 100644 --- a/apps/openmw/mwmechanics/stat.cpp +++ b/apps/openmw/mwmechanics/stat.cpp @@ -1,176 +1,45 @@ #include "stat.hpp" -#include +#include namespace MWMechanics { template - Stat::Stat() : mBase (0), mModified (0), mCurrentModified (0) {} + Stat::Stat() : mBase (0), mModifier (0) {} template - Stat::Stat(T base) : mBase (base), mModified (base), mCurrentModified (base) {} - template - Stat::Stat(T base, T modified) : mBase (base), mModified (modified), mCurrentModified (modified) {} - - template - const T& Stat::getBase() const - { - return mBase; - } - - template - T Stat::getModified() const - { - return std::max(static_cast(0), mModified); - } - - template - T Stat::getCurrentModified() const - { - return mCurrentModified; - } - - template - T Stat::getModifier() const - { - return mModified-mBase; - } - - template - T Stat::getCurrentModifier() const - { - return mCurrentModified - mModified; - } - - template - void Stat::set (const T& value) - { - T diff = value - mBase; - mBase = mModified = value; - mCurrentModified += diff; - } - - template - void Stat::setBase (const T& value) - { - T diff = value - mBase; - mBase = value; - mModified += diff; - mCurrentModified += diff; - } - - template - void Stat::setModified (T value, const T& min, const T& max) - { - T diff = value - mModified; - - if (mBase+diffmax) - { - value = max + (mModified - mBase); - diff = value - mModified; - } - - mModified = value; - mBase += diff; - mCurrentModified += diff; - } - - template - void Stat::setCurrentModified(T value) - { - mCurrentModified = value; - } - - template - void Stat::setModifier (const T& modifier) - { - mModified = mBase + modifier; - } + Stat::Stat(T base, T modified) : mBase (base), mModifier (modified) {} template - void Stat::setCurrentModifier(const T& modifier) + T Stat::getModified(bool capped) const { - mCurrentModified = mModified + modifier; + if(capped) + return std::max({}, mModifier + mBase); + return mModifier + mBase; } template void Stat::writeState (ESM::StatState& state) const { state.mBase = mBase; - state.mMod = mCurrentModified; + state.mMod = mModifier; } template void Stat::readState (const ESM::StatState& state) { mBase = state.mBase; - mModified = state.mBase; - mCurrentModified = state.mMod; + mModifier = state.mMod; } template - DynamicStat::DynamicStat() : mStatic (0), mCurrent (0) {} + DynamicStat::DynamicStat() : mStatic(0, 0), mCurrent(0) {} template - DynamicStat::DynamicStat(T base) : mStatic (base), mCurrent (base) {} + DynamicStat::DynamicStat(T base) : mStatic(base, 0), mCurrent(base) {} template DynamicStat::DynamicStat(T base, T modified, T current) : mStatic(base, modified), mCurrent (current) {} template DynamicStat::DynamicStat(const Stat &stat, T current) : mStatic(stat), mCurrent (current) {} - - template - const T& DynamicStat::getBase() const - { - return mStatic.getBase(); - } - template - T DynamicStat::getModified() const - { - return mStatic.getModified(); - } - template - T DynamicStat::getCurrentModified() const - { - return mStatic.getCurrentModified(); - } - - template - const T& DynamicStat::getCurrent() const - { - return mCurrent; - } - - template - void DynamicStat::set (const T& value) - { - mStatic.set (value); - mCurrent = value; - } - template - void DynamicStat::setBase (const T& value) - { - mStatic.setBase (value); - - if (mCurrent>getModified()) - mCurrent = getModified(); - } - template - void DynamicStat::setModified (T value, const T& min, const T& max) - { - mStatic.setModified (value, min, max); - - if (mCurrent>getModified()) - mCurrent = getModified(); - } - template - void DynamicStat::setCurrentModified(T value) - { - mStatic.setCurrentModified(value); - } template void DynamicStat::setCurrent (const T& value, bool allowDecreaseBelowZero, bool allowIncreaseAboveModified) { @@ -195,22 +64,18 @@ namespace MWMechanics mCurrent = 0; } } - template - void DynamicStat::setModifier (const T& modifier, bool allowCurrentDecreaseBelowZero) - { - T diff = modifier - mStatic.getModifier(); - mStatic.setModifier (modifier); - setCurrent (getCurrent()+diff, allowCurrentDecreaseBelowZero); - } template - void DynamicStat::setCurrentModifier(const T& modifier, bool allowCurrentDecreaseBelowZero) + T DynamicStat::getRatio(bool nanIsZero) const { - T diff = modifier - mStatic.getCurrentModifier(); - mStatic.setCurrentModifier(modifier); - - // The (modifier > 0) check here allows increase over modified only if the modifier is positive (a fortify effect is active). - setCurrent (getCurrent() + diff, allowCurrentDecreaseBelowZero, (modifier > 0)); + T modified = getModified(); + if(modified == T{}) + { + if(nanIsZero) + return modified; + return {1}; + } + return getCurrent() / modified; } template @@ -244,24 +109,30 @@ namespace MWMechanics return mModifier; } - void AttributeValue::setBase(float base) + void AttributeValue::setBase(float base, bool clearModifier) { mBase = base; + if(clearModifier) + { + mModifier = 0.f; + mDamage = 0.f; + } } void AttributeValue::setModifier(float mod) { - mModifier = mod; + if(mod < 0) + { + mModifier = 0.f; + mDamage -= mod; + } + else + mModifier = mod; } void AttributeValue::damage(float damage) { - float threshold = mBase + mModifier; - - if (mDamage + damage > threshold) - mDamage = threshold; - else - mDamage += damage; + mDamage += damage; } void AttributeValue::restore(float amount) { diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index 5f49da48e8..1e9bb100d0 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -16,38 +16,22 @@ namespace MWMechanics class Stat { T mBase; - T mModified; - T mCurrentModified; + T mModifier; public: typedef T Type; Stat(); - Stat(T base); Stat(T base, T modified); - const T& getBase() const; + const T& getBase() const { return mBase; }; - T getModified() const; - T getCurrentModified() const; - T getModifier() const; - T getCurrentModifier() const; + T getModified(bool capped = true) const; + T getModifier() const { return mModifier; }; - /// Set base and modified to \a value. - void set (const T& value); + void setBase(const T& value) { mBase = value; }; - /// Set base and adjust modified accordingly. - void setBase (const T& value); - - /// Set modified value and adjust base accordingly. - void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); - - /// Set "current modified," used for drain and fortify. Unlike the regular modifier - /// this just adds and subtracts from the current value without changing the maximum. - void setCurrentModified(T value); - - void setModifier (const T& modifier); - void setCurrentModifier (const T& modifier); + void setModifier(const T& modifier) { mModifier = modifier; }; void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); @@ -57,7 +41,7 @@ namespace MWMechanics inline bool operator== (const Stat& left, const Stat& right) { return left.getBase()==right.getBase() && - left.getModified()==right.getModified(); + left.getModifier()==right.getModifier(); } template @@ -80,27 +64,18 @@ namespace MWMechanics DynamicStat(T base, T modified, T current); DynamicStat(const Stat &stat, T current); - const T& getBase() const; - T getModified() const; - T getCurrentModified() const; - const T& getCurrent() const; - - /// Set base, modified and current to \a value. - void set (const T& value); + const T& getBase() const { return mStatic.getBase(); }; + T getModified(bool capped = true) const { return mStatic.getModified(capped); }; + const T& getCurrent() const { return mCurrent; }; + T getRatio(bool nanIsZero = true) const; - /// Set base and adjust modified accordingly. - void setBase (const T& value); - - /// Set modified value and adjust base accordingly. - void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); - - /// Set "current modified," used for drain and fortify. Unlike the regular modifier - /// this just adds and subtracts from the current value without changing the maximum. - void setCurrentModified(T value); + /// Set base and adjust current accordingly. + void setBase(const T& value) { mStatic.setBase(value); }; void setCurrent (const T& value, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false); - void setModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero=false); - void setCurrentModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero = false); + + T getModifier() const { return mStatic.getModifier(); } + void setModifier(T value) { mStatic.setModifier(value); } void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); @@ -110,7 +85,7 @@ namespace MWMechanics inline bool operator== (const DynamicStat& left, const DynamicStat& right) { return left.getBase()==right.getBase() && - left.getModified()==right.getModified() && + left.getModifier()==right.getModifier() && left.getCurrent()==right.getCurrent(); } @@ -133,13 +108,14 @@ namespace MWMechanics float getBase() const; float getModifier() const; - void setBase(float base); + void setBase(float base, bool clearModifier = false); void setModifier(float mod); // Maximum attribute damage is limited to the modified value. - // Note: I think MW applies damage directly to mModified, since you can also - // "restore" drained attributes. We need to rewrite the magic effect system to support this. + // Note: MW applies damage directly to mModified, however it does track how much + // a damaged attribute that has been fortified beyond its base can be restored. + // Getting rid of mDamage would require calculating its value by ignoring active effects when restoring void damage(float damage); void restore(float amount); diff --git a/apps/openmw/mwmechanics/steering.hpp b/apps/openmw/mwmechanics/steering.hpp index f305a6961c..99fa1387db 100644 --- a/apps/openmw/mwmechanics/steering.hpp +++ b/apps/openmw/mwmechanics/steering.hpp @@ -16,9 +16,11 @@ namespace MWMechanics // Max rotating speed, radian/sec inline float getAngularVelocity(const float actorSpeed) { - const float baseAngluarVelocity = 10; + constexpr float degreesPerFrame = 15.f; + constexpr int framesPerSecond = 60; + const float baseAngularVelocity = osg::DegreesToRadians(degreesPerFrame * framesPerSecond); const float baseSpeed = 200; - return baseAngluarVelocity * std::max(actorSpeed / baseSpeed, 1.0f); + return baseAngularVelocity * std::max(actorSpeed / baseSpeed, 1.0f); } /// configure rotation settings for an actor to reach this target angle (eventually) diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 0f699ccade..0cf2e26f47 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -1,6 +1,7 @@ #include "summoning.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -60,107 +61,91 @@ namespace MWMechanics return std::string(); } - UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) - : mActor(actor) + int summonCreature(int effectId, const MWWorld::Ptr& summoner) { - } - - void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) - { - if (isSummoningEffect(key.mId) && magnitude > 0) - { - mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); - } - } - - void UpdateSummonedCreatures::process(bool cleanup) - { - MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - - for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) + std::string creatureID = getSummonedCreature(effectId); + int creatureActorId = -1; + if (!creatureID.empty()) { - bool found = creatureMap.find(*it) != creatureMap.end(); - if (!found) + try { - std::string creatureID = getSummonedCreature(it->mEffectId); - if (!creatureID.empty()) - { - int creatureActorId = -1; - try - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); + auto world = MWBase::Environment::get().getWorld(); + MWWorld::ManualRef ref(world->getStore(), creatureID, 1); - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); + MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - // Make the summoned creature follow its master and help in fights - AiFollow package(mActor); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - creatureActorId = summonedCreatureStats.getActorId(); + // Make the summoned creature follow its master and help in fights + AiFollow package(summoner); + summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + creatureActorId = summonedCreatureStats.getActorId(); - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - } - catch (std::exception& e) + MWRender::Animation* anim = world->getAnimation(placed); + if (anim) + { + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_Start"); + if (fx) { - Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); - // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1, false); } - - creatureMap.emplace(*it, creatureActorId); } } - } - - // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) - { - bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); - if (!found) + catch (std::exception& e) { - // Effect has ended - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); - creatureMap.erase(it++); - continue; + Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); + // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } - ++it; + + summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId); } + return creatureActorId; + } + + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup) + { + MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature); if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (auto it = creatureMap.begin(); it != creatureMap.end(); ) { + if(it->second == -1) + { + // Keep the spell effect active if we failed to spawn anything + it++; + continue; + } MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->second); - if (ptr.isEmpty() || (ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished())) + if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - const ESM::SummonKey& key = it->first; - creatureStats.getActiveSpells().purgeEffect(key.mEffectId, key.mSourceId, key.mEffectIndex); - creatureStats.getSpells().purgeEffect(key.mEffectId, key.mSourceId); - if (mActor.getClass().hasInventoryStore(mActor)) - mActor.getClass().getInventoryStore(mActor).purgeEffect(key.mEffectId, key.mSourceId, false, key.mEffectIndex); - - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); + auto summon = *it; creatureMap.erase(it++); + purgeSummonEffect(summoner, summon); } else ++it; } } + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) + { + auto& creatureStats = summoner.getClass().getCreatureStats(summoner); + creatureStats.getActiveSpells().purge([summon] (const auto& spell, const auto& effect) + { + return effect.mEffectId == summon.first && effect.mArg == summon.second; + }, summoner); + + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); + } } diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index 7e787499e4..091ee98185 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -5,36 +5,21 @@ #include "../mwworld/ptr.hpp" -#include +#include #include "magiceffects.hpp" namespace MWMechanics { - class CreatureStats; - bool isSummoningEffect(int effectId); std::string getSummonedCreature(int effectId); - struct UpdateSummonedCreatures : public EffectSourceVisitor - { - UpdateSummonedCreatures(const MWWorld::Ptr& actor); - virtual ~UpdateSummonedCreatures() = default; - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - - /// To call after all effect sources have been visited - void process(bool cleanup); - - private: - MWWorld::Ptr mActor; + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - std::set mActiveEffects; - }; + int summonCreature(int effectId, const MWWorld::Ptr& summoner); + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp deleted file mode 100644 index 5056179f8f..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "tickableeffects.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "actorutil.hpp" -#include "npcstats.hpp" - -namespace MWMechanics -{ - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) - { - if (!ptr.getClass().hasInventoryStore(ptr)) - return false; - - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - - return false; - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - if (godmode) - break; - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - if (godmode) - break; - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - if (godmode) - break; - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (!godmode || magnitude <= 0) - { - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - } - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - if (!godmode || magnitude <= 0) - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - if (godmode) - break; - static const std::array priorities - { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - for (const int priority : priorities) - { - if (disintegrateSlot(actor, priority, magnitude)) - break; - } - - break; - } - case ESM::MagicEffect::DisintegrateWeapon: - if (!godmode) - disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); - break; - - case ESM::MagicEffect::SunDamage: - { - // isInCell shouldn't be needed, but updateActor called during game start - if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - if (godmode) - break; - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } -} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp deleted file mode 100644 index ccd42ca19b..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MWMECHANICS_TICKABLEEFFECTS_H -#define MWMECHANICS_TICKABLEEFFECTS_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class CreatureStats; - struct EffectKey; - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); -} - -#endif diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp index b824d7c450..a40b745d21 100644 --- a/apps/openmw/mwmechanics/trading.cpp +++ b/apps/openmw/mwmechanics/trading.cpp @@ -23,7 +23,7 @@ namespace MWMechanics } // reject if npc is a creature - if ( merchant.getTypeName() != typeid(ESM::NPC).name() ) { + if ( merchant.getType() != ESM::NPC::sRecordId ) { return false; } @@ -57,7 +57,8 @@ namespace MWMechanics + gmst.find("fBargainOfferBase")->mValue.getFloat() + int(pcTerm - npcTerm); - int roll = Misc::Rng::rollDice(100) + 1; + 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) @@ -71,10 +72,10 @@ namespace MWMechanics int initialMerchantOffer = std::abs(merchantOffer); if ( !buying && (finalPrice > initialMerchantOffer) ) { - skillGain = floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); + skillGain = std::floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); } else if ( buying && (finalPrice < initialMerchantOffer) ) { - skillGain = floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); + skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); } player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); diff --git a/apps/openmw/mwmechanics/typedaipackage.hpp b/apps/openmw/mwmechanics/typedaipackage.hpp index d2d424326c..0ea276999f 100644 --- a/apps/openmw/mwmechanics/typedaipackage.hpp +++ b/apps/openmw/mwmechanics/typedaipackage.hpp @@ -11,6 +11,9 @@ namespace MWMechanics TypedAiPackage() : AiPackage(T::getTypeId(), T::makeDefaultOptions()) {} + TypedAiPackage(bool repeat) : + AiPackage(T::getTypeId(), T::makeDefaultOptions().withRepeat(repeat)) {} + TypedAiPackage(const Options& options) : AiPackage(T::getTypeId(), options) {} diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 13ce309277..024d837fe7 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -1,6 +1,6 @@ #include "weaponpriority.hpp" -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -9,7 +9,6 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "npcstats.hpp" #include "combat.hpp" #include "aicombataction.hpp" #include "spellpriority.hpp" @@ -21,7 +20,7 @@ namespace MWMechanics float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type, float arrowRating, float boltRating) { - if (enemy.isEmpty() || item.getTypeName() != typeid(ESM::Weapon).name()) + if (enemy.isEmpty() || item.getType() != ESM::Weapon::sRecordId) return 0.f; if (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) == 0) @@ -129,8 +128,7 @@ namespace MWMechanics } // Take hit chance in account, but do not allow rating become negative. - float chance = getHitChance(actor, enemy, value) / 100.f; - rating *= std::min(1.f, std::max(0.01f, chance)); + rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; diff --git a/apps/openmw/mwmechanics/weaponpriority.hpp b/apps/openmw/mwmechanics/weaponpriority.hpp index 67de7b50fe..9dcef3e2e5 100644 --- a/apps/openmw/mwmechanics/weaponpriority.hpp +++ b/apps/openmw/mwmechanics/weaponpriority.hpp @@ -1,8 +1,6 @@ #ifndef OPENMW_WEAPON_PRIORITY_H #define OPENMW_WEAPON_PRIORITY_H -#include - #include "../mwworld/ptr.hpp" namespace MWMechanics diff --git a/apps/openmw/mwmechanics/weapontype.cpp b/apps/openmw/mwmechanics/weapontype.cpp index 07345557f0..9e9e06eba7 100644 --- a/apps/openmw/mwmechanics/weapontype.cpp +++ b/apps/openmw/mwmechanics/weapontype.cpp @@ -1,35 +1,36 @@ #include "weapontype.hpp" +#include "drawstate.hpp" +#include "creaturestats.hpp" + #include "../mwworld/class.hpp" namespace MWMechanics { - static const ESM::WeaponType *sWeaponTypeListEnd = &sWeaponTypeList[sizeof(sWeaponTypeList)/sizeof(sWeaponTypeList[0])]; - - MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype) + MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int *weaptype) { MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor); CreatureStats &stats = actor.getClass().getCreatureStats(actor); - if(stats.getDrawState() == MWMechanics::DrawState_Spell) + if(stats.getDrawState() == MWMechanics::DrawState::Spell) { *weaptype = ESM::Weapon::Spell; return inv.end(); } - if(stats.getDrawState() == MWMechanics::DrawState_Weapon) + if(stats.getDrawState() == MWMechanics::DrawState::Weapon) { MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end()) *weaptype = ESM::Weapon::HandToHand; else { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); *weaptype = ref->mBase->mData.mType; } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) *weaptype = ESM::Weapon::PickProbe; } diff --git a/apps/openmw/mwmechanics/weapontype.hpp b/apps/openmw/mwmechanics/weapontype.hpp index 056a1dbfd7..4d10e9b1c5 100644 --- a/apps/openmw/mwmechanics/weapontype.hpp +++ b/apps/openmw/mwmechanics/weapontype.hpp @@ -3,8 +3,6 @@ #include "../mwworld/inventorystore.hpp" -#include "creaturestats.hpp" - namespace MWMechanics { static std::map sWeaponTypeList = @@ -263,7 +261,7 @@ namespace MWMechanics } }; - MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype); + MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int *weaptype); const ESM::WeaponType* getWeaponType(const int weaponType); } diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 5caaba5c9b..64e38559f6 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -1,6 +1,5 @@ #include "actor.hpp" -#include #include #include @@ -13,17 +12,22 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include "trace.h" + +#include namespace MWPhysics { -Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler) - : mCanWaterWalk(false), mWalkingOnWater(false) - , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBoxTranslate), mHalfExtents(shape->mCollisionBoxHalfExtents) +Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) + : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) + , mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents) + , mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) + , mActive(false) , mTaskScheduler(scheduler) { mPtr = ptr; @@ -31,7 +35,7 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic // We can not create actor without collisions - he will fall through the ground. // In this case we should autogenerate collision box based on mesh shape // (NPCs have bodyparts and use a different approach) - if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f) + if (!ptr.getClass().isNpc() && mOriginalHalfExtents.length2() == 0.f) { if (shape->mCollisionShape) { @@ -41,54 +45,58 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic btVector3 max; shape->mCollisionShape->getAabb(transform, min, max); - mHalfExtents.x() = (max[0] - min[0])/2.f; - mHalfExtents.y() = (max[1] - min[1])/2.f; - mHalfExtents.z() = (max[2] - min[2])/2.f; + mOriginalHalfExtents.x() = (max[0] - min[0])/2.f; + mOriginalHalfExtents.y() = (max[1] - min[1])/2.f; + mOriginalHalfExtents.z() = (max[2] - min[2])/2.f; - mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z()); + mMeshTranslation = osg::Vec3f(0.f, 0.f, mOriginalHalfExtents.z()); } - if (mHalfExtents.length2() == 0.f) + if (mOriginalHalfExtents.length2() == 0.f) Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; } - // Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it) - if (std::abs(mHalfExtents.x()-mHalfExtents.y())= mHalfExtents.x()) + mShape = std::make_unique(Misc::Convert::toBullet(mOriginalHalfExtents)); + + if ((mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) + && std::fabs(mOriginalHalfExtents.x() - mOriginalHalfExtents.y()) < 2.2) { - mShape.reset(new btCapsuleShapeZ(mHalfExtents.x(), 2*mHalfExtents.z() - 2*mHalfExtents.x())); mRotationallyInvariant = true; + mCollisionShapeType = DetourNavigator::CollisionShapeType::Aabb; } else { - mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); mRotationallyInvariant = false; + mCollisionShapeType = DetourNavigator::CollisionShapeType::RotatingBox; } mConvexShape = static_cast(mShape.get()); + mConvexShape->setMargin(0.001); // make sure bullet isn't using the huge default convex shape margin of 0.04 - mCollisionObject.reset(new btCollisionObject); + mCollisionObject = std::make_unique(); mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); - mCollisionObject->setUserPointer(static_cast(this)); + mCollisionObject->setUserPointer(this); - updateRotation(); updateScale(); - updatePosition(); + if(!mRotationallyInvariant) + setRotation(mPtr.getRefData().getBaseNode()->getAttitude()); + + updatePosition(); addCollisionMask(getCollisionMask()); - commitPositionChange(); + updateCollisionObjectPosition(); } Actor::~Actor() { - if (mCollisionObject) - mTaskScheduler->removeCollisionObject(mCollisionObject.get()); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } void Actor::enableCollisionMode(bool collision) { - mInternalCollisionMode.store(collision, std::memory_order_release); + mInternalCollisionMode = collision; } void Actor::enableCollisionBody(bool collision) @@ -122,88 +130,81 @@ int Actor::getCollisionMask() const void Actor::updatePosition() { - std::unique_lock lock(mPositionMutex); - osg::Vec3f position = mPtr.getRefData().getPosition().asVec3(); + std::scoped_lock lock(mPositionMutex); + const auto worldPosition = mPtr.getRefData().getPosition().asVec3(); + mPreviousPosition = worldPosition; + mPosition = worldPosition; + mSimulationPosition = worldPosition; + mPositionOffset = osg::Vec3f(); + mStandingOnPtr = nullptr; + mSkipSimulation = true; +} - mPosition = position; - mPreviousPosition = position; +void Actor::setSimulationPosition(const osg::Vec3f& position) +{ + if (!std::exchange(mSkipSimulation, false)) + mSimulationPosition = position; +} - mTransformUpdatePending = true; - updateCollisionObjectPosition(); +osg::Vec3f Actor::getScaledMeshTranslation() const +{ + return mRotation * osg::componentMultiply(mMeshTranslation, mScale); } void Actor::updateCollisionObjectPosition() { - osg::Vec3f scaledTranslation = mRotation * osg::componentMultiply(mMeshTranslation, mScale); - osg::Vec3f newPosition = scaledTranslation + mPosition; - mLocalTransform.setOrigin(Misc::Convert::toBullet(newPosition)); - mLocalTransform.setRotation(Misc::Convert::toBullet(mRotation)); + std::scoped_lock lock(mPositionMutex); + mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); + osg::Vec3f newPosition = getScaledMeshTranslation() + mPosition; -} + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(newPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); -void Actor::commitPositionChange() -{ - std::unique_lock lock(mPositionMutex); - if (mScaleUpdatePending) - { - mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); - mScaleUpdatePending = false; - } - if (mTransformUpdatePending) - { - mCollisionObject->setWorldTransform(mLocalTransform); - mTransformUpdatePending = false; - } + mWorldPositionChanged = false; } osg::Vec3f Actor::getCollisionObjectPosition() const { - std::unique_lock lock(mPositionMutex); - return Misc::Convert::toOsg(mLocalTransform.getOrigin()); + std::scoped_lock lock(mPositionMutex); + return getScaledMeshTranslation() + mPosition; } -void Actor::setPosition(const osg::Vec3f &position, bool updateCollisionObject) +bool Actor::setPosition(const osg::Vec3f& position) { - std::unique_lock lock(mPositionMutex); - if (mTransformUpdatePending) - { - mCollisionObject->setWorldTransform(mLocalTransform); - mTransformUpdatePending = false; - } - else + std::scoped_lock lock(mPositionMutex); + applyOffsetChange(); + bool hasChanged = (mPosition.operator!=(position) && !mSkipSimulation) || mWorldPositionChanged; + if (!mSkipSimulation) { mPreviousPosition = mPosition; - mPosition = position; - if (updateCollisionObject) - { - updateCollisionObjectPosition(); - mCollisionObject->setWorldTransform(mLocalTransform); - } } + return hasChanged; } -osg::Vec3f Actor::getPosition() const +void Actor::adjustPosition(const osg::Vec3f& offset) { - std::unique_lock lock(mPositionMutex); - return mPosition; + std::scoped_lock lock(mPositionMutex); + mPositionOffset += offset; } -osg::Vec3f Actor::getPreviousPosition() const +void Actor::applyOffsetChange() { - std::unique_lock lock(mPositionMutex); - return mPreviousPosition; + if (mPositionOffset.length() == 0) + return; + mPosition += mPositionOffset; + mPreviousPosition += mPositionOffset; + mSimulationPosition += mPositionOffset; + mPositionOffset = osg::Vec3f(); + mWorldPositionChanged = true; } -void Actor::updateRotation () +void Actor::setRotation(osg::Quat quat) { - std::unique_lock lock(mPositionMutex); - if (mRotation == mPtr.getRefData().getBaseNode()->getAttitude()) - return; - mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); - - mTransformUpdatePending = true; - updateCollisionObjectPosition(); + std::scoped_lock lock(mPositionMutex); + mRotation = quat; } bool Actor::isRotationallyInvariant() const @@ -213,38 +214,32 @@ bool Actor::isRotationallyInvariant() const void Actor::updateScale() { - std::unique_lock lock(mPositionMutex); + std::scoped_lock lock(mPositionMutex); float scale = mPtr.getCellRef().getScale(); osg::Vec3f scaleVec(scale,scale,scale); mPtr.getClass().adjustScale(mPtr, scaleVec, false); mScale = scaleVec; - mScaleUpdatePending = true; + mHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); scaleVec = osg::Vec3f(scale,scale,scale); mPtr.getClass().adjustScale(mPtr, scaleVec, true); - mRenderingScale = scaleVec; - - mTransformUpdatePending = true; - updateCollisionObjectPosition(); + mRenderingHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); } osg::Vec3f Actor::getHalfExtents() const { - std::unique_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mScale); + return mHalfExtents; } osg::Vec3f Actor::getOriginalHalfExtents() const { - std::unique_lock lock(mPositionMutex); - return mHalfExtents; + return mOriginalHalfExtents; } osg::Vec3f Actor::getRenderingHalfExtents() const { - std::unique_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mRenderingScale); + return mRenderingHalfExtents; } void Actor::setInertialForce(const osg::Vec3f &force) @@ -254,27 +249,26 @@ void Actor::setInertialForce(const osg::Vec3f &force) void Actor::setOnGround(bool grounded) { - mOnGround.store(grounded, std::memory_order_release); + mOnGround = grounded; } void Actor::setOnSlope(bool slope) { - mOnSlope.store(slope, std::memory_order_release); + mOnSlope = slope; } bool Actor::isWalkingOnWater() const { - return mWalkingOnWater.load(std::memory_order_acquire); + return mWalkingOnWater; } void Actor::setWalkingOnWater(bool walkingOnWater) { - mWalkingOnWater.store(walkingOnWater, std::memory_order_release); + mWalkingOnWater = walkingOnWater; } void Actor::setCanWaterWalk(bool waterWalk) { - std::unique_lock lock(mPositionMutex); if (waterWalk != mCanWaterWalk) { mCanWaterWalk = waterWalk; @@ -282,4 +276,27 @@ void Actor::setCanWaterWalk(bool waterWalk) } } +MWWorld::Ptr Actor::getStandingOnPtr() const +{ + std::scoped_lock lock(mPositionMutex); + return mStandingOnPtr; +} + +void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) +{ + std::scoped_lock lock(mPositionMutex); + mStandingOnPtr = ptr; +} + +bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const +{ + const float halfZ = getHalfExtents().z(); + const osg::Vec3f actorPosition = getPosition(); + const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); + const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); + MWPhysics::ActorTracer tracer; + tracer.doTrace(getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); +} + } diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index ef7b368b99..322ad74d7c 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -1,19 +1,20 @@ #ifndef OPENMW_MWPHYSICS_ACTOR_H #define OPENMW_MWPHYSICS_ACTOR_H -#include #include #include #include "ptrholder.hpp" +#include + #include #include #include -#include class btCollisionShape; class btCollisionObject; +class btCollisionWorld; class btConvexShape; namespace Resource @@ -28,7 +29,7 @@ namespace MWPhysics class Actor final : public PtrHolder { public: - Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler); + Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk); ~Actor() override; /** @@ -38,7 +39,7 @@ namespace MWPhysics bool getCollisionMode() const { - return mInternalCollisionMode.load(std::memory_order_acquire); + return mInternalCollisionMode; } btConvexShape* getConvexShape() const { return mConvexShape; } @@ -49,7 +50,7 @@ namespace MWPhysics void enableCollisionBody(bool collision); void updateScale(); - void updateRotation(); + void setRotation(osg::Quat quat); /** * Return true if the collision shape looks the same no matter how its Z rotated. @@ -57,13 +58,12 @@ namespace MWPhysics bool isRotationallyInvariant() const; /** - * Set mPosition and mPreviousPosition to the position in the Ptr's RefData. This should be used - * when an object is "instantly" moved/teleported as opposed to being moved by the physics simulation. - */ - void updatePosition(); + * Used by the physics simulation to store the simulation result. Used in conjunction with mWorldPosition + * to account for e.g. scripted movements + */ + void setSimulationPosition(const osg::Vec3f& position); void updateCollisionObjectPosition(); - void commitPositionChange(); /** * Returns the half extents of the collision body (scaled according to collision scale) @@ -83,13 +83,18 @@ namespace MWPhysics /** * Store the current position into mPreviousPosition, then move to this position. - * Optionally, inform the physics engine about the change of position. + * Returns true if the new position is different. */ - void setPosition(const osg::Vec3f& position, bool updateCollisionObject=true); + bool setPosition(const osg::Vec3f& position); + + // force set actor position to be as in Ptr::RefData + void updatePosition(); - osg::Vec3f getPosition() const; + // register a position offset that will be applied during simulation. + void adjustPosition(const osg::Vec3f& offset); - osg::Vec3f getPreviousPosition() const; + // apply position offset. Can't be called during simulation + void applyOffsetChange(); /** * Returns the half extents of the collision body (scaled according to rendering scale) @@ -113,22 +118,11 @@ namespace MWPhysics void setOnGround(bool grounded); - bool getOnGround() const - { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnGround.load(std::memory_order_acquire); - } + bool getOnGround() const { return mOnGround; } void setOnSlope(bool slope); - bool getOnSlope() const - { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnSlope.load(std::memory_order_acquire); - } - - btCollisionObject* getCollisionObject() const - { - return mCollisionObject.get(); - } + bool getOnSlope() const { return mOnSlope; } /// Sets whether this actor should be able to collide with the water surface void setCanWaterWalk(bool waterWalk); @@ -137,40 +131,76 @@ namespace MWPhysics void setWalkingOnWater(bool walkingOnWater); bool isWalkingOnWater() const; + MWWorld::Ptr getStandingOnPtr() const; + void setStandingOnPtr(const MWWorld::Ptr& ptr); + + unsigned int getStuckFrames() const + { + return mStuckFrames; + } + void setStuckFrames(unsigned int frames) + { + mStuckFrames = frames; + } + + const osg::Vec3f &getLastStuckPosition() const + { + return mLastStuckPosition; + } + void setLastStuckPosition(osg::Vec3f position) + { + mLastStuckPosition = position; + } + + bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; + + bool isActive() const { return mActive; } + + void setActive(bool value) { mActive = value; } + + DetourNavigator::CollisionShapeType getCollisionShapeType() const { return mCollisionShapeType; } + private: + MWWorld::Ptr mStandingOnPtr; /// Removes then re-adds the collision object to the dynamics world void updateCollisionMask(); void addCollisionMask(int collisionMask); int getCollisionMask() const; + /// Returns the mesh translation, scaled and rotated as necessary + osg::Vec3f getScaledMeshTranslation() const; + bool mCanWaterWalk; - std::atomic mWalkingOnWater; + bool mWalkingOnWater; bool mRotationallyInvariant; + DetourNavigator::CollisionShapeType mCollisionShapeType; + std::unique_ptr mShape; btConvexShape* mConvexShape; - std::unique_ptr mCollisionObject; - osg::Vec3f mMeshTranslation; + osg::Vec3f mOriginalHalfExtents; osg::Vec3f mHalfExtents; + osg::Vec3f mRenderingHalfExtents; osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mRenderingScale; - osg::Vec3f mPosition; - osg::Vec3f mPreviousPosition; - btTransform mLocalTransform; - bool mScaleUpdatePending; - bool mTransformUpdatePending; + osg::Vec3f mPositionOffset; + bool mWorldPositionChanged; + bool mSkipSimulation; mutable std::mutex mPositionMutex; + unsigned int mStuckFrames; + osg::Vec3f mLastStuckPosition; + osg::Vec3f mForce; - std::atomic mOnGround; - std::atomic mOnSlope; - std::atomic mInternalCollisionMode; + bool mOnGround; + bool mOnSlope; + bool mInternalCollisionMode; bool mExternalCollisionMode; + bool mActive; PhysicsTaskScheduler* mTaskScheduler; diff --git a/apps/openmw/mwphysics/actorconvexcallback.cpp b/apps/openmw/mwphysics/actorconvexcallback.cpp new file mode 100644 index 0000000000..672af05058 --- /dev/null +++ b/apps/openmw/mwphysics/actorconvexcallback.cpp @@ -0,0 +1,101 @@ +#include "actorconvexcallback.hpp" +#include "collisiontype.hpp" +#include "contacttestwrapper.h" + +#include +#include + +#include "projectile.hpp" + +namespace MWPhysics +{ + class ActorOverlapTester : public btCollisionWorld::ContactResultCallback + { + public: + bool overlapping = false; + + btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObjectWrapper* colObj0Wrap, + int partId0, + int index0, + const btCollisionObjectWrapper* colObj1Wrap, + int partId1, + int index1) override + { + if(cp.getDistance() <= 0.0f) + overlapping = true; + return btScalar(1); + } + }; + + 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 ActorConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) + { + if (convexResult.m_hitCollisionObject == mMe) + return btScalar(1); + + // override data for actor-actor collisions + // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them + // For some reason this doesn't work as well as it should when using capsules, but it still helps a lot. + if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) + { + ActorOverlapTester isOverlapping; + // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct. + ContactTestWrapper::contactPairTest(const_cast(mWorld), const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); + + if(isOverlapping.overlapping) + { + auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); + auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); + osg::Vec3f motion = Misc::Convert::toOsg(mMotion); + osg::Vec3f normal = (originA-originB); + normal.z() = 0; + normal.normalize(); + // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted) + // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them. + // It happens in vanilla Morrowind too, but much less often. + // I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug. + if(normal * motion > 0.0f) + { + convexResult.m_hitFraction = 0.0f; + convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); + return ClosestConvexResultCallback::addSingleResult(convexResult, true); + } + else + { + return btScalar(1); + } + } + } + if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) + { + auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); + if (!projectileHolder->isActive()) + return btScalar(1); + if (projectileHolder->isValidTarget(mMe)) + projectileHolder->hit(mMe, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); + return btScalar(1); + } + + btVector3 hitNormalWorld; + if (normalInWorldSpace) + hitNormalWorld = convexResult.m_hitNormalLocal; + else + { + ///need to transform normal into worldspace + hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + } + + // dot product of the motion vector against the collision contact normal + btScalar dotCollision = mMotion.dot(hitNormalWorld); + if (dotCollision <= mMinCollisionDot) + return btScalar(1); + + return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); + } +} diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp b/apps/openmw/mwphysics/actorconvexcallback.hpp similarity index 52% rename from apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp rename to apps/openmw/mwphysics/actorconvexcallback.hpp index 97aaa64a1c..1c28ee6cc4 100644 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp +++ b/apps/openmw/mwphysics/actorconvexcallback.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_MWPHYSICS_CLOSESTNOTMECONVEXRESULTCALLBACK_H -#define OPENMW_MWPHYSICS_CLOSESTNOTMECONVEXRESULTCALLBACK_H +#ifndef OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H +#define OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H #include @@ -7,10 +7,10 @@ class btCollisionObject; namespace MWPhysics { - class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback + class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot); + ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override; @@ -18,6 +18,7 @@ namespace MWPhysics const btCollisionObject *mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; + const btCollisionWorld * mWorld; }; } diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp deleted file mode 100644 index ddfdb8a42f..0000000000 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "closestnotmeconvexresultcallback.hpp" - -#include - -namespace MWPhysics -{ - ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot) - : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), - mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot) - { - } - - btScalar ClosestNotMeConvexResultCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) - { - if (convexResult.m_hitCollisionObject == mMe) - return btScalar(1); - - btVector3 hitNormalWorld; - if (normalInWorldSpace) - hitNormalWorld = convexResult.m_hitNormalLocal; - else - { - ///need to transform normal into worldspace - hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; - } - - // dot product of the motion vector against the collision contact normal - btScalar dotCollision = mMotion.dot(hitNormalWorld); - if (dotCollision <= mMinCollisionDot) - return btScalar(1); - - return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); - } -} diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp index 86763a7933..3f6cb2b727 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp @@ -1,34 +1,35 @@ #include "closestnotmerayresultcallback.hpp" #include +#include #include #include "../mwworld/class.hpp" +#include "collisiontype.hpp" #include "ptrholder.hpp" namespace MWPhysics { - ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3& from, const btVector3& to) + ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to) : btCollisionWorld::ClosestRayResultCallback(from, to) - , mMe(me), mTargets(targets) + , mMe(me), mTargets(std::move(targets)) { } btScalar ClosestNotMeRayResultCallback::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { - if (rayResult.m_collisionObject == mMe) + const auto* hitObject = rayResult.m_collisionObject; + if (hitObject == mMe) return 1.f; - if (!mTargets.empty()) + + if (hitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { - if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end())) - { - PtrHolder* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 1.f; - } + if ((std::find(mTargets.begin(), mTargets.end(), hitObject) == mTargets.end())) + return 1.f; } + return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); } } diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp index 23d52998ca..1fa32ef686 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp @@ -9,12 +9,15 @@ class btCollisionObject; namespace MWPhysics { + class Projectile; + class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: - ClosestNotMeRayResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3& from, const btVector3& to); + ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to); btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; + private: const btCollisionObject* mMe; const std::vector mTargets; diff --git a/apps/openmw/mwphysics/collisiontype.hpp b/apps/openmw/mwphysics/collisiontype.hpp index 0d6a32fc09..b51a22a2f5 100644 --- a/apps/openmw/mwphysics/collisiontype.hpp +++ b/apps/openmw/mwphysics/collisiontype.hpp @@ -10,7 +10,11 @@ enum CollisionType { CollisionType_Actor = 1<<2, CollisionType_HeightMap = 1<<3, CollisionType_Projectile = 1<<4, - CollisionType_Water = 1<<5 + CollisionType_Water = 1<<5, + CollisionType_Default = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, + CollisionType_AnyPhysical = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door|CollisionType_Projectile|CollisionType_Water, + CollisionType_CameraOnly = 1<<6, + CollisionType_VisualOnly = 1<<7 }; } diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index 46367ab343..b2d189e874 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -3,14 +3,22 @@ namespace MWPhysics { - static const float sStepSizeUp = 34.0f; - static const float sStepSizeDown = 62.0f; - static const float sMinStep = 10.f; - static const float sGroundOffset = 1.0f; - static const float sMaxSlope = 49.0f; + static constexpr float sStepSizeDown = 62.0f; + + static constexpr float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes + static constexpr float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes + // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance + static constexpr bool sDoExtraStairHacks = true; + + static constexpr float sGroundOffset = 1.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. - static const int sMaxIterations = 8; + static constexpr int sMaxIterations = 8; + // Allows for more precise movement solving without getting stuck or snagging too easily. + static constexpr float sCollisionMargin = 0.2f; + // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily + // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues + static constexpr float sAllowedPenetration = 0.0f; } #endif diff --git a/apps/openmw/mwphysics/contacttestresultcallback.hpp b/apps/openmw/mwphysics/contacttestresultcallback.hpp index fbe12d5dc2..3d1b3b8aab 100644 --- a/apps/openmw/mwphysics/contacttestresultcallback.hpp +++ b/apps/openmw/mwphysics/contacttestresultcallback.hpp @@ -5,8 +5,6 @@ #include -#include "../mwworld/ptr.hpp" - #include "physicssystem.hpp" class btCollisionObject; diff --git a/apps/openmw/mwphysics/contacttestwrapper.cpp b/apps/openmw/mwphysics/contacttestwrapper.cpp new file mode 100644 index 0000000000..c11a7e2926 --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.cpp @@ -0,0 +1,21 @@ +#include + +#include "contacttestwrapper.h" + +namespace MWPhysics +{ + // Concurrent calls to contactPairTest (and by extension contactTest) are forbidden. + static std::mutex contactMutex; + void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactTest(colObj, resultCallback); + } + + void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); + } + +} diff --git a/apps/openmw/mwphysics/contacttestwrapper.h b/apps/openmw/mwphysics/contacttestwrapper.h new file mode 100644 index 0000000000..b3b6edc59a --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.h @@ -0,0 +1,14 @@ +#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H +#define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H + +#include + +namespace MWPhysics +{ + struct ContactTestWrapper + { + static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); + static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); + }; +} +#endif diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp index 7744af14b5..3531cc8eb8 100644 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp +++ b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp @@ -6,6 +6,7 @@ #include "../mwworld/class.hpp" +#include "collisiontype.hpp" #include "ptrholder.hpp" namespace MWPhysics @@ -23,14 +24,10 @@ namespace MWPhysics const btCollisionObject* collisionObject = col1Wrap->m_collisionObject; if (collisionObject != mMe) { - if (!mTargets.empty()) + if (collisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), collisionObject) == mTargets.end())) - { - PtrHolder* holder = static_cast(collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 0.f; - } + return 0.f; } btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index 275325cf67..09be0fd306 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -2,9 +2,7 @@ #define OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #include -#include #include -#include #include @@ -15,35 +13,43 @@ namespace MWPhysics const btVector3& position, const btScalar radius) { const btVector3 nearest( - std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), - std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), - std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) + std::clamp(position.x(), aabbMin.x(), aabbMax.x()), + std::clamp(position.y(), aabbMin.y(), aabbMax.y()), + std::clamp(position.z(), aabbMin.z(), aabbMax.z()) ); return nearest.distance(position) < radius; } + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object, - const int mask, const int group) + HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, + const Ignore& ignore, OnCollision* onCollision) : mPosition(position), mRadius(radius), - mCollisionObject(object), + mIgnore(ignore), mCollisionFilterMask(mask), - mCollisionFilterGroup(group) + mCollisionFilterGroup(group), + mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult) + if (mResult && mOnCollision == nullptr) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (collisionObject == mCollisionObject) + if (mIgnore(collisionObject) + || !needsCollision(*proxy) + || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; - if (needsCollision(*proxy)) - mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius); + mResult = true; + if (mOnCollision != nullptr) + { + (*mOnCollision)(collisionObject); + return true; + } return !mResult; } @@ -55,9 +61,10 @@ namespace MWPhysics private: btVector3 mPosition; btScalar mRadius; - btCollisionObject* mCollisionObject; + Ignore mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; + OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/heightfield.cpp b/apps/openmw/mwphysics/heightfield.cpp index e1448116bf..d363ddef11 100644 --- a/apps/openmw/mwphysics/heightfield.cpp +++ b/apps/openmw/mwphysics/heightfield.cpp @@ -1,4 +1,7 @@ #include "heightfield.hpp" +#include "mtphysics.hpp" + +#include #include @@ -9,20 +12,26 @@ #include +#if BT_BULLET_VERSION < 310 +// Older Bullet versions only support `btScalar` heightfields. +// Our heightfield data is `float`. +// +// These functions handle conversion from `float` to `double` when +// `btScalar` is `double` (`BT_USE_DOUBLE_PRECISION`). namespace { template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { return {}; } template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { - return std::vector(heights, heights + static_cast(sqrtVerts * sqrtVerts)); + return std::vector(heights, heights + static_cast(verts * verts)); } template @@ -39,52 +48,70 @@ namespace return btScalarHeights.data(); } } +#endif namespace MWPhysics { - HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject) - : mHeights(makeHeights(heights, sqrtVerts)) + HeightField::HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) + : mHoldObject(holdObject) +#if BT_BULLET_VERSION < 310 + , mHeights(makeHeights(heights, verts)) +#endif + , mTaskScheduler(scheduler) { - mShape = new btHeightfieldTerrainShape( - sqrtVerts, sqrtVerts, +#if BT_BULLET_VERSION < 310 + mShape = std::make_unique( + verts, verts, getHeights(heights, mHeights), 1, minH, maxH, 2, PHY_FLOAT, false ); +#else + mShape = std::make_unique( + verts, verts, heights, minH, maxH, 2, false); +#endif mShape->setUseDiamondSubdivision(true); - mShape->setLocalScaling(btVector3(triSize, triSize, 1)); - btTransform transform(btQuaternion::getIdentity(), - btVector3((x+0.5f) * triSize * (sqrtVerts-1), - (y+0.5f) * triSize * (sqrtVerts-1), - (maxH+minH)*0.5f)); + const float scaling = static_cast(size) / static_cast(verts - 1); + mShape->setLocalScaling(btVector3(scaling, scaling, 1)); - mCollisionObject = new btCollisionObject; - mCollisionObject->setCollisionShape(mShape); - mCollisionObject->setWorldTransform(transform); +#if BT_BULLET_VERSION >= 289 + // Accelerates some collision tests. + // + // Note: The accelerator data structure in Bullet is only used + // in some operations. This could be improved, see: + // https://github.com/bulletphysics/bullet3/issues/3276 + mShape->buildAccelerator(); +#endif - mHoldObject = holdObject; + const btTransform transform(btQuaternion::getIdentity(), + BulletHelpers::getHeightfieldShift(x, y, size, minH, maxH)); + + mCollisionObject = std::make_unique(); + mCollisionObject->setCollisionShape(mShape.get()); + mCollisionObject->setWorldTransform(transform); + mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_HeightMap, CollisionType_Actor|CollisionType_Projectile); } HeightField::~HeightField() { - delete mCollisionObject; - delete mShape; + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } btCollisionObject* HeightField::getCollisionObject() { - return mCollisionObject; + return mCollisionObject.get(); } const btCollisionObject* HeightField::getCollisionObject() const { - return mCollisionObject; + return mCollisionObject.get(); } const btHeightfieldTerrainShape* HeightField::getShape() const { - return mShape; + return mShape.get(); } } diff --git a/apps/openmw/mwphysics/heightfield.hpp b/apps/openmw/mwphysics/heightfield.hpp index 2ba58afff8..c320225258 100644 --- a/apps/openmw/mwphysics/heightfield.hpp +++ b/apps/openmw/mwphysics/heightfield.hpp @@ -5,6 +5,7 @@ #include +#include #include class btCollisionObject; @@ -17,10 +18,13 @@ namespace osg namespace MWPhysics { + class PhysicsTaskScheduler; + class HeightField { public: - HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); + HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); ~HeightField(); btCollisionObject* getCollisionObject(); @@ -28,10 +32,14 @@ namespace MWPhysics const btHeightfieldTerrainShape* getShape() const; private: - btHeightfieldTerrainShape* mShape; - btCollisionObject* mCollisionObject; + std::unique_ptr mShape; + std::unique_ptr mCollisionObject; osg::ref_ptr mHoldObject; +#if BT_BULLET_VERSION < 310 std::vector mHeights; +#endif + + PhysicsTaskScheduler* mTaskScheduler; void operator=(const HeightField&); HeightField(const HeightField&); diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 3c78104dc7..da07d5975b 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -3,8 +3,9 @@ #include #include #include +#include -#include +#include #include #include "../mwbase/world.hpp" @@ -17,10 +18,15 @@ #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" +#include "contacttestwrapper.h" #include "physicssystem.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" #include "stepper.hpp" #include "trace.h" +#include + namespace MWPhysics { static bool isActor(const btCollisionObject *obj) @@ -29,12 +35,50 @@ namespace MWPhysics return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } - template - static bool isWalkableSlope(const Vec3 &normal) + class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); - return (normal.z() > sMaxSlopeCos); - } + public: + ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me) + { + 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) + { + 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; + }; osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) { @@ -57,7 +101,7 @@ namespace MWPhysics btVector3 to = from - btVector3(0,0,maxHeight); btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); - resultCallback1.m_collisionFilterGroup = 0xff; + resultCallback1.m_collisionFilterGroup = CollisionType_AnyPhysical; resultCallback1.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap; collisionWorld->rayTest(from, to, resultCallback1); @@ -75,68 +119,54 @@ namespace MWPhysics } void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, - WorldFrameData& worldData) + const WorldFrameData& worldData) { - auto* physicActor = actor.mActorRaw; - auto ptr = actor.mPtr; - const ESM::Position& refpos = actor.mRefpos; - // Early-out for totally static creatures - // (Not sure if gravity should still apply?) - if (!ptr.getClass().isMobile(ptr)) - return; - // Reset per-frame data - physicActor->setWalkingOnWater(false); + actor.mWalkingOnWater = false; // Anything to collide with? - if(!physicActor->getCollisionMode()) + if(actor.mSkipCollisionDetection) { - actor.mPosition += (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * - osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1)) + actor.mPosition += (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * + osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1)) ) * actor.mMovement * time; return; } - const btCollisionObject *colobj = physicActor->getCollisionObject(); - osg::Vec3f halfExtents = physicActor->getHalfExtents(); - - // NOTE: here we don't account for the collision box translation (i.e. physicActor->getPosition() - refpos.pos). - // That means the collision shape used for moving this actor is in a different spot than the collision shape - // other actors are using to collide against this actor. - // While this is strictly speaking wrong, it's needed for MW compatibility. - actor.mPosition.z() += halfExtents.z(); + // Adjust for collision mesh offset relative to actor's "location" + // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) + // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation + // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + actor.mPosition.z() += actor.mHalfExtentsZ; // vanilla-accurate - static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); - float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); + float swimlevel = actor.mSwimLevel + actor.mHalfExtentsZ; ActorTracer tracer; - osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; - if (actor.mPosition.z() < swimlevel || actor.mFlying) + // Dead and paralyzed actors underwater will float to the surface, + // if the CharacterController tells us to do so + if (actor.mMovement.z() > 0 && actor.mInert && actor.mPosition.z() < swimlevel) + { + velocity = osg::Vec3f(0,0,1) * 25; + } + else if (actor.mPosition.z() < swimlevel || actor.mFlying) { - velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; } else { - velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + velocity = (osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; - if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) - || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) - inertia = velocity; - else if (!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity = velocity + inertia; + if ((velocity.z() > 0.f && actor.mIsOnGround && !actor.mIsOnSlope) + || (velocity.z() > 0.f && velocity.z() + actor.mInertia.z() <= -velocity.z() && actor.mIsOnSlope)) + actor.mInertia = velocity; + else if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity = velocity + actor.mInertia; } - // dead actors underwater will float to the surface, if the CharacterController tells us to do so - if (actor.mMovement.z() > 0 && actor.mIsDead && actor.mPosition.z() < swimlevel) - velocity = osg::Vec3f(0,0,1) * 25; - - if (actor.mWantJump) - actor.mDidJump = true; - // Now that we have the effective movement vector, apply wind forces to it - if (worldData.mIsInStorm) + if (worldData.mIsInStorm && velocity.length() > 0) { osg::Vec3f stormDirection = worldData.mStormDirection; float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); @@ -144,7 +174,7 @@ namespace MWPhysics velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f)); } - Stepper stepper(collisionWorld, colobj); + Stepper stepper(collisionWorld, actor.mCollisionObject); osg::Vec3f origVelocity = velocity; osg::Vec3f newPosition = actor.mPosition; /* @@ -153,15 +183,22 @@ namespace MWPhysics * The initial velocity was set earlier (see above). */ float remainingTime = time; - for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations) + + int numTimesSlid = 0; + osg::Vec3f lastSlideNormal(0,0,1); + osg::Vec3f lastSlideNormalFallback(0,0,1); + bool forceGroundTest = false; + + for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.0001f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; + bool underwater = newPosition.z() < swimlevel; // If not able to fly, don't allow to swim up into the air - if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) + if(!actor.mFlying && nextpos.z() > swimlevel && underwater) { const osg::Vec3f down(0,0,-1); - velocity = slide(velocity, down); + velocity = reject(velocity, down); // NOTE: remainingTime is unchanged before the loop continues continue; // velocity updated, calculate nextpos again } @@ -169,7 +206,7 @@ namespace MWPhysics if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(colobj, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -190,111 +227,317 @@ namespace MWPhysics break; } - // We are touching something. - if (tracer.mFraction < 1E-9f) - { - // Try to separate by backing off slighly to unstuck the solver - osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f; - newPosition += backOff; - } + bool seenGround = !actor.mFlying && !underwater && ((actor.mIsOnGround && !actor.mIsOnSlope) || isWalkableSlope(tracer.mPlaneNormal)); // We hit something. Check if we can step up. - float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); + float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; - bool result = false; - if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) + bool usedStepLogic = false; + if (hitHeight < Constants::sStepSizeUp && !isActor(tracer.mHitObject)) { // Try to step up onto it. - // NOTE: stepMove does not allow stepping over, modifies newPosition if successful - result = stepper.step(newPosition, velocity*remainingTime, remainingTime); + // NOTE: this modifies newPosition and velocity on its own if successful + usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); } - if (result) + if (usedStepLogic) { - // don't let pure water creatures move out of water after stepMove - if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) + if (actor.mIsAquatic && newPosition.z() + actor.mHalfExtentsZ > actor.mWaterlevel) newPosition = oldPosition; + else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) + forceGroundTest = true; } else { - // Can't move this way, try to find another spot along the plane - osg::Vec3f newVelocity = slide(velocity, tracer.mPlaneNormal); + // Can't step up, so slide against what we ran into + remainingTime *= (1.0f-tracer.mFraction); - // Do not allow sliding upward if there is gravity. - // Stepping will have taken care of that. - if(!(newPosition.z() < swimlevel || actor.mFlying)) - newVelocity.z() = std::min(newVelocity.z(), 0.0f); + auto planeNormal = tracer.mPlaneNormal; + // need to know the unadjusted normal to handle certain types of seams properly + const auto origPlaneNormal = planeNormal; - if ((newVelocity-velocity).length2() < 0.01) - break; - if ((newVelocity * origVelocity) <= 0.f) - break; // ^ dot product + // If we touched the ground this frame, and whatever we ran into is a wall of some sort, + // pretend that its collision normal is pointing horizontally + // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin) + if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + + // Move up to what we ran into (with a bit of a collision margin) + if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin) + { + auto direction = velocity; + direction.normalize(); + newPosition = tracer.mEndPos; + newPosition -= direction*sCollisionMargin; + } + + osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity; + bool usedSeamLogic = false; + + // check for the current and previous collision planes forming an acute angle; slide along the seam if they do + // for this, we want to use the original plane normal, or else certain types of geometry will snag + if(numTimesSlid > 0) + { + auto dotA = lastSlideNormal * origPlaneNormal; + auto dotB = lastSlideNormalFallback * origPlaneNormal; + if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide + dotB = 1.0; + if(dotA <= 0.0 || dotB <= 0.0) + { + osg::Vec3f bestNormal = lastSlideNormal; + // use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't + if(dotB < dotA) + { + bestNormal = lastSlideNormalFallback; + lastSlideNormal = lastSlideNormalFallback; + } + + auto constraintVector = bestNormal ^ origPlaneNormal; // cross product + if(constraintVector.length2() > 0) // only if it's not zero length + { + constraintVector.normalize(); + newVelocity = project(velocity, constraintVector); + + // version of surface rejection for acute crevices/seams + auto averageNormal = bestNormal + origPlaneNormal; + averageNormal.normalize(); + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + + usedSeamLogic = true; + } + } + } + // otherwise just keep the normal vector rejection + // move away from the collision plane slightly, if possible + // this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings + // this is different from the normal collision margin, because the normal collision margin is along the movement path, + // but this is along the collision normal + if(!usedSeamLogic) + { + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + } + + // short circuit if we went backwards, but only if it was mostly horizontal and we're on the ground + if (seenGround && newVelocity * origVelocity <= 0.0f) + { + auto perpendicular = newVelocity ^ origVelocity; + if (perpendicular.length2() > 0.0f) + { + perpendicular.normalize(); + if (std::abs(perpendicular.z()) > 0.7071f) + break; + } + } + + // Do not allow sliding up steep slopes if there is gravity. + // The purpose of this is to prevent air control from letting you slide up tall, unwalkable slopes. + // For that purpose, it is not necessary to do it when trying to slide along acute seams/crevices (i.e. usedSeamLogic) + // and doing so would actually break air control in some situations where vanilla allows air control. + // Vanilla actually allows you to slide up slopes as long as you're in the "walking" animation, which can be true even + // in the air, so allowing this for seams isn't a compatibility break. + if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal) && !usedSeamLogic) + newVelocity.z() = std::min(newVelocity.z(), velocity.z()); + + numTimesSlid += 1; + lastSlideNormalFallback = lastSlideNormal; + lastSlideNormal = origPlaneNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; - if (!(inertia.z() > 0.f) && !(newPosition.z() < swimlevel)) + if (forceGroundTest || (actor.mInertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; - osg::Vec3f to = newPosition - (physicActor->getOnGround() ? osg::Vec3f(0,0,sStepSizeDown + 2*sGroundOffset) : osg::Vec3f(0,0,2*sGroundOffset)); - tracer.doTrace(colobj, from, to, collisionWorld); - if(tracer.mFraction < 1.0f && !isActor(tracer.mHitObject)) - { - const btCollisionObject* standingOn = tracer.mHitObject; - PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); - if (ptrHolder) - actor.mStandingOn = ptrHolder->getPtr(); - - if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) - physicActor->setWalkingOnWater(true); - if (!actor.mFlying) - newPosition.z() = tracer.mEndPos.z() + sGroundOffset; - - isOnGround = true; - - isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); - } - else + auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); + osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); + if(tracer.mFraction < 1.0f) { - // standing on actors is not allowed (see above). - // in addition to that, apply a sliding effect away from the center of the actor, - // so that we do not stay suspended in air indefinitely. - if (tracer.mFraction < 1.0f && isActor(tracer.mHitObject)) + if (!isActor(tracer.mHitObject)) { - if (osg::Vec3f(velocity.x(), velocity.y(), 0).length2() < 100.f*100.f) + isOnGround = true; + isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); + actor.mStandingOn = tracer.mHitObject; + + if (actor.mStandingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) + actor.mWalkingOnWater = true; + if (!actor.mFlying && !isOnSlope) { - btVector3 aabbMin, aabbMax; - tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); - btVector3 center = (aabbMin + aabbMax) / 2.f; - inertia = osg::Vec3f(actor.mPosition.x() - center.x(), actor.mPosition.y() - center.y(), 0); - inertia.normalize(); - inertia *= 100; + if (tracer.mFraction*dropDistance > sGroundOffset) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; + else + { + newPosition.z() = tracer.mEndPos.z(); + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); + newPosition = (newPosition+tracer.mEndPos)/2.0; + } } } + else + { + // Vanilla allows actors to float on top of other actors. Do not push them off. + if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z()) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; - isOnGround = false; + isOnGround = false; + } + } + // forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things can/will break ground detection + if(actor.mStuckFrames > 0) + { + isOnGround = true; + isOnSlope = false; } } if((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) - physicActor->setInertialForce(osg::Vec3f(0.f, 0.f, 0.f)); + actor.mInertia = osg::Vec3f(0.f, 0.f, 0.f); else { - inertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; - if (inertia.z() < 0) - inertia.z() *= actor.mSlowFall; + actor.mInertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; + if (actor.mInertia.z() < 0) + actor.mInertia.z() *= actor.mSlowFall; if (actor.mSlowFall < 1.f) { - inertia.x() *= actor.mSlowFall; - inertia.y() *= actor.mSlowFall; + actor.mInertia.x() *= actor.mSlowFall; + actor.mInertia.y() *= actor.mSlowFall; } - physicActor->setInertialForce(inertia); } - physicActor->setOnGround(isOnGround); - physicActor->setOnSlope(isOnSlope); + actor.mIsOnGround = isOnGround; + actor.mIsOnSlope = isOnSlope; - newPosition.z() -= halfExtents.z(); // remove what was added at the beginning actor.mPosition = newPosition; + // remove what was added earlier in compensating for doTrace not taking interior transformation into account + actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate + } + + void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld) + { + btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition); + btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time); + + if (btFrom == btTo) + return; + + ProjectileConvexCallback resultCallback(projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, projectile.mProjectile); + resultCallback.m_collisionFilterMask = CollisionType_AnyPhysical; + resultCallback.m_collisionFilterGroup = CollisionType_Projectile; + + const btQuaternion btrot = btQuaternion::getIdentity(); + btTransform from_ (btrot, btFrom); + btTransform to_ (btrot, btTo); + + const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape(); + assert(shape->isConvex()); + collisionWorld->convexSweepTest(static_cast(shape), from_, to_, resultCallback); + + projectile.mPosition = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld); + } + + btVector3 addMarginToDelta(btVector3 delta) + { + if(delta.length2() == 0.0) + return delta; + return delta + delta.normalized() * sCollisionMargin; + } + + void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) + { + if(actor.mSkipCollisionDetection) // noclipping/tcl + return; + + auto tempPosition = actor.mPosition; + + if(actor.mStuckFrames >= 10) + { + if((actor.mLastStuckPosition - actor.mPosition).length2() < 100) + return; + else + { + actor.mStuckFrames = 0; + actor.mLastStuckPosition = {0, 0, 0}; + } + } + + // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) + // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, actor.mHalfExtentsZ); + + // use a 3d approximation of the movement vector to better judge player intent + auto velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; + // try to pop outside of the world before doing anything else if we're inside of it + if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity += actor.mInertia; + + // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, + // we need to replicate part of the collision box's transform process from scratch + osg::Vec3f refPosition = tempPosition + verticalHalfExtent; + osg::Vec3f goodPosition = refPosition; + const btTransform oldTransform = actor.mCollisionObject->getWorldTransform(); + btTransform newTransform = oldTransform; + + auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback + { + goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); + newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); + actor.mCollisionObject->setWorldTransform(newTransform); + + ContactCollectionCallback callback{actor.mCollisionObject, velocity}; + ContactTestWrapper::contactTest(const_cast(collisionWorld), actor.mCollisionObject, callback); + return callback; + }; + + // check whether we're inside the world with our collision box with manually-derived offset + auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); + if(contactCallback.mDistance < -sAllowedPenetration) + { + ++actor.mStuckFrames; + actor.mLastStuckPosition = actor.mPosition; + // we are; try moving it out of the world + auto positionDelta = contactCallback.mContactSum; + // limit rejection delta to the largest known individual rejections + if(std::abs(positionDelta.x()) > contactCallback.mMaxX) + positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); + if(std::abs(positionDelta.y()) > contactCallback.mMaxY) + positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); + if(std::abs(positionDelta.z()) > contactCallback.mMaxZ) + positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z()); + + auto contactCallback2 = gatherContacts(positionDelta); + // successfully moved further out from contact (does not have to be in open space, just less inside of things) + if(contactCallback2.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + // try again but only upwards (fixes some bad coc floors) + else + { + // upwards-only offset + auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())}); + // success + if(contactCallback3.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + else + // try again but fixed distance up + { + auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0}); + // success + if(contactCallback4.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + } + } + } + else + { + actor.mStuckFrames = 0; + actor.mLastStuckPosition = {0, 0, 0}; + } + + actor.mCollisionObject->setWorldTransform(oldTransform); + actor.mPosition = tempPosition; } } diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 75fba1cf0e..1bbe76cbec 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -1,10 +1,12 @@ #ifndef OPENMW_MWPHYSICS_MOVEMENTSOLVER_H #define OPENMW_MWPHYSICS_MOVEMENTSOLVER_H -#include - #include +#include + +#include "../mwworld/ptr.hpp" + class btCollisionWorld; namespace MWWorld @@ -14,29 +16,37 @@ namespace MWWorld namespace MWPhysics { + /// Vector projection + static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) + { + return v * (u * v); + } + + /// Vector rejection + static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) + { + return direction - project(direction, planeNormal); + } + + template + static bool isWalkableSlope(const Vec3 &normal) + { + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); + return (normal.z() > sMaxSlopeCos); + } + class Actor; struct ActorFrameData; + struct ProjectileFrameData; struct WorldFrameData; class MovementSolver { - private: - ///Project a vector u on another vector v - static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) - { - return v * (u * v); - // ^ dot product - } - - ///Helper for computing the character sliding - static inline osg::Vec3f slide(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) - { - return direction - project(direction, planeNormal); - } - public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); - static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); + static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); + static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index f105efce5e..297f40c6a7 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -1,319 +1,512 @@ +#include +#include +#include +#include +#include + #include #include +#include + #include "components/debug/debuglog.hpp" #include #include "components/misc/convert.hpp" #include "components/settings/settings.hpp" + #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwmechanics/creaturestats.hpp" + +#include "../mwrender/bulletdebugdraw.hpp" + #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "actor.hpp" +#include "contacttestwrapper.h" #include "movementsolver.hpp" #include "mtphysics.hpp" #include "object.hpp" #include "physicssystem.hpp" +#include "projectile.hpp" namespace { - /// @brief A scoped lock that is either shared or exclusive depending on configuration + template + std::optional> makeExclusiveLock(Mutex& mutex, unsigned threadCount) + { + if (threadCount > 0) + return std::unique_lock(mutex); + return {}; + } + + /// @brief A scoped lock that is either exclusive or inexistent depending on configuration template - class MaybeSharedLock + class MaybeExclusiveLock { public: - /// @param mutex a shared mutex - /// @param canBeSharedLock decide wether the lock will be shared or exclusive - MaybeSharedLock(Mutex& mutex, bool canBeSharedLock) : mMutex(mutex), mCanBeSharedLock(canBeSharedLock) - { - if (mCanBeSharedLock) - mMutex.lock_shared(); - else - mMutex.lock(); - } + /// @param mutex a mutex + /// @param threadCount decide wether the excluse lock will be taken + explicit MaybeExclusiveLock(Mutex& mutex, unsigned threadCount) + : mImpl(makeExclusiveLock(mutex, threadCount)) + {} - ~MaybeSharedLock() - { - if (mCanBeSharedLock) - mMutex.unlock_shared(); - else - mMutex.unlock(); - } private: - Mutex& mMutex; - bool mCanBeSharedLock; + std::optional> mImpl; }; - void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed) + template + std::optional> makeSharedLock(Mutex& mutex, unsigned threadCount) { - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mActorRaw->getOnGround()); - - if (isStillOnGround || actorData.mFlying || actorData.mSwimming || actorData.mSlowFall < 1) - actorData.mNeedLand = true; - else if (heightDiff < 0) - actorData.mFallHeight += heightDiff; + if (threadCount > 0) + return std::shared_lock(mutex); + return {}; } - void handleJump(const MWWorld::Ptr &ptr) + /// @brief A scoped lock that is either shared or inexistent depending on configuration + template + class MaybeSharedLock { - const bool isPlayer = (ptr == MWMechanics::getPlayer()); - // Advance acrobatics and set flag for GetPCJumping - if (isPlayer) - { - ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); - MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); - } + public: + /// @param mutex a shared mutex + /// @param threadCount decide wether the shared lock will be taken + explicit MaybeSharedLock(Mutex& mutex, unsigned threadCount) + : mImpl(makeSharedLock(mutex, threadCount)) + {} - // Decrease fatigue - if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) - { - const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); - const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); - const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); - const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; - MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); - } - ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; - } + private: + std::optional> mImpl; + }; - void updateStandingCollision(MWPhysics::ActorFrameData& actorData, MWPhysics::CollisionMap& standingCollisions) + template + std::variant, std::shared_lock> makeLock(Mutex& mutex, unsigned threadCount) { - if (!actorData.mStandingOn.isEmpty()) - standingCollisions[actorData.mPtr] = actorData.mStandingOn; - else - standingCollisions.erase(actorData.mPtr); + if (threadCount > 1) + return std::shared_lock(mutex); + if (threadCount == 1) + return std::unique_lock(mutex); + return std::monostate {}; } - void updateMechanics(MWPhysics::ActorFrameData& actorData) + /// @brief A scoped lock that is either shared, exclusive or inexistent depending on configuration + template + class MaybeLock { - if (actorData.mDidJump) - handleJump(actorData.mPtr); + public: + /// @param mutex a shared mutex + /// @param threadCount decide wether the lock will be shared, exclusive or inexistent + explicit MaybeLock(Mutex& mutex, unsigned threadCount) + : mImpl(makeLock(mutex, threadCount)) {} - MWMechanics::CreatureStats& stats = actorData.mPtr.getClass().getCreatureStats(actorData.mPtr); - if (actorData.mNeedLand) - stats.land(actorData.mPtr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); - else if (actorData.mFallHeight < 0) - stats.addToFallHeight(-actorData.mFallHeight); + private: + std::variant, std::shared_lock> mImpl; + }; + + bool isUnderWater(const MWPhysics::ActorFrameData& actorData) + { + return actorData.mPosition.z() < actorData.mSwimLevel; } - osg::Vec3f interpolateMovements(const MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { - const float interpolationFactor = timeAccum / physicsDt; - return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); + const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); + return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); } - struct WorldFrameData + using LockedActorSimulation = std::pair< + std::shared_ptr, + std::reference_wrapper + >; + using LockedProjectileSimulation = std::pair< + std::shared_ptr, + std::reference_wrapper + >; + + namespace Visitors { - WorldFrameData() : mIsInStorm(MWBase::Environment::get().getWorld()->isInStorm()) - , mStormDirection(MWBase::Environment::get().getWorld()->getStormDirection()) - {} + template class Lock> + struct WithLockedPtr + { + const Impl& mImpl; + std::shared_mutex& mCollisionWorldMutex; + const unsigned mNumThreads; - bool mIsInStorm; - osg::Vec3f mStormDirection; - }; + template + void operator()(MWPhysics::SimulationImpl& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto&& [ptr, frameData] = *std::move(locked); + // Locked shared_ptr has to be destructed after releasing mCollisionWorldMutex to avoid + // possible deadlock. Ptr destructor also acquires mCollisionWorldMutex. + const std::pair arg(std::move(ptr), frameData); + const Lock lock(mCollisionWorldMutex, mNumThreads); + mImpl(arg); + } + }; + + struct InitPosition + { + const btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [actor, frameDataRef] = *locked; + auto& frameData = frameDataRef.get(); + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) + { + const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z()); + MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset); + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + } + frameData.mOldHeight = frameData.mPosition.z(); + const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); + frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z()); + frameData.mInertia = actor->getInertialForce(); + frameData.mStuckFrames = actor->getStuckFrames(); + frameData.mLastStuckPosition = actor->getLastStuckPosition(); + } + void operator()(MWPhysics::ProjectileSimulation& /*sim*/) const + { + } + }; + + struct PreStep + { + btCollisionWorld* mCollisionWorld; + void operator()(const LockedActorSimulation& sim) const + { + MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); + } + void operator()(const LockedProjectileSimulation& /*sim*/) const + { + } + }; + + struct UpdatePosition + { + btCollisionWorld* mCollisionWorld; + void operator()(const LockedActorSimulation& sim) const + { + auto& [actor, frameDataRef] = sim; + auto& frameData = frameDataRef.get(); + if (actor->setPosition(frameData.mPosition)) + { + frameData.mPosition = actor->getPosition(); // account for potential position change made by script + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + } + void operator()(const LockedProjectileSimulation& sim) const + { + auto& [proj, frameDataRef] = sim; + auto& frameData = frameDataRef.get(); + proj->setPosition(frameData.mPosition); + proj->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(proj->getCollisionObject()); + } + }; + + struct Move + { + const float mPhysicsDt; + const btCollisionWorld* mCollisionWorld; + const MWPhysics::WorldFrameData& mWorldFrameData; + void operator()(const LockedActorSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); + } + void operator()(const LockedProjectileSimulation& sim) const + { + if (sim.first->isActive()) + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld); + } + }; + + struct Sync + { + const bool mAdvanceSimulation; + const float mTimeAccum; + const float mPhysicsDt; + const MWPhysics::PhysicsTaskScheduler* scheduler; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [actor, frameDataRef] = *locked; + auto& frameData = frameDataRef.get(); + auto ptr = actor->getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight; + const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround); + + if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + + actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt)); + actor->setLastStuckPosition(frameData.mLastStuckPosition); + actor->setStuckFrames(frameData.mStuckFrames); + if (mAdvanceSimulation) + { + MWWorld::Ptr standingOn; + auto* ptrHolder = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder) + 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 + if (actor->getOnGround() == frameData.mWasOnGround) + actor->setOnGround(frameData.mIsOnGround); + actor->setOnSlope(frameData.mIsOnSlope); + actor->setWalkingOnWater(frameData.mWalkingOnWater); + actor->setInertialForce(frameData.mInertia); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [proj, frameData] = *locked; + proj->setSimulationPosition(::interpolateMovements(*proj, mTimeAccum, mPhysicsDt)); + } + }; + } namespace Config { - /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading - int computeNumThreads(bool& threadSafeBullet) + /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading and user requested more than 1 background threads + unsigned computeNumThreads() { int wantedThread = Settings::Manager::getInt("async num threads", "Physics"); auto broad = std::make_unique(); auto maxSupportedThreads = broad->m_rayTestStacks.size(); - threadSafeBullet = (maxSupportedThreads > 1); + auto threadSafeBullet = (maxSupportedThreads > 1); if (!threadSafeBullet && wantedThread > 1) { Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used"; return 1; } - return std::max(0, wantedThread); + return static_cast(std::max(0, wantedThread)); } } } namespace MWPhysics { - PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, std::shared_ptr collisionWorld) - : mPhysicsDt(physicsDt) + PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, btCollisionWorld *collisionWorld, MWRender::DebugDrawer* debugDrawer) + : mDefaultPhysicsDt(physicsDt) + , mPhysicsDt(physicsDt) , mTimeAccum(0.f) - , mCollisionWorld(std::move(collisionWorld)) + , mCollisionWorld(collisionWorld) + , mDebugDrawer(debugDrawer) + , mNumThreads(Config::computeNumThreads()) , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) - , mDeferAabbUpdate(Settings::Manager::getBool("defer aabb update", "Physics")) - , mNewFrame(false) + , mFrameCounter(0) , mAdvanceSimulation(false) , mQuit(false) , mNextJob(0) , mNextLOS(0) + , mFrameNumber(0) + , mTimer(osg::Timer::instance()) + , mPrevStepCount(1) + , mBudget(physicsDt) + , mAsyncBudget(0.0f) + , mBudgetCursor(0) + , mAsyncStartTime(0) + , mTimeBegin(0) + , mTimeEnd(0) + , mFrameStart(0) { - mNumThreads = Config::computeNumThreads(mThreadSafeBullet); - if (mNumThreads >= 1) { - for (int i = 0; i < mNumThreads; ++i) + for (unsigned i = 0; i < mNumThreads; ++i) mThreads.emplace_back([&] { worker(); } ); } else { - mLOSCacheExpiry = -1; - mDeferAabbUpdate = false; + mLOSCacheExpiry = 0; } - mPreStepBarrier = std::make_unique(mNumThreads, [&]() - { - updateAabbs(); - }); + mPreStepBarrier = std::make_unique(mNumThreads); - mPostStepBarrier = std::make_unique(mNumThreads, [&]() - { - if (mRemainingSteps) - --mRemainingSteps; - mNextJob.store(0, std::memory_order_release); - updateActorsPositions(); - }); + mPostStepBarrier = std::make_unique(mNumThreads); - mPostSimBarrier = std::make_unique(mNumThreads, [&]() - { - udpateActorsAabbs(); - mNewFrame = false; - if (mLOSCacheExpiry >= 0) - { - std::unique_lock lock(mLOSCacheMutex); - mLOSCache.erase( - std::remove_if(mLOSCache.begin(), mLOSCache.end(), - [](const LOSRequest& req) { return req.mStale; }), - mLOSCache.end()); - } - }); + mPostSimBarrier = std::make_unique(mNumThreads); } PhysicsTaskScheduler::~PhysicsTaskScheduler() { - std::unique_lock lock(mSimulationMutex); - mQuit = true; - mNumJobs = 0; - mRemainingSteps = 0; - lock.unlock(); - mHasJob.notify_all(); + waitForWorkers(); + { + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); + mQuit = true; + mNumJobs = 0; + mRemainingSteps = 0; + mHasJob.notify_all(); + } for (auto& thread : mThreads) thread.join(); } - const PtrPositionList& PhysicsTaskScheduler::moveActors(int numSteps, float timeAccum, std::vector&& actorsData, CollisionMap& standingCollisions, bool skipSimulation) + std::tuple PhysicsTaskScheduler::calculateStepConfig(float timeAccum) const + { + int maxAllowedSteps = 2; + int numSteps = timeAccum / mDefaultPhysicsDt; + + // adjust maximum step count based on whether we're likely physics bottlenecked or not + // if maxAllowedSteps ends up higher than numSteps, we will not invoke delta time + // if it ends up lower than numSteps, but greater than 1, we will run a number of true delta time physics steps that we expect to be within budget + // if it ends up lower than numSteps and also 1, we will run a single delta time physics step + // if we did not do this, and had a fixed step count limit, + // we would have an unnecessarily low render framerate if we were only physics bottlenecked, + // and we would be unnecessarily invoking true delta time if we were only render bottlenecked + + // get physics timing stats + float budgetMeasurement = std::max(mBudget.get(), mAsyncBudget.get()); + // time spent per step in terms of the intended physics framerate + budgetMeasurement /= mDefaultPhysicsDt; + // ensure sane minimum value + budgetMeasurement = std::max(0.00001f, budgetMeasurement); + // we're spending almost or more than realtime per physics frame; limit to a single step + if (budgetMeasurement > 0.95) + maxAllowedSteps = 1; + // physics is fairly cheap; limit based on expense + if (budgetMeasurement < 0.5) + maxAllowedSteps = std::ceil(1.0/budgetMeasurement); + // limit to a reasonable amount + maxAllowedSteps = std::min(10, maxAllowedSteps); + + // fall back to delta time for this frame if fixed timestep physics would fall behind + float actualDelta = mDefaultPhysicsDt; + if (numSteps > maxAllowedSteps) + { + numSteps = maxAllowedSteps; + // ensure that we do not simulate a frame ahead when doing delta time; this reduces stutter and latency + // this causes interpolation to 100% use the most recent physics result when true delta time is happening + // and we deliberately simulate up to exactly the timestamp that we want to render + actualDelta = timeAccum/float(numSteps+1); + // actually: if this results in a per-step delta less than the target physics steptime, clamp it + // this might reintroduce some stutter, but only comes into play in obscure cases + // (because numSteps is originally based on mDefaultPhysicsDt, this won't cause us to overrun) + actualDelta = std::max(actualDelta, mDefaultPhysicsDt); + } + + return std::make_tuple(numSteps, actualDelta); + } + + void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { + waitForWorkers(); + // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. - std::unique_lock lock(mSimulationMutex); + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); + + double timeStart = mTimer->tick(); // start by finishing previous background computation if (mNumThreads != 0) { - if (mAdvanceSimulation) - standingCollisions.clear(); + syncWithMainThread(); - for (auto& data : mActorsFrameData) - { - // Ignore actors that were deleted while the background thread was running - if (!data.mActor.lock()) - continue; - - updateMechanics(data); - if (mAdvanceSimulation) - updateStandingCollision(data, standingCollisions); - } + if(mAdvanceSimulation) + mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); + updateStats(frameStart, frameNumber, stats); } + auto [numSteps, newDelta] = calculateStepConfig(timeAccum); + timeAccum -= numSteps*newDelta; + // init + const Visitors::InitPosition vis{mCollisionWorld}; + for (auto& sim : simulations) + { + std::visit(vis, sim); + } + mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; - mActorsFrameData = std::move(actorsData); + mPhysicsDt = newDelta; + mSimulations = std::move(simulations); mAdvanceSimulation = (mRemainingSteps != 0); - mNewFrame = true; - mNumJobs = mActorsFrameData.size(); + ++mFrameCounter; + mNumJobs = mSimulations.size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); if (mAdvanceSimulation) mWorldFrameData = std::make_unique(); - // update each actor position based on latest data - for (auto& data : mActorsFrameData) - data.updatePosition(); - - // we are asked to skip the simulation (load a savegame for instance) - // just return the actors' reference position without applying the movements - if (skipSimulation) - { - standingCollisions.clear(); - mMovementResults.clear(); - for (const auto& m : mActorsFrameData) - mMovementResults[m.mPtr] = m.mPosition; - return mMovementResults; - } + if (mAdvanceSimulation) + mBudgetCursor += 1; if (mNumThreads == 0) { - mMovementResults.clear(); - syncComputation(); - - if (mAdvanceSimulation) - { - standingCollisions.clear(); - for (auto& data : mActorsFrameData) - updateStandingCollision(data, standingCollisions); - } - return mMovementResults; + doSimulation(); + syncWithMainThread(); + if(mAdvanceSimulation) + mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); + return; } - // Remove actors that were deleted while the background thread was running - for (auto& data : mActorsFrameData) + mAsyncStartTime = mTimer->tick(); + mHasJob.notify_all(); + if (mAdvanceSimulation) + mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); + } + + void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) + { + waitForWorkers(); + MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); + mBudget.reset(mDefaultPhysicsDt); + mAsyncBudget.reset(0.0f); + mSimulations.clear(); + for (const auto& [_, actor] : actors) { - if (!data.mActor.lock()) - mMovementResults.erase(data.mPtr); + actor->updatePosition(); + actor->updateCollisionObjectPosition(); } - std::swap(mMovementResults, mPreviousMovementResults); - - // mMovementResults is shared between all workers instance - // pre-allocate all nodes so that we don't need synchronization - mMovementResults.clear(); - for (const auto& m : mActorsFrameData) - mMovementResults[m.mPtr] = m.mPosition; - - lock.unlock(); - mHasJob.notify_all(); - return mPreviousMovementResults; } void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->rayTest(rayFromWorld, rayToWorld, resultCallback); } void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback); } void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { - std::shared_lock lock(mCollisionWorldMutex); - mCollisionWorld->contactTest(colObj, resultCallback); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); + ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mNumThreads); // target the collision object's world origin, this should be the center of the collision object btTransform rayTo; rayTo.setIdentity(); @@ -330,64 +523,59 @@ namespace MWPhysics void PhysicsTaskScheduler::aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback); } void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max); } void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask; } void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { - std::unique_lock lock(mCollisionWorldMutex); + mCollisionObjects.insert(collisionObject); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { - std::unique_lock lock(mCollisionWorldMutex); + mCollisionObjects.erase(collisionObject); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); mCollisionWorld->removeCollisionObject(collisionObject); } - void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr) + void PhysicsTaskScheduler::updateSingleAabb(const std::shared_ptr& ptr, bool immediate) { - if (mDeferAabbUpdate) + if (immediate || mNumThreads == 0) { - std::unique_lock lock(mUpdateAabbMutex); - mUpdateAabb.insert(std::move(ptr)); + updatePtrAabb(ptr); } else { - std::unique_lock lock(mCollisionWorldMutex); - updatePtrAabb(ptr); + MaybeExclusiveLock lock(mUpdateAabbMutex, mNumThreads); + mUpdateAabb.insert(ptr); } } - bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2) + bool PhysicsTaskScheduler::getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2) { - std::unique_lock lock(mLOSCacheMutex); - - auto actorPtr1 = actor1.lock(); - auto actorPtr2 = actor2.lock(); - if (!actorPtr1 || !actorPtr2) - return false; + MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads); auto req = LOSRequest(actor1, actor2); auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req); if (result == mLOSCache.end()) { - req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); - if (mLOSCacheExpiry >= 0) - mLOSCache.push_back(req); + req.mResult = hasLineOfSight(actor1.get(), actor2.get()); + mLOSCache.push_back(req); return req.mResult; } result->mAge = 0; @@ -396,7 +584,7 @@ namespace MWPhysics void PhysicsTaskScheduler::refreshLOSCache() { - std::shared_lock lock(mLOSCacheMutex); + MaybeSharedLock lock(mLOSCacheMutex, mNumThreads); int job = 0; int numLOS = mLOSCache.size(); while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS) @@ -415,96 +603,59 @@ namespace MWPhysics void PhysicsTaskScheduler::updateAabbs() { - std::scoped_lock lock(mCollisionWorldMutex, mUpdateAabbMutex); + MaybeExclusiveLock lock(mUpdateAabbMutex, mNumThreads); std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), - [this](const std::weak_ptr& ptr) { updatePtrAabb(ptr); }); + [this](const std::weak_ptr& ptr) + { + auto p = ptr.lock(); + if (p != nullptr) + updatePtrAabb(p); + }); mUpdateAabb.clear(); } - void PhysicsTaskScheduler::updatePtrAabb(const std::weak_ptr& ptr) + void PhysicsTaskScheduler::updatePtrAabb(const std::shared_ptr& ptr) { - if (const auto p = ptr.lock()) + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); + if (const auto actor = std::dynamic_pointer_cast(ptr)) { - if (const auto actor = std::dynamic_pointer_cast(p)) - { - actor->commitPositionChange(); - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } - else if (const auto object = std::dynamic_pointer_cast(p)) - { - object->commitPositionChange(); - mCollisionWorld->updateSingleAabb(object->getCollisionObject()); - } - }; + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + else if (const auto object = std::dynamic_pointer_cast(ptr)) + { + object->commitPositionChange(); + mCollisionWorld->updateSingleAabb(object->getCollisionObject()); + } + else if (const auto projectile = std::dynamic_pointer_cast(ptr)) + { + projectile->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); + } } void PhysicsTaskScheduler::worker() { + std::size_t lastFrame = 0; std::shared_lock lock(mSimulationMutex); while (!mQuit) { - if (!mNewFrame) - mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); - - if (mDeferAabbUpdate) - mPreStepBarrier->wait(); - - int job = 0; - while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) + if (lastFrame == mFrameCounter) { - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); - if(const auto actor = mActorsFrameData[job].mActor.lock()) - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + mHasJob.wait(lock, [&] { return mQuit || lastFrame != mFrameCounter; }); + lastFrame = mFrameCounter; } - mPostStepBarrier->wait(); - - if (!mRemainingSteps) - { - while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) - { - if(const auto actor = mActorsFrameData[job].mActor.lock()) - { - auto& actorData = mActorsFrameData[job]; - handleFall(actorData, mAdvanceSimulation); - mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt); - } - } - - if (mLOSCacheExpiry >= 0) - refreshLOSCache(); - mPostSimBarrier->wait(); - } + doSimulation(); } } void PhysicsTaskScheduler::updateActorsPositions() { - std::unique_lock lock(mCollisionWorldMutex); - for (auto& actorData : mActorsFrameData) - { - if(const auto actor = actorData.mActor.lock()) - { - if (actorData.mPosition == actor->getPosition()) - actor->setPosition(actorData.mPosition, false); // update previous position to make sure interpolation is correct - else - { - actorData.mPositionChanged = true; - actor->setPosition(actorData.mPosition); - } - } - } - } - - void PhysicsTaskScheduler::udpateActorsAabbs() - { - std::unique_lock lock(mCollisionWorldMutex); - for (const auto& actorData : mActorsFrameData) - if (actorData.mPositionChanged) - { - if(const auto actor = actorData.mActor.lock()) - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } + const Visitors::UpdatePosition impl{mCollisionWorld}; + const Visitors::WithLockedPtr vis{impl, mCollisionWorldMutex, mNumThreads}; + for (Simulation& sim : mSimulations) + std::visit(vis, sim); } bool PhysicsTaskScheduler::hasLineOfSight(const Actor* actor1, const Actor* actor2) @@ -513,31 +664,126 @@ namespace MWPhysics btVector3 pos2 = Misc::Convert::toBullet(actor2->getCollisionObjectPosition() + osg::Vec3f(0,0,actor2->getHalfExtents().z() * 0.9)); btCollisionWorld::ClosestRayResultCallback resultCallback(pos1, pos2); - resultCallback.m_collisionFilterGroup = 0xFF; + resultCallback.m_collisionFilterGroup = CollisionType_AnyPhysical; resultCallback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); mCollisionWorld->rayTest(pos1, pos2, resultCallback); return !resultCallback.hasHit(); } - void PhysicsTaskScheduler::syncComputation() + void PhysicsTaskScheduler::doSimulation() + { + while (mRemainingSteps) + { + mPreStepBarrier->wait([this] { afterPreStep(); }); + int job = 0; + const Visitors::Move impl{mPhysicsDt, mCollisionWorld, *mWorldFrameData}; + const Visitors::WithLockedPtr vis{impl, mCollisionWorldMutex, mNumThreads}; + while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) + std::visit(vis, mSimulations[job]); + + mPostStepBarrier->wait([this] { afterPostStep(); }); + } + + refreshLOSCache(); + mPostSimBarrier->wait([this] { afterPostSim(); }); + } + + void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { - while (mRemainingSteps--) + if (!stats.collectStats("engine")) + return; + if (mFrameNumber == frameNumber - 1) { - for (auto& actorData : mActorsFrameData) - MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin)); + stats.setAttribute(mFrameNumber, "physicsworker_time_taken", mTimer->delta_s(mTimeBegin, mTimeEnd)); + stats.setAttribute(mFrameNumber, "physicsworker_time_end", mTimer->delta_s(mFrameStart, mTimeEnd)); + } + mFrameStart = frameStart; + mTimeBegin = mTimer->tick(); + mFrameNumber = frameNumber; + } + + void PhysicsTaskScheduler::debugDraw() + { + MaybeSharedLock lock(mCollisionWorldMutex, mNumThreads); + mDebugDrawer->step(); + } + void* PhysicsTaskScheduler::getUserPointer(const btCollisionObject* object) const + { + auto it = mCollisionObjects.find(object); + if (it == mCollisionObjects.end()) + return nullptr; + return (*it)->getUserPointer(); + } + + void PhysicsTaskScheduler::releaseSharedStates() + { + waitForWorkers(); + std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); + mSimulations.clear(); + mUpdateAabb.clear(); + } + + void PhysicsTaskScheduler::afterPreStep() + { + updateAabbs(); + if (!mRemainingSteps) + return; + const Visitors::PreStep impl{mCollisionWorld}; + const Visitors::WithLockedPtr vis{impl, mCollisionWorldMutex, mNumThreads}; + for (auto& sim : mSimulations) + std::visit(vis, sim); + } + + void PhysicsTaskScheduler::afterPostStep() + { + if (mRemainingSteps) + { + --mRemainingSteps; updateActorsPositions(); } + mNextJob.store(0, std::memory_order_release); + } - for (auto& actorData : mActorsFrameData) + void PhysicsTaskScheduler::afterPostSim() + { { - handleFall(actorData, mAdvanceSimulation); - mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt); - updateMechanics(actorData); + MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads); + mLOSCache.erase( + std::remove_if(mLOSCache.begin(), mLOSCache.end(), + [](const LOSRequest& req) { return req.mStale; }), + mLOSCache.end()); } - udpateActorsAabbs(); + mTimeEnd = mTimer->tick(); + + std::unique_lock lock(mWorkersDoneMutex); + ++mWorkersFrameCounter; + mWorkersDone.notify_all(); + } + + void PhysicsTaskScheduler::syncWithMainThread() + { + const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this}; + for (auto& sim : mSimulations) + std::visit(vis, sim); + } + + // Attempt to acquire unique lock on mSimulationMutex while not all worker + // threads are holding shared lock but will have to may lead to a deadlock because + // C++ standard does not guarantee priority for exclusive and shared locks + // for std::shared_mutex. For example microsoft STL implementation points out + // for the absence of such priority: + // https://docs.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks + void PhysicsTaskScheduler::waitForWorkers() + { + if (mNumThreads == 0) + return; + std::unique_lock lock(mWorkersDoneMutex); + if (mFrameCounter != mWorkersFrameCounter) + mWorkersDone.wait(lock); } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 100e71a907..74556ccd81 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -6,23 +6,33 @@ #include #include #include +#include +#include #include +#include + #include "physicssystem.hpp" #include "ptrholder.hpp" +#include "components/misc/budgetmeasurement.hpp" namespace Misc { class Barrier; } +namespace MWRender +{ + class DebugDrawer; +} + namespace MWPhysics { class PhysicsTaskScheduler { public: - PhysicsTaskScheduler(float physicsDt, std::shared_ptr collisionWorld); + PhysicsTaskScheduler(float physicsDt, btCollisionWorld* collisionWorld, MWRender::DebugDrawer* debugDrawer); ~PhysicsTaskScheduler(); /// @brief move actors taking into account desired movements and collisions @@ -30,7 +40,9 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - const PtrPositionList& moveActors(int numSteps, float timeAccum, std::vector&& actorsData, CollisionMap& standingCollisions, bool skip); + void applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + void resetSimulation(const ActorMap& actors); // Thread safe wrappers void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const; @@ -42,26 +54,36 @@ namespace MWPhysics void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); void removeCollisionObject(btCollisionObject* collisionObject); - void updateSingleAabb(std::weak_ptr ptr); - bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); + void updateSingleAabb(const std::shared_ptr& ptr, bool immediate=false); + bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); + void debugDraw(); + void* getUserPointer(const btCollisionObject* object) const; + void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: - void syncComputation(); + void doSimulation(); void worker(); void updateActorsPositions(); - void udpateActorsAabbs(); bool hasLineOfSight(const Actor* actor1, const Actor* actor2); void refreshLOSCache(); void updateAabbs(); - void updatePtrAabb(const std::weak_ptr& ptr); + void updatePtrAabb(const std::shared_ptr& ptr); + void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + std::tuple calculateStepConfig(float timeAccum) const; + void afterPreStep(); + void afterPostStep(); + void afterPostSim(); + void syncWithMainThread(); + void waitForWorkers(); std::unique_ptr mWorldFrameData; - std::vector mActorsFrameData; - PtrPositionList mMovementResults; - PtrPositionList mPreviousMovementResults; - const float mPhysicsDt; + std::vector mSimulations; + std::unordered_set mCollisionObjects; + float mDefaultPhysicsDt; + float mPhysicsDt; float mTimeAccum; - std::shared_ptr mCollisionWorld; + btCollisionWorld* mCollisionWorld; + MWRender::DebugDrawer* mDebugDrawer; std::vector mLOSCache; std::set, std::owner_less>> mUpdateAabb; @@ -70,24 +92,38 @@ namespace MWPhysics std::unique_ptr mPostStepBarrier; std::unique_ptr mPostSimBarrier; - int mNumThreads; + unsigned mNumThreads; int mNumJobs; int mRemainingSteps; int mLOSCacheExpiry; - bool mDeferAabbUpdate; - bool mNewFrame; + std::size_t mFrameCounter; bool mAdvanceSimulation; - bool mThreadSafeBullet; bool mQuit; std::atomic mNextJob; std::atomic mNextLOS; std::vector mThreads; + std::size_t mWorkersFrameCounter = 0; + std::condition_variable mWorkersDone; + std::mutex mWorkersDoneMutex; + mutable std::shared_mutex mSimulationMutex; mutable std::shared_mutex mCollisionWorldMutex; mutable std::shared_mutex mLOSCacheMutex; mutable std::mutex mUpdateAabbMutex; std::condition_variable_any mHasJob; + + unsigned int mFrameNumber; + const osg::Timer* mTimer; + + int mPrevStepCount; + Misc::BudgetMeasurement mBudget; + Misc::BudgetMeasurement mAsyncBudget; + unsigned int mBudgetCursor; + osg::Timer_t mAsyncStartTime; + osg::Timer_t mTimeBegin; + osg::Timer_t mTimeEnd; + osg::Timer_t mFrameStart; }; } diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index c822bbcbe0..f222a38340 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -6,37 +6,33 @@ #include #include #include +#include #include -#include #include namespace MWPhysics { - Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, PhysicsTaskScheduler* scheduler) - : mShapeInstance(shapeInstance) + Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, osg::Quat rotation, int collisionType, PhysicsTaskScheduler* scheduler) + : mShapeInstance(std::move(shapeInstance)) , mSolid(true) + , mScale(ptr.getCellRef().getScale(), ptr.getCellRef().getScale(), ptr.getCellRef().getScale()) + , mPosition(ptr.getRefData().getPosition().asVec3()) + , mRotation(rotation) , mTaskScheduler(scheduler) { mPtr = ptr; - - mCollisionObject.reset(new btCollisionObject); - mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape()); - - mCollisionObject->setUserPointer(static_cast(this)); - - setScale(ptr.getCellRef().getScale()); - setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude())); - const float* pos = ptr.getRefData().getPosition().pos; - setOrigin(btVector3(pos[0], pos[1], pos[2])); - commitPositionChange(); + mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); + mCollisionObject->setUserPointer(this); + mShapeInstance->setLocalScaling(mScale); + mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); } Object::~Object() { - if (mCollisionObject) - mTaskScheduler->removeCollisionObject(mCollisionObject.get()); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } const Resource::BulletShapeInstance* Object::getShapeInstance() const @@ -51,17 +47,17 @@ namespace MWPhysics mScaleUpdatePending = true; } - void Object::setRotation(const btQuaternion& quat) + void Object::setRotation(osg::Quat quat) { std::unique_lock lock(mPositionMutex); - mLocalTransform.setRotation(quat); + mRotation = quat; mTransformUpdatePending = true; } - void Object::setOrigin(const btVector3& vec) + void Object::updatePosition() { std::unique_lock lock(mPositionMutex); - mLocalTransform.setOrigin(vec); + mPosition = mPtr.getRefData().getPosition().asVec3(); mTransformUpdatePending = true; } @@ -75,25 +71,21 @@ namespace MWPhysics } if (mTransformUpdatePending) { - mCollisionObject->setWorldTransform(mLocalTransform); + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } - btCollisionObject* Object::getCollisionObject() - { - return mCollisionObject.get(); - } - - const btCollisionObject* Object::getCollisionObject() const - { - return mCollisionObject.get(); - } - btTransform Object::getTransform() const { std::unique_lock lock(mPositionMutex); - return mLocalTransform; + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + return trans; } bool Object::isSolid() const @@ -108,7 +100,7 @@ namespace MWPhysics bool Object::isAnimated() const { - return !mShapeInstance->mAnimatedShapes.empty(); + return mShapeInstance->isAnimated(); } bool Object::animateCollisionShapes() @@ -116,14 +108,12 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; - assert (mShapeInstance->getCollisionShape()->isCompound()); + assert (mShapeInstance->mCollisionShape->isCompound()); - btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); - for (const auto& shape : mShapeInstance->mAnimatedShapes) + btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); + bool result = false; + for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { - int recIndex = shape.first; - int shapeIndex = shape.second; - auto nodePathFound = mRecIndexToNodePath.find(recIndex); if (nodePathFound == mRecIndexToNodePath.end()) { @@ -155,8 +145,11 @@ namespace MWPhysics // Note: we can not apply scaling here for now since we treat scaled shapes // as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now if (!(transform == compound->getChildTransform(shapeIndex))) + { compound->updateChildTransform(shapeIndex, transform); + result = true; + } } - return true; + return result; } } diff --git a/apps/openmw/mwphysics/object.hpp b/apps/openmw/mwphysics/object.hpp index 876e35651f..520a57a4e8 100644 --- a/apps/openmw/mwphysics/object.hpp +++ b/apps/openmw/mwphysics/object.hpp @@ -16,7 +16,6 @@ namespace Resource } class btCollisionObject; -class btQuaternion; class btVector3; namespace MWPhysics @@ -26,16 +25,14 @@ namespace MWPhysics class Object final : public PtrHolder { public: - Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, PhysicsTaskScheduler* scheduler); + Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, osg::Quat rotation, int collisionType, PhysicsTaskScheduler* scheduler); ~Object() override; const Resource::BulletShapeInstance* getShapeInstance() const; void setScale(float scale); - void setRotation(const btQuaternion& quat); - void setOrigin(const btVector3& vec); + void setRotation(osg::Quat quat); + void updatePosition(); void commitPositionChange(); - btCollisionObject* getCollisionObject(); - const btCollisionObject* getCollisionObject() const; btTransform getTransform() const; /// Return solid flag. Not used by the object itself, true by default. bool isSolid() const; @@ -46,14 +43,14 @@ namespace MWPhysics bool animateCollisionShapes(); private: - std::unique_ptr mCollisionObject; osg::ref_ptr mShapeInstance; std::map mRecIndexToNodePath; bool mSolid; btVector3 mScale; - btTransform mLocalTransform; - bool mScaleUpdatePending; - bool mTransformUpdatePending; + osg::Vec3f mPosition; + osg::Quat mRotation; + bool mScaleUpdatePending = false; + bool mTransformUpdatePending = false; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; }; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 9a777bd454..f4718e0d8f 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -1,30 +1,30 @@ #include "physicssystem.hpp" -#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 // FindRecIndexVisitor @@ -38,6 +38,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/player.hpp" #include "../mwrender/bulletdebugdraw.hpp" @@ -45,6 +46,8 @@ #include "collisiontype.hpp" #include "actor.hpp" + +#include "projectile.hpp" #include "trace.h" #include "object.hpp" #include "heightfield.hpp" @@ -52,17 +55,51 @@ #include "deepestnotmecontacttestresultcallback.hpp" #include "closestnotmerayresultcallback.hpp" #include "contacttestresultcallback.hpp" -#include "constants.hpp" +#include "projectileconvexcallback.hpp" #include "movementsolver.hpp" #include "mtphysics.hpp" +namespace +{ + void handleJump(const MWWorld::Ptr &ptr) + { + if (!ptr.getClass().isActor()) + return; + if (ptr.getClass().getMovementSettings(ptr).mPosition[2] == 0) + return; + const bool isPlayer = (ptr == MWMechanics::getPlayer()); + // Advance acrobatics and set flag for GetPCJumping + if (isPlayer) + { + ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); + MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); + } + + // Decrease fatigue + if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) + { + const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); + const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); + const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); + const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; + MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); + } + ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; + } + +} + namespace MWPhysics { PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode) - : mShapeManager(new Resource::BulletShapeManager(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager())) + : mShapeManager(std::make_unique(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager())) , mResourceSystem(resourceSystem) , mDebugDrawEnabled(false) , mTimeAccum(0.0f) + , mProjectileId(0) , mWaterHeight(0) , mWaterEnabled(false) , mParentNode(parentNode) @@ -74,7 +111,7 @@ namespace MWPhysics mDispatcher = std::make_unique(mCollisionConfiguration.get()); mBroadphase = std::make_unique(); - mCollisionWorld = std::make_shared(mDispatcher.get(), mBroadphase.get(), mCollisionConfiguration.get()); + mCollisionWorld = std::make_unique(mDispatcher.get(), mBroadphase.get(), mCollisionConfiguration.get()); // Don't update AABBs of all objects every frame. Most objects in MW are static, so we don't need this. // Should a "static" object ever be moved, we have to update its AABB manually using DynamicsWorld::updateSingleAabb. @@ -92,8 +129,8 @@ namespace MWPhysics } } - mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld); - mDebugDrawer = std::make_unique(mParentNode, mCollisionWorld.get()); + mDebugDrawer = std::make_unique(mParentNode, mCollisionWorld.get(), mDebugDrawEnabled); + mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld.get(), mDebugDrawer.get()); } PhysicsSystem::~PhysicsSystem() @@ -103,20 +140,11 @@ namespace MWPhysics if (mWaterCollisionObject) mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); - for (auto& heightField : mHeightFields) - { - mTaskScheduler->removeCollisionObject(heightField.second->getCollisionObject()); - delete heightField.second; - } - + mTaskScheduler->releaseSharedStates(); + mHeightFields.clear(); mObjects.clear(); mActors.clear(); - - } - - void PhysicsSystem::setUnrefQueue(SceneUtil::UnrefQueue *unrefQueue) - { - mUnrefQueue = unrefQueue; + mProjectiles.clear(); } Resource::BulletShapeManager *PhysicsSystem::getShapeManager() @@ -135,7 +163,7 @@ namespace MWPhysics void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); + ObjectMap::iterator found = mObjects.find(ptr.mRef); if (found == mObjects.end()) return; @@ -145,14 +173,14 @@ namespace MWPhysics bool PhysicsSystem::isOnSolidGround (const MWWorld::Ptr& actor) const { const Actor* physactor = getActor(actor); - if (!physactor || !physactor->getOnGround()) + if (!physactor || !physactor->getOnGround() || !physactor->getCollisionMode()) return false; - CollisionMap::const_iterator found = mStandingCollisions.find(actor); - if (found == mStandingCollisions.end()) + const auto obj = physactor->getStandingOnPtr(); + if (obj.isEmpty()) return true; // assume standing on terrain (which is a non-object, so not collision tracked) - ObjectMap::const_iterator foundObj = mObjects.find(found->second); + ObjectMap::const_iterator foundObj = mObjects.find(obj.mRef); if (foundObj == mObjects.end()) return false; @@ -247,7 +275,7 @@ namespace MWPhysics return 0.f; } - RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, std::vector targets, int mask, int group) const + RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, const std::vector& targets, int mask, int group) const { if (from == to) { @@ -276,7 +304,7 @@ namespace MWPhysics if (!targets.empty()) { - for (MWWorld::Ptr& target : targets) + for (const MWWorld::Ptr& target : targets) { const Actor* actor = getActor(target); if (actor) @@ -302,11 +330,11 @@ namespace MWPhysics return result; } - RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius) const + RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius, int mask, int group) const { btCollisionWorld::ClosestConvexResultCallback callback(Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); - callback.m_collisionFilterGroup = 0xff; - callback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; + callback.m_collisionFilterGroup = group; + callback.m_collisionFilterMask = mask; btSphereShape shape(radius); const btQuaternion btrot = btQuaternion::getIdentity(); @@ -322,41 +350,34 @@ namespace MWPhysics { result.mHitPos = Misc::Convert::toOsg(callback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(callback.m_hitNormalWorld); + if (auto* ptrHolder = static_cast(callback.m_hitCollisionObject->getUserPointer())) + result.mHitObject = ptrHolder->getPtr(); } return result; } bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { - const auto getWeakPtr = [&](const MWWorld::ConstPtr &ptr) -> std::weak_ptr - { - const auto found = mActors.find(ptr); - if (found != mActors.end()) - return { found->second }; - return {}; - }; + if (actor1 == actor2) return true; + + const auto it1 = mActors.find(actor1.mRef); + const auto it2 = mActors.find(actor2.mRef); + if (it1 == mActors.end() || it2 == mActors.end()) + return false; - return mTaskScheduler->getLineOfSight(getWeakPtr(actor1), getWeakPtr(actor2)); + return mTaskScheduler->getLineOfSight(it1->second, it2->second); } bool PhysicsSystem::isOnGround(const MWWorld::Ptr &actor) { Actor* physactor = getActor(actor); - return physactor && physactor->getOnGround(); + return physactor && physactor->getOnGround() && physactor->getCollisionMode(); } bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) { - const Actor* physicActor = getActor(actor); - if (!physicActor) - return false; - const float halfZ = physicActor->getHalfExtents().z(); - const osg::Vec3f actorPosition = physicActor->getPosition(); - const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); - const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); - ActorTracer tracer; - tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, mCollisionWorld.get()); - return (tracer.mFraction >= 1.0f); + const auto* physactor = getActor(actor); + return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const @@ -407,7 +428,7 @@ namespace MWPhysics { btCollisionObject* me = nullptr; - auto found = mObjects.find(ptr); + auto found = mObjects.find(ptr.mRef); if (found != mObjects.end()) me = found->second->getCollisionObject(); else @@ -430,31 +451,22 @@ namespace MWPhysics osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found == mActors.end()) return ptr.getRefData().getPosition().asVec3(); - else - return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); + return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); } - void PhysicsSystem::addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject) + void PhysicsSystem::addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject) { - HeightField *heightfield = new HeightField(heights, x, y, triSize, sqrtVerts, minH, maxH, holdObject); - mHeightFields[std::make_pair(x,y)] = heightfield; - - mTaskScheduler->addCollisionObject(heightfield->getCollisionObject(), CollisionType_HeightMap, - CollisionType_Actor|CollisionType_Projectile); + mHeightFields[std::make_pair(x,y)] = std::make_unique(heights, x, y, size, verts, minH, maxH, holdObject, mTaskScheduler.get()); } void PhysicsSystem::removeHeightField (int x, int y) { HeightFieldMap::iterator heightfield = mHeightFields.find(std::make_pair(x,y)); if(heightfield != mHeightFields.end()) - { - mTaskScheduler->removeCollisionObject(heightfield->second->getCollisionObject()); - delete heightfield->second; mHeightFields.erase(heightfield); - } } const HeightField* PhysicsSystem::getHeightField(int x, int y) const @@ -462,87 +474,82 @@ namespace MWPhysics const auto heightField = mHeightFields.find(std::make_pair(x, y)); if (heightField == mHeightFields.end()) return nullptr; - return heightField->second; + return heightField->second.get(); } - void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType) + void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType) { + if (ptr.mRef->mData.mPhysicsPostponed) + return; osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); - if (!shapeInstance || !shapeInstance->getCollisionShape()) + if (!shapeInstance || !shapeInstance->mCollisionShape) return; - auto obj = std::make_shared(ptr, shapeInstance, mTaskScheduler.get()); - mObjects.emplace(ptr, obj); + assert(!getObject(ptr)); - if (obj->isAnimated()) - mAnimatedObjects.insert(obj.get()); + // Override collision type based on shape content. + switch (shapeInstance->mCollisionType) + { + case Resource::BulletShape::CollisionType::Camera: + collisionType = CollisionType_CameraOnly; + break; + case Resource::BulletShape::CollisionType::None: + collisionType = CollisionType_VisualOnly; + break; + } + + auto obj = std::make_shared(ptr, shapeInstance, rotation, collisionType, mTaskScheduler.get()); + mObjects.emplace(ptr.mRef, obj); - mTaskScheduler->addCollisionObject(obj->getCollisionObject(), collisionType, - CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); + if (obj->isAnimated()) + mAnimatedObjects.emplace(obj.get(), false); } void PhysicsSystem::remove(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(found->second->getShapeInstance()); + mAnimatedObjects.erase(foundObject->second.get()); - mAnimatedObjects.erase(found->second.get()); - - mObjects.erase(found); + mObjects.erase(foundObject); } - - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { mActors.erase(foundActor); } } - void PhysicsSystem::updateCollisionMapPtr(CollisionMap& map, const MWWorld::Ptr &old, const MWWorld::Ptr &updated) + void PhysicsSystem::removeProjectile(const int projectileId) { - CollisionMap::iterator found = map.find(old); - if (found != map.end()) - { - map[updated] = found->second; - map.erase(found); - } - - for (auto& collision : map) - { - if (collision.second == old) - collision.second = updated; - } + ProjectileMap::iterator foundProjectile = mProjectiles.find(projectileId); + if (foundProjectile != mProjectiles.end()) + mProjectiles.erase(foundProjectile); } void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { - ObjectMap::iterator found = mObjects.find(old); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) + foundObject->second->updatePtr(updated); + else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) + foundActor->second->updatePtr(updated); + + for (auto& [_, actor] : mActors) { - auto obj = found->second; - obj->updatePtr(updated); - mObjects.erase(found); - mObjects.emplace(updated, std::move(obj)); + if (actor->getStandingOnPtr() == old) + actor->setStandingOnPtr(updated); } - ActorMap::iterator foundActor = mActors.find(old); - if (foundActor != mActors.end()) + for (auto& [_, projectile] : mProjectiles) { - auto actor = foundActor->second; - actor->updatePtr(updated); - mActors.erase(foundActor); - mActors.emplace(updated, std::move(actor)); + if (projectile->getCaster() == old) + projectile->setCaster(updated); } - updateCollisionMapPtr(mStandingCollisions, old, updated); } Actor *PhysicsSystem::getActor(const MWWorld::Ptr &ptr) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; @@ -550,7 +557,7 @@ namespace MWPhysics const Actor *PhysicsSystem::getActor(const MWWorld::ConstPtr &ptr) const { - ActorMap::const_iterator found = mActors.find(ptr); + ActorMap::const_iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; @@ -558,67 +565,63 @@ namespace MWPhysics const Object* PhysicsSystem::getObject(const MWWorld::ConstPtr &ptr) const { - ObjectMap::const_iterator found = mObjects.find(ptr); + ObjectMap::const_iterator found = mObjects.find(ptr.mRef); if (found != mObjects.end()) return found->second.get(); return nullptr; } + Projectile* PhysicsSystem::getProjectile(int projectileId) const + { + ProjectileMap::const_iterator found = mProjectiles.find(projectileId); + if (found != mProjectiles.end()) + return found->second.get(); + return nullptr; + } + void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { float scale = ptr.getCellRef().getScale(); - found->second->setScale(scale); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setScale(scale); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updateScale(); mTaskScheduler->updateSingleAabb(foundActor->second); - return; } } - void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr) + void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude())); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setRotation(rotate); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { if (!foundActor->second->isRotationallyInvariant()) { - foundActor->second->updateRotation(); + foundActor->second->setRotation(rotate); mTaskScheduler->updateSingleAabb(foundActor->second); } - return; } } void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3())); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->updatePosition(); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updatePosition(); - mTaskScheduler->updateSingleAabb(foundActor->second); - return; + mTaskScheduler->updateSingleAabb(foundActor->second, true); } } @@ -627,7 +630,7 @@ namespace MWPhysics osg::ref_ptr shape = mShapeManager->getShape(mesh); // Try to get shape from basic model as fallback for creatures - if (!ptr.getClass().isNpc() && shape && shape->mCollisionBoxHalfExtents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) @@ -639,13 +642,41 @@ namespace MWPhysics if (!shape) return; - auto actor = std::make_shared(ptr, shape, mTaskScheduler.get()); - mActors.emplace(ptr, std::move(actor)); + // check if Actor should spawn above water + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); + const bool canWaterWalk = effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + + auto actor = std::make_shared(ptr, shape, mTaskScheduler.get(), canWaterWalk); + + mActors.emplace(ptr.mRef, std::move(actor)); + } + + int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) + { + osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); + assert(shapeInstance); + float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; + + mProjectileId++; + + auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); + mProjectiles.emplace(mProjectileId, std::move(projectile)); + + return mProjectileId; + } + + void PhysicsSystem::setCaster(int projectileId, const MWWorld::Ptr& caster) + { + const auto foundProjectile = mProjectiles.find(projectileId); + assert(foundProjectile != mProjectiles.end()); + auto* projectile = foundProjectile->second.get(); + + projectile->setCaster(caster); } bool PhysicsSystem::toggleCollisionMode() { - ActorMap::iterator found = mActors.find(MWMechanics::getPlayer()); + ActorMap::iterator found = mActors.find(MWMechanics::getPlayer().mRef); if (found != mActors.end()) { bool cmode = found->second->getCollisionMode(); @@ -660,107 +691,125 @@ namespace MWPhysics void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) { - for(auto& movementItem : mMovementQueue) - { - if (movementItem.first == ptr) - { - movementItem.second = velocity; - return; - } - } - - mMovementQueue.emplace_back(ptr, velocity); + ActorMap::iterator found = mActors.find(ptr.mRef); + if (found != mActors.end()) + found->second->setVelocity(velocity); } void PhysicsSystem::clearQueuedMovement() { - mMovementQueue.clear(); - mStandingCollisions.clear(); + for (const auto& [_, actor] : mActors) + actor->setVelocity(osg::Vec3f()); } - const PtrPositionList& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation) + std::vector PhysicsSystem::prepareSimulation(bool willSimulate) { - mTimeAccum += dt; - - const int maxAllowedSteps = 20; - int numSteps = mTimeAccum / mPhysicsDt; - numSteps = std::min(numSteps, maxAllowedSteps); - - mTimeAccum -= numSteps * mPhysicsDt; - - return mTaskScheduler->moveActors(numSteps, mTimeAccum, prepareFrameData(numSteps), mStandingCollisions, skipSimulation); - } - - std::vector PhysicsSystem::prepareFrameData(int numSteps) - { - std::vector actorsFrameData; - actorsFrameData.reserve(mMovementQueue.size()); + std::vector simulations; + simulations.reserve(mActors.size() + mProjectiles.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); - for (const auto& [character, movement] : mMovementQueue) + for (const auto& [ref, physicActor] : mActors) { - const auto foundActor = mActors.find(character); - if (foundActor == mActors.end()) // actor was already removed from the scene - { - mStandingCollisions.erase(character); + if (!physicActor->isActive()) continue; - } - auto physicActor = foundActor->second; + auto ptr = physicActor->getPtr(); + if (!ptr.getClass().isMobile(ptr)) + continue; float waterlevel = -std::numeric_limits::max(); - const MWWorld::CellStore *cell = character.getCell(); + const MWWorld::CellStore *cell = ptr.getCell(); if(cell->getCell()->hasWater()) waterlevel = cell->getWaterLevel(); - const MWMechanics::MagicEffects& effects = character.getClass().getCreatureStats(character).getMagicEffects(); + const auto& stats = ptr.getClass().getCreatureStats(ptr); + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); bool waterCollision = false; - bool moveToWaterSurface = false; if (cell->getCell()->hasWater() && effects.get(ESM::MagicEffect::WaterWalking).getMagnitude()) { - if (!world->isUnderwater(character.getCell(), osg::Vec3f(character.getRefData().getPosition().asVec3()))) + if (physicActor->getCollisionMode() || !world->isUnderwater(ptr.getCell(), ptr.getRefData().getPosition().asVec3())) waterCollision = true; - else if (physicActor->getCollisionMode() && canMoveToWaterSurface(character, waterlevel)) - { - moveToWaterSurface = true; - waterCollision = true; - } } physicActor->setCanWaterWalk(waterCollision); // Slow fall reduces fall speed by a factor of (effect magnitude / 200) - const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); + const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); + const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); + const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); + + simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}}); - // Ue current value only if we don't advance the simulation. Otherwise we might get a stale value. - MWWorld::Ptr standingOn; - if (numSteps == 0) - standingOn = mStandingCollisions[character]; + // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. + if (willSimulate) + handleJump(ptr); + } - actorsFrameData.emplace_back(std::move(physicActor), character, standingOn, moveToWaterSurface, movement, slowFall, waterlevel); + for (const auto& [id, projectile] : mProjectiles) + { + simulations.emplace_back(ProjectileSimulation{projectile, ProjectileFrameData{*projectile}}); } - mMovementQueue.clear(); - return actorsFrameData; + + return simulations; } - void PhysicsSystem::stepSimulation() + void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { - for (Object* animatedObject : mAnimatedObjects) + for (auto& [animatedObject, changed] : mAnimatedObjects) + { if (animatedObject->animateCollisionShapes()) { - auto obj = mObjects.find(animatedObject->getPtr()); + auto obj = mObjects.find(animatedObject->getPtr().mRef); assert(obj != mObjects.end()); mTaskScheduler->updateSingleAabb(obj->second); + changed = true; } + else + { + changed = false; + } + } #ifndef BT_NO_PROFILE CProfileManager::Reset(); CProfileManager::Increment_Frame_Counter(); #endif + + mTimeAccum += dt; + + if (skipSimulation) + mTaskScheduler->resetSimulation(mActors); + else + { + auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt); + // modifies mTimeAccum + mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(simulations), frameStart, frameNumber, stats); + } + } + + void PhysicsSystem::moveActors() + { + auto* player = getActor(MWMechanics::getPlayer()); + const auto world = MWBase::Environment::get().getWorld(); + + // copy new ptr position in temporary vector. player is handled separately as its movement might change active cell. + std::vector> newPositions; + newPositions.reserve(mActors.size() - 1); + for (const auto& [ptr, physicActor] : mActors) + { + if (physicActor.get() == player) + continue; + newPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); + } + + for (auto& [ptr, pos] : newPositions) + world->moveObject(ptr, pos, false, false); + + world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false); } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object) { - ObjectMap::iterator found = mObjects.find(object); + ObjectMap::iterator found = mObjects.find(object.mRef); if (found != mObjects.end()) if (found->second->animateCollisionShapes()) mTaskScheduler->updateSingleAabb(found->second); @@ -769,25 +818,23 @@ namespace MWPhysics void PhysicsSystem::debugDraw() { if (mDebugDrawEnabled) - mDebugDrawer->step(); + mTaskScheduler->debugDraw(); } bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const { - for (const auto& standingActor : mStandingCollisions) - { - if (standingActor.first == actor && standingActor.second == object) - return true; - } + const auto physActor = mActors.find(actor.mRef); + if (physActor != mActors.end()) + return physActor->second->getStandingOnPtr() == object; return false; } void PhysicsSystem::getActorsStandingOn(const MWWorld::ConstPtr &object, std::vector &out) const { - for (const auto& standingActor : mStandingCollisions) + for (const auto& [_, actor] : mActors) { - if (standingActor.second == object) - out.push_back(standingActor.first); + if (actor->getStandingOnPtr() == object) + out.emplace_back(actor->getPtr()); } } @@ -844,25 +891,45 @@ namespace MWPhysics return; } - mWaterCollisionObject.reset(new btCollisionObject()); - mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight)); + mWaterCollisionObject = std::make_unique(); + mWaterCollisionShape = std::make_unique(btVector3(0,0,1), mWaterHeight); mWaterCollisionObject->setCollisionShape(mWaterCollisionShape.get()); mTaskScheduler->addCollisionObject(mWaterCollisionObject.get(), CollisionType_Water, - CollisionType_Actor); + CollisionType_Actor|CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const Misc::Span& ignore, std::vector* occupyingActors) const { - btCollisionObject* object = nullptr; - const auto it = mActors.find(ignore); - if (it != mActors.end()) - object = it->second->getCollisionObject(); + std::vector ignoredObjects; + ignoredObjects.reserve(ignore.size()); + for (const auto& v : ignore) + if (const auto it = mActors.find(v.mRef); it != mActors.end()) + ignoredObjects.push_back(it->second->getCollisionObject()); + std::sort(ignoredObjects.begin(), ignoredObjects.end()); + ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); + const auto ignoreFilter = [&] (const btCollisionObject* v) + { + return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); + }; const auto bulletPosition = Misc::Convert::toBullet(position); const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; - const int group = 0xff; - HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group); + const int group = MWPhysics::CollisionType_AnyPhysical; + if (occupyingActors == nullptr) + { + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, + static_cast(nullptr)); + mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); + return callback.getResult(); + } + const auto onCollision = [&] (const btCollisionObject* object) + { + if (PtrHolder* holder = static_cast(object->getUserPointer())) + occupyingActors->push_back(holder->getPtr()); + }; + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } @@ -871,6 +938,7 @@ namespace MWPhysics { stats.setAttribute(frameNumber, "Physics Actors", mActors.size()); stats.setAttribute(frameNumber, "Physics Objects", mObjects.size()); + stats.setAttribute(frameNumber, "Physics Projectiles", mProjectiles.size()); stats.setAttribute(frameNumber, "Physics HeightFields", mHeightFields.size()); } @@ -880,31 +948,37 @@ namespace MWPhysics mDebugDrawer->addCollision(position, normal); } - ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr character, const MWWorld::Ptr standingOn, - bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel) - : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), - mPositionChanged(false), mDidJump(false), mNeedLand(false), mMoveToWaterSurface(moveToWaterSurface), - mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(movement), mPosition(), mRefpos() - { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - mPtr = actor->getPtr(); - mFlying = world->isFlying(character); - mSwimming = world->isSwimming(character); - mWantJump = mPtr.getClass().getMovementSettings(mPtr).mPosition[2] != 0; - mIsDead = mPtr.getClass().getCreatureStats(mPtr).isDead(); - mWasOnGround = actor->getOnGround(); - } - - void ActorFrameData::updatePosition() + ActorFrameData::ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel) + : mPosition() + , mStandingOn(nullptr) + , mIsOnGround(actor.getOnGround()) + , mIsOnSlope(actor.getOnSlope()) + , mWalkingOnWater(false) + , mInert(inert) + , mCollisionObject(actor.getCollisionObject()) + , mSwimLevel(waterlevel - (actor.getRenderingHalfExtents().z() * 2 * MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat())) + , mSlowFall(slowFall) + , mRotation() + , mMovement(actor.velocity()) + , mWaterlevel(waterlevel) + , mHalfExtentsZ(actor.getHalfExtents().z()) + , mOldHeight(0) + , mStuckFrames(0) + , mFlying(MWBase::Environment::get().getWorld()->isFlying(actor.getPtr())) + , mWasOnGround(actor.getOnGround()) + , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) + , mWaterCollision(waterCollision) + , mSkipCollisionDetection(!actor.getCollisionMode()) + { + } + + ProjectileFrameData::ProjectileFrameData(Projectile& projectile) + : mPosition(projectile.getPosition()) + , mMovement(projectile.velocity()) + , mCaster(projectile.getCasterCollisionObject()) + , mCollisionObject(projectile.getCollisionObject()) + , mProjectile(&projectile) { - mPosition = mActorRaw->getPosition(); - if (mMoveToWaterSurface) - { - mPosition.z() = mWaterlevel; - mActorRaw->setPosition(mPosition); - } - mOldHeight = mPosition.z(); - mRefpos = mPtr.getRefData().getPosition(); } WorldFrameData::WorldFrameData() diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 36ef762d3e..4afafede4f 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -5,11 +5,18 @@ #include #include #include +#include #include +#include +#include +#include #include #include #include +#include + +#include #include "../mwworld/ptr.hpp" @@ -34,11 +41,6 @@ namespace Resource class ResourceSystem; } -namespace SceneUtil -{ - class UnrefQueue; -} - class btCollisionWorld; class btBroadphaseInterface; class btDefaultCollisionConfiguration; @@ -49,13 +51,13 @@ class btVector3; namespace MWPhysics { - using PtrPositionList = std::map; - using CollisionMap = std::map; - class HeightField; class Object; class Actor; class PhysicsTaskScheduler; + class Projectile; + + using ActorMap = std::unordered_map>; struct ContactPoint { @@ -77,28 +79,39 @@ namespace MWPhysics struct ActorFrameData { - ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr character, const MWWorld::Ptr standingOn, bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel); - void updatePosition(); - std::weak_ptr mActor; - Actor* mActorRaw; - MWWorld::Ptr mPtr; - MWWorld::Ptr mStandingOn; - bool mFlying; - bool mSwimming; - bool mPositionChanged; - bool mWasOnGround; - bool mWantJump; - bool mDidJump; - bool mIsDead; - bool mNeedLand; - bool mMoveToWaterSurface; - float mWaterlevel; - float mSlowFall; - float mOldHeight; - float mFallHeight; + ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); + osg::Vec3f mPosition; + osg::Vec3f mInertia; + const btCollisionObject* mStandingOn; + bool mIsOnGround; + bool mIsOnSlope; + bool mWalkingOnWater; + const bool mInert; + btCollisionObject* mCollisionObject; + const float mSwimLevel; + const float mSlowFall; + osg::Vec2f mRotation; osg::Vec3f mMovement; + osg::Vec3f mLastStuckPosition; + const float mWaterlevel; + const float mHalfExtentsZ; + float mOldHeight; + unsigned int mStuckFrames; + const bool mFlying; + const bool mWasOnGround; + const bool mIsAquatic; + const bool mWaterCollision; + const bool mSkipCollisionDetection; + }; + + struct ProjectileFrameData + { + explicit ProjectileFrameData(Projectile& projectile); osg::Vec3f mPosition; - ESM::Position mRefpos; + osg::Vec3f mMovement; + const btCollisionObject* mCaster; + const btCollisionObject* mCollisionObject; + Projectile* mProjectile; }; struct WorldFrameData @@ -108,23 +121,47 @@ namespace MWPhysics osg::Vec3f mStormDirection; }; + template + class SimulationImpl + { + public: + explicit SimulationImpl(const std::weak_ptr& ptr, FrameData&& data) : mPtr(ptr), mData(data) {} + + std::optional, std::reference_wrapper>> lock() + { + if (auto locked = mPtr.lock()) + return {{std::move(locked), std::ref(mData)}}; + return std::nullopt; + } + + private: + std::weak_ptr mPtr; + FrameData mData; + }; + + using ActorSimulation = SimulationImpl; + using ProjectileSimulation = SimulationImpl; + using Simulation = std::variant; + class PhysicsSystem : public RayCastingInterface { public: PhysicsSystem (Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); virtual ~PhysicsSystem (); - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); - Resource::BulletShapeManager* getShapeManager(); void enableWater(float height); void setWaterHeight(float height); void disableWater(); - void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType = CollisionType_World); + void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType = CollisionType_World); void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); + int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); + void setCaster(int projectileId, const MWWorld::Ptr& caster); + void removeProjectile(const int projectileId); + void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); Actor* getActor(const MWWorld::Ptr& ptr); @@ -132,15 +169,16 @@ namespace MWPhysics const Object* getObject(const MWWorld::ConstPtr& ptr) const; + Projectile* getProjectile(int projectileId) const; + // Object or Actor void remove (const MWWorld::Ptr& ptr); void updateScale (const MWWorld::Ptr& ptr); - void updateRotation (const MWWorld::Ptr& ptr); + void updateRotation (const MWWorld::Ptr& ptr, osg::Quat rotate); void updatePosition (const MWWorld::Ptr& ptr); - - void addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); + void addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject); void removeHeightField (int x, int y); @@ -148,7 +186,11 @@ namespace MWPhysics bool toggleCollisionMode(); - void stepSimulation(); + /// Determine new position based on all queued movements, then clear the list. + void stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + /// Apply new positions to actors + void moveActors(); void debugDraw(); std::vector getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; ///< get handles this object collides with @@ -169,10 +211,11 @@ namespace MWPhysics /// @param me Optional, a 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(), - std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; + const std::vector& targets = std::vector(), + int mask = CollisionType_Default, int group=0xff) const override; - RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; + RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const override; /// Return true if actor1 can see actor2. bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; @@ -198,12 +241,9 @@ namespace MWPhysics osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr &object) const; /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will - /// be overwritten. Valid until the next call to applyQueuedMovement. + /// be overwritten. Valid until the next call to stepSimulation void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity); - /// Apply all queued movements, then clear the list. - const PtrPositionList& applyQueuedMovement(float dt, bool skipSimulation); - /// Clear the queued movements list without applying. void clearQueuedMovement(); @@ -238,7 +278,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const Misc::Span& ignore, std::vector* occupyingActors) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); @@ -247,44 +288,36 @@ namespace MWPhysics void updateWater(); - std::vector prepareFrameData(int numSteps); - - osg::ref_ptr mUnrefQueue; + std::vector prepareSimulation(bool willSimulate); std::unique_ptr mBroadphase; std::unique_ptr mCollisionConfiguration; std::unique_ptr mDispatcher; - std::shared_ptr mCollisionWorld; + std::unique_ptr mCollisionWorld; std::unique_ptr mTaskScheduler; std::unique_ptr mShapeManager; Resource::ResourceSystem* mResourceSystem; - using ObjectMap = std::map>; + using ObjectMap = std::unordered_map>; ObjectMap mObjects; - std::set mAnimatedObjects; // stores pointers to elements in mObjects + std::map mAnimatedObjects; // stores pointers to elements in mObjects - using ActorMap = std::map>; ActorMap mActors; - using HeightFieldMap = std::map, HeightField *>; + using ProjectileMap = std::map>; + ProjectileMap mProjectiles; + + using HeightFieldMap = std::map, std::unique_ptr>; HeightFieldMap mHeightFields; bool mDebugDrawEnabled; - // Tracks standing collisions happening during a single frame. - // This will detect standing on an object, but won't detect running e.g. against a wall. - CollisionMap mStandingCollisions; - - // replaces all occurrences of 'old' in the map by 'updated', no matter if it's a key or value - void updateCollisionMapPtr(CollisionMap& map, const MWWorld::Ptr &old, const MWWorld::Ptr &updated); - - using PtrVelocityList = std::vector>; - PtrVelocityList mMovementQueue; - float mTimeAccum; + unsigned int mProjectileId; + float mWaterHeight; bool mWaterEnabled; diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp new file mode 100644 index 0000000000..29ec4387b1 --- /dev/null +++ b/apps/openmw/mwphysics/projectile.cpp @@ -0,0 +1,123 @@ +#include + +#include + +#include + +#include "../mwworld/class.hpp" + +#include "actor.hpp" +#include "collisiontype.hpp" +#include "mtphysics.hpp" +#include "object.hpp" +#include "projectile.hpp" + +namespace MWPhysics +{ +Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) + : mHitWater(false) + , mActive(true) + , mHitTarget(nullptr) + , mPhysics(physicssystem) + , mTaskScheduler(scheduler) +{ + mShape = std::make_unique(radius); + mConvexShape = static_cast(mShape.get()); + + mCollisionObject = std::make_unique(); + mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); + mCollisionObject->setActivationState(DISABLE_DEACTIVATION); + mCollisionObject->setCollisionShape(mShape.get()); + mCollisionObject->setUserPointer(this); + + mPosition = position; + mPreviousPosition = position; + mSimulationPosition = position; + setCaster(caster); + + const int collisionMask = CollisionType_World | CollisionType_HeightMap | + CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; + mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); + + updateCollisionObjectPosition(); +} + +Projectile::~Projectile() +{ + if (!mActive) + mPhysics->reportCollision(mHitPosition, mHitNormal); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); +} + +void Projectile::updateCollisionObjectPosition() +{ + std::scoped_lock lock(mMutex); + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); +} + +void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) +{ + bool active = true; + if (!mActive.compare_exchange_strong(active, false, std::memory_order_relaxed) || !active) + return; + mHitTarget = target; + mHitPosition = pos; + mHitNormal = normal; +} + +MWWorld::Ptr Projectile::getTarget() const +{ + assert(!mActive); + auto* target = static_cast(mHitTarget->getUserPointer()); + return target ? target->getPtr() : MWWorld::Ptr(); +} + +MWWorld::Ptr Projectile::getCaster() const +{ + return mCaster; +} + +void Projectile::setCaster(const MWWorld::Ptr& caster) +{ + mCaster = caster; + mCasterColObj = [this,&caster]() -> const btCollisionObject* + { + const Actor* actor = mPhysics->getActor(caster); + if (actor) + return actor->getCollisionObject(); + const Object* object = mPhysics->getObject(caster); + if (object) + return object->getCollisionObject(); + return nullptr; + }(); +} + +void Projectile::setValidTargets(const std::vector& targets) +{ + std::scoped_lock lock(mMutex); + mValidTargets.clear(); + for (const auto& ptr : targets) + { + const auto* physicActor = mPhysics->getActor(ptr); + if (physicActor) + mValidTargets.push_back(physicActor->getCollisionObject()); + } +} + +bool Projectile::isValidTarget(const btCollisionObject* target) const +{ + assert(target); + std::scoped_lock lock(mMutex); + if (mCasterColObj == target) + return false; + + if (mValidTargets.empty()) + return true; + + return std::any_of(mValidTargets.begin(), mValidTargets.end(), + [target](const btCollisionObject* actor) { return target == actor; }); +} + +} diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp new file mode 100644 index 0000000000..10ed2c9582 --- /dev/null +++ b/apps/openmw/mwphysics/projectile.hpp @@ -0,0 +1,102 @@ +#ifndef OPENMW_MWPHYSICS_PROJECTILE_H +#define OPENMW_MWPHYSICS_PROJECTILE_H + +#include +#include +#include + +#include + +#include "ptrholder.hpp" + +class btCollisionObject; +class btCollisionShape; +class btConvexShape; + +namespace osg +{ + class Vec3f; +} + +namespace Resource +{ + class BulletShape; +} + +namespace MWPhysics +{ + class PhysicsTaskScheduler; + class PhysicsSystem; + + class Projectile final : public PtrHolder + { + public: + Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); + ~Projectile() override; + + btConvexShape* getConvexShape() const { return mConvexShape; } + + void updateCollisionObjectPosition(); + + bool isActive() const + { + return mActive.load(std::memory_order_acquire); + } + + MWWorld::Ptr getTarget() const; + + MWWorld::Ptr getCaster() const; + void setCaster(const MWWorld::Ptr& caster); + const btCollisionObject* getCasterCollisionObject() const + { + return mCasterColObj; + } + + void setHitWater() + { + mHitWater = true; + } + + bool getHitWater() const + { + return mHitWater; + } + + void hit(const btCollisionObject* target, btVector3 pos, btVector3 normal); + + void setValidTargets(const std::vector& targets); + bool isValidTarget(const btCollisionObject* target) const; + + btVector3 getHitPosition() const + { + return mHitPosition; + } + + private: + + std::unique_ptr mShape; + btConvexShape* mConvexShape; + + bool mHitWater; + std::atomic mActive; + MWWorld::Ptr mCaster; + const btCollisionObject* mCasterColObj; + const btCollisionObject* mHitTarget; + btVector3 mHitPosition; + btVector3 mHitNormal; + + std::vector mValidTargets; + + mutable std::mutex mMutex; + + PhysicsSystem *mPhysics; + PhysicsTaskScheduler *mTaskScheduler; + + Projectile(const Projectile&); + Projectile& operator=(const Projectile&); + }; + +} + + +#endif diff --git a/apps/openmw/mwphysics/projectileconvexcallback.cpp b/apps/openmw/mwphysics/projectileconvexcallback.cpp new file mode 100644 index 0000000000..6520be787d --- /dev/null +++ b/apps/openmw/mwphysics/projectileconvexcallback.cpp @@ -0,0 +1,62 @@ +#include + +#include "../mwworld/class.hpp" + +#include "actor.hpp" +#include "collisiontype.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" +#include "ptrholder.hpp" + +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) + { + const auto* hitObject = result.m_hitCollisionObject; + // don't hit the caster + if (hitObject == mCaster) + return 1.f; + + // don't hit the projectile + if (hitObject == mMe) + return 1.f; + + btCollisionWorld::ClosestConvexResultCallback::addSingleResult(result, normalInWorldSpace); + switch (hitObject->getBroadphaseHandle()->m_collisionFilterGroup) + { + case CollisionType_Actor: + { + if (!mProjectile->isValidTarget(hitObject)) + return 1.f; + break; + } + case CollisionType_Projectile: + { + auto* target = static_cast(hitObject->getUserPointer()); + if (!mProjectile->isValidTarget(target->getCasterCollisionObject())) + return 1.f; + target->hit(mMe, m_hitPointWorld, m_hitNormalWorld); + break; + } + case CollisionType_Water: + { + mProjectile->setHitWater(); + break; + } + } + 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 new file mode 100644 index 0000000000..f35cfbd3c8 --- /dev/null +++ b/apps/openmw/mwphysics/projectileconvexcallback.hpp @@ -0,0 +1,26 @@ +#ifndef OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H +#define OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H + +#include + +class btCollisionObject; + +namespace MWPhysics +{ + class Projectile; + + class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback + { + public: + ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj); + + btScalar addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) override; + + private: + const btCollisionObject* mCaster; + const btCollisionObject* mMe; + Projectile* mProjectile; + }; +} + +#endif diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index f8188b43ed..c4141d14bc 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -1,6 +1,14 @@ #ifndef OPENMW_MWPHYSICS_PTRHOLDER_H #define OPENMW_MWPHYSICS_PTRHOLDER_H +#include +#include +#include + +#include + +#include + #include "../mwworld/ptr.hpp" namespace MWPhysics @@ -8,25 +16,66 @@ namespace MWPhysics class PtrHolder { public: - virtual ~PtrHolder() {} + virtual ~PtrHolder() = default; void updatePtr(const MWWorld::Ptr& updated) { mPtr = updated; } - MWWorld::Ptr getPtr() + MWWorld::Ptr getPtr() const { return mPtr; } - MWWorld::ConstPtr getPtr() const + btCollisionObject* getCollisionObject() const { - return mPtr; + return mCollisionObject.get(); + } + + void setVelocity(osg::Vec3f velocity) + { + mVelocity = velocity; + } + + osg::Vec3f velocity() + { + return std::exchange(mVelocity, osg::Vec3f()); + } + + void setSimulationPosition(const osg::Vec3f& position) + { + mSimulationPosition = position; + } + + osg::Vec3f getSimulationPosition() const + { + return mSimulationPosition; + } + + void setPosition(const osg::Vec3f& position) + { + mPreviousPosition = mPosition; + mPosition = position; + } + + osg::Vec3d getPosition() const + { + return mPosition; + } + + osg::Vec3d getPreviousPosition() const + { + return mPreviousPosition; } protected: MWWorld::Ptr mPtr; + std::unique_ptr mCollisionObject; + osg::Vec3f mVelocity; + osg::Vec3f mSimulationPosition; + osg::Vec3d mPosition; + osg::Vec3d mPreviousPosition; }; } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 7afbe93214..848f17a01a 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -9,14 +9,15 @@ namespace MWPhysics { - struct RayCastingResult + class RayCastingResult { - bool mHit; - osg::Vec3f mHitPos; - osg::Vec3f mHitNormal; - MWWorld::Ptr mHitObject; + public: + bool mHit; + osg::Vec3f mHitPos; + osg::Vec3f mHitNormal; + MWWorld::Ptr mHitObject; }; - + class RayCastingInterface { public: @@ -28,10 +29,11 @@ namespace MWPhysics /// @param me Optional, a 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(), - std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; + const std::vector& targets = std::vector(), + int mask = CollisionType_Default, int group=0xff) const = 0; - virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; + virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const = 0; /// Return true if actor1 can see actor2. virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 0ab383dd1e..5ef6833701 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -1,12 +1,13 @@ #include "stepper.hpp" -#include - #include #include +#include + #include "collisiontype.hpp" #include "constants.hpp" +#include "movementsolver.hpp" namespace MWPhysics { @@ -14,7 +15,7 @@ namespace MWPhysics { if (!stepper.mHitObject) return false; - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); if (stepper.mPlaneNormal.z() <= sMaxSlopeCos) return false; @@ -24,125 +25,155 @@ namespace MWPhysics Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) : mColWorld(colWorld) , mColObj(colObj) - , mHaveMoved(true) { } - bool Stepper::step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime) + bool Stepper::step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration) { - /* - * Slide up an incline or set of stairs. Should be called only after a - * collision detection otherwise unnecessary tracing will be performed. - * - * NOTE: with a small change this method can be used to step over an obstacle - * of height sStepSize. - * - * If successful return 'true' and update 'position' to the new possible - * location and adjust 'remainingTime'. - * - * If not successful return 'false'. May fail for these reasons: - * - can't move directly up from current position - * - having moved up by between epsilon() and sStepSize, can't move forward - * - having moved forward by between epsilon() and toMove, - * = moved down between 0 and just under sStepSize but slope was too steep, or - * = moved the full sStepSize down (FIXME: this could be a bug) - * - * Starting position. Obstacle or stairs with height upto sStepSize in front. - * - * +--+ +--+ |XX - * | | -------> toMove | | +--+XX - * | | | | |XXXXX - * | | +--+ | | +--+XXXXX - * | | |XX| | | |XXXXXXXX - * +--+ +--+ +--+ +-------- - * ============================================== - */ - - /* - * Try moving up sStepSize using stepper. - * FIXME: does not work in case there is no front obstacle but there is one above - * - * +--+ +--+ - * | | | | - * | | | | |XX - * | | | | +--+XX - * | | | | |XXXXX - * +--+ +--+ +--+ +--+XXXXX - * |XX| |XXXXXXXX - * +--+ +-------- - * ============================================== - */ - if (mHaveMoved) - { - mHaveMoved = false; + if(velocity.x() == 0.0 && velocity.y() == 0.0) + return false; - mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); - if (mUpStepper.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount - // (TODO: shouldn't this be larger? Why bother with such a small amount?) + // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. + // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. + + mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); + + float upDistance = 0; + if(!mUpStepper.mHitObject) + upDistance = Constants::sStepSizeUp; + else if(mUpStepper.mFraction * Constants::sStepSizeUp > sCollisionMargin) + upDistance = mUpStepper.mFraction * Constants::sStepSizeUp - sCollisionMargin; + else + { + return false; } - /* - * Try moving from the elevated position using tracer. - * - * +--+ +--+ - * | | |YY| FIXME: collision with object YY - * | | +--+ - * | | - * <------------------->| | - * +--+ +--+ - * |XX| the moved amount is toMove*tracer.mFraction - * +--+ - * ============================================== - */ - osg::Vec3f tracerPos = mUpStepper.mEndPos; - mTracer.doTrace(mColObj, tracerPos, tracerPos + toMove, mColWorld); - if (mTracer.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount - - /* - * Try moving back down sStepSizeDown using stepper. - * NOTE: if there is an obstacle below (e.g. stairs), we'll be "stepping up". - * Below diagram is the case where we "stepped over" an obstacle in front. - * - * +--+ - * |YY| - * +--+ +--+ - * | | - * | | - * +--+ | | - * |XX| | | - * +--+ +--+ - * ============================================== - */ - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) + auto toMove = velocity * remainingTime; + + osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + + osg::Vec3f tracerDest; + auto normalMove = toMove; + auto moveDistance = normalMove.normalize(); + // attempt 1: normal movement + // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to avoid a glitch + // attempt 3: further, less tall fixed distance movement, same as above + // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. + int attempt = 0; + float downStepSize = 0; + while(attempt < 3) { - // Try again with increased step length - if (mTracer.mFraction < 1.0f || toMove.length2() > sMinStep*sMinStep) - return false; + attempt++; - osg::Vec3f direction = toMove; - direction.normalize(); - mTracer.doTrace(mColObj, tracerPos, tracerPos + direction*sMinStep, mColWorld); - if (mTracer.mFraction < 0.001f) + if(attempt == 1) + tracerDest = tracerPos + toMove; + else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled + { return false; + } + else if(attempt == 2) + { + moveDistance = sMinStep; + tracerDest = tracerPos + normalMove*sMinStep; + } + else if(attempt == 3) + { + if(upDistance > Constants::sStepSizeUp) + { + upDistance = Constants::sStepSizeUp; + tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + } + moveDistance = sMinStep2; + tracerDest = tracerPos + normalMove*sMinStep2; + } + + mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); + if(mTracer.mHitObject) + { + // map against what we hit, minus the safety margin + moveDistance *= mTracer.mFraction; + if(moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything + { + return false; + } + + moveDistance -= sCollisionMargin; + tracerDest = tracerPos + normalMove*moveDistance; + + // safely eject from what we hit by the safety margin + auto tempDest = tracerDest + mTracer.mPlaneNormal*sCollisionMargin*2; + + ActorTracer tempTracer; + tempTracer.doTrace(mColObj, tracerDest, tempDest, mColWorld); + + if(tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked sCollisionMargin*2 distance) + { + auto effectiveFraction = tempTracer.mFraction*2.0f - 1.0f; + tracerDest += mTracer.mPlaneNormal*sCollisionMargin*effectiveFraction; + } + } + + if(attempt > 2) // do not allow stepping down below original height for attempt 3 + downStepSize = upDistance; + else + downStepSize = moveDistance + upDistance + sStepSizeDown; + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround); + + // can't step down onto air, non-walkable-slopes, or actors + // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs + // (like the bottoms of the staircases in aldruhn's guild of mages) + // The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep (10.0) but it caused all sorts of other problems. + // Switched back to cylinders to avoid that and similer problems. + if(canStepDown(mDownStepper)) + { + break; + } + else + { + // do not try attempt 3 if we just tried attempt 2 and the horizontal distance was rather large + // (forces actor to get snug against the defective ledge for attempt 3 to be tried) + if(attempt == 2 && moveDistance > upDistance-(mDownStepper.mFraction*downStepSize)) + { + return false; + } + // do next attempt if first iteration of movement solver and not out of attempts + if(firstIteration && attempt < 3) + { + continue; + } - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) return false; + } } - if (mDownStepper.mFraction < 1.0f) + // note: can't downstep onto actors so no need to pick safety margin + float downDistance = 0; + if(mDownStepper.mFraction*downStepSize > sCollisionMargin) + downDistance = mDownStepper.mFraction*downStepSize - sCollisionMargin; + + if(downDistance-sCollisionMargin-sGroundOffset > upDistance && !onGround) + return false; + + auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); + + if((position-newpos).length2() < sCollisionMargin*sCollisionMargin) + return false; + + if(mTracer.mHitObject) { - // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - // TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing - // NOTE: caller's variables 'position' & 'remainingTime' are modified here - position = mDownStepper.mEndPos; - remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance - mHaveMoved = true; - return true; + auto planeNormal = mTracer.mPlaneNormal; + if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + velocity = reject(velocity, planeNormal); } - return false; + velocity = reject(velocity, mDownStepper.mPlaneNormal); + + position = newpos; + + remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance + return true; } } diff --git a/apps/openmw/mwphysics/stepper.hpp b/apps/openmw/mwphysics/stepper.hpp index 27e6294b05..512493c524 100644 --- a/apps/openmw/mwphysics/stepper.hpp +++ b/apps/openmw/mwphysics/stepper.hpp @@ -20,12 +20,11 @@ namespace MWPhysics const btCollisionObject *mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; - bool mHaveMoved; public: Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); - bool step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime); + bool step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration); }; } diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 58082f4db2..b7930bfa53 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -7,43 +7,89 @@ #include "collisiontype.hpp" #include "actor.hpp" -#include "closestnotmeconvexresultcallback.hpp" +#include "actorconvexcallback.hpp" namespace MWPhysics { -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& from, const btVector3& to, const btCollisionWorld* world, bool actorFilter) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - const btTransform &trans = actor->getWorldTransform(); - btTransform from(trans); - btTransform to(trans); - from.setOrigin(btstart); - to.setOrigin(btend); - - const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor, motion, btScalar(0.0)); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + btTransform transFrom(trans); + btTransform transTo(trans); + transFrom.setOrigin(from); + transTo.setOrigin(to); const btCollisionShape *shape = actor->getCollisionShape(); assert(shape->isConvex()); - world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); + + const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too + ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); + // Inherit the actor's collision group and mask + traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; + traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + if(actorFilter) + traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; + + world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); + return traceCallback; +} + +void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace) +{ + const btVector3 btstart = Misc::Convert::toBullet(start); + btVector3 btend = Misc::Convert::toBullet(end); + + // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests + // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be + // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. + // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. + // This trace needs to be at least a couple units long, but there's no one particular ideal length. + // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. + // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) + const float fallback_length = 2.1f; + bool doing_short_trace = false; + // For some reason, typical scenes perform a little better if we increase the threshold length for the length test. + // (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks this was + // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) + if(attempt_short_trace && (btend-btstart).length2() > fallback_length*fallback_length*2.0) + { + btend = btstart + (btend-btstart).normalized()*fallback_length; + doing_short_trace = true; + } + + const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + // ensure fraction is correct (covers intended distance traveled instead of actual distance traveled) + if(doing_short_trace && (end-start).length2() > 0.0) + mFraction *= (btend-btstart).length() / (end-start).length(); + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; - mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); - mHitObject = newTraceCallback.m_hitCollisionObject; + mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld); + mHitObject = traceCallback.m_hitCollisionObject; } else { + if(doing_short_trace) + { + btend = Misc::Convert::toBullet(end); + const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + + if(newTraceCallback.hasHit()) + { + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mEndPos = (end-start)*mFraction + start; + mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); + mHitObject = newTraceCallback.m_hitCollisionObject; + return; + } + } + // fallthrough mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; @@ -54,25 +100,11 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - - const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); - btTransform from(trans.getBasis(), btstart); - btTransform to(trans.getBasis(), btend); - - const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0)); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - - world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); - if(newTraceCallback.hasHit()) + const auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; } else diff --git a/apps/openmw/mwphysics/trace.h b/apps/openmw/mwphysics/trace.h index 0297c9e076..af38756b3e 100644 --- a/apps/openmw/mwphysics/trace.h +++ b/apps/openmw/mwphysics/trace.h @@ -20,7 +20,7 @@ namespace MWPhysics float mFraction; - void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); + void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace = false); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index fcffe220b9..9b9c521a51 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -5,8 +5,8 @@ #include #include -#include -#include +#include +#include #include #include @@ -17,6 +17,7 @@ #include #include +#include #include @@ -30,6 +31,8 @@ #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" +#include "../mwmechanics/drawstate.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "vismask.hpp" @@ -75,7 +78,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return PartHolderPtr(); @@ -85,30 +88,59 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } -std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +osg::ref_ptr ActorAnimation::attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight) +{ + osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); + + const NodeMap& nodeMap = getNodeMap(); + auto found = nodeMap.find(bonename); + if (found == nodeMap.end()) + throw std::runtime_error("Can't find attachment node " + bonename); + if(isLight) + { + osg::Quat rotation(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0)); + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); + } + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); +} + +std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const { - std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; + // Try to recover the body part model, use ground model as a fallback otherwise. if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); - - // Try to get shield model from bodyparts first, with ground model as fallback for (const auto& part : bodyparts) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + if (part.mPart != ESM::PRT_Shield) continue; - const ESM::BodyPart *bodypart = partStore.search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + + std::string bodypartName; + if (female && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) { - mesh = "meshes\\" + bodypart->mModel; - break; + const ESM::BodyPart *bodypart = partStore.search(bodypartName); + 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 shield.getClass().getModel(shield); +} + +std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const +{ + std::string mesh = getShieldMesh(shield, false); if (mesh.empty()) return mesh; @@ -144,21 +176,21 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getSheathedShieldMesh(*shield).empty()) { - if(stats.getDrawState() != MWMechanics::DrawState_Weapon) + if(stats.getDrawState() != MWMechanics::DrawState::Weapon) return false; if (weapon != inv.end()) { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) return true; } } @@ -185,7 +217,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) return; // Can not show holdstered shields with two-handed weapons at all @@ -193,8 +225,8 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) if(weapon == inv.end()) return; - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; @@ -202,7 +234,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) return; } - std::string mesh = getShieldMesh(*shield); + std::string mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; @@ -233,9 +265,6 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) if (isEnchanted) SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); } - - if (mAlpha != 1.f) - mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); } bool ActorAnimation::useShieldAnimations() const @@ -255,24 +284,24 @@ bool ActorAnimation::useShieldAnimations() const const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (weapon != inv.end() && shield != inv.end() && - shield->getTypeName() == typeid(ESM::Armor).name() && - !getShieldMesh(*shield).empty()) + shield->getType() == ESM::Armor::sRecordId && + !getSheathedShieldMesh(*shield).empty()) { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) return true; } return false; } -osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) +osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) const { if (!mObjectRoot) return nullptr; @@ -289,8 +318,8 @@ std::string ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& if(weapon.isEmpty()) return boneName; - const std::string &type = weapon.getClass().getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon.getClass().getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon.get(); int weaponType = ref->mBase->mData.mType; @@ -305,9 +334,7 @@ void ActorAnimation::resetControllers(osg::Node* node) if (node == nullptr) return; - std::shared_ptr src; - src.reset(new NullAnimationTime); - SceneUtil::AssignControllerSourcesVisitor removeVisitor(src); + SceneUtil::ForceControllerSourcesVisitor removeVisitor(std::make_shared()); node->accept(removeVisitor); } @@ -324,7 +351,7 @@ void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; // Since throwing weapons stack themselves, do not show such weapon itself @@ -358,6 +385,8 @@ void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) } mScabbard = attachMesh(scabbardName, boneName); + if (mScabbard) + resetControllers(mScabbard->getNode()); osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); if (!weaponNode) @@ -398,7 +427,7 @@ void ActorAnimation::updateQuiver() const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; std::string mesh = weapon->getClass().getModel(*weapon); @@ -470,7 +499,7 @@ void ActorAnimation::updateQuiver() void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) { - if (item.getTypeName() == typeid(ESM::Light).name()) + if (item.getType() == ESM::Light::sRecordId) { const ESM::Light* light = item.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) @@ -485,7 +514,7 @@ void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); @@ -501,7 +530,7 @@ void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) { - if (item.getTypeName() == typeid(ESM::Light).name()) + if (item.getType() == ESM::Light::sRecordId) { ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) @@ -519,7 +548,7 @@ void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 86929a18a7..1ece0c326d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -6,7 +6,6 @@ #include #include "../mwworld/containerstore.hpp" -#include "../mwworld/inventorystore.hpp" #include "animation.hpp" @@ -42,11 +41,12 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener bool updateCarriedLeftVisible(const int weaptype) const override; protected: - osg::Group* getBoneByName(const std::string& boneName); + osg::Group* getBoneByName(const std::string& boneName) const; virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; + std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; + virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) @@ -54,6 +54,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); return attachMesh(model, bonename, false, &stubColor); }; + osg::ref_ptr attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp index 35b2553555..45b35df4b9 100644 --- a/apps/openmw/mwrender/actorspaths.cpp +++ b/apps/openmw/mwrender/actorspaths.cpp @@ -2,9 +2,17 @@ #include "vismask.hpp" #include +#include +#include +#include #include +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + +#include + namespace MWRender { ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) @@ -30,31 +38,32 @@ namespace MWRender } void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const DetourNavigator::Settings& settings) { if (!mEnabled) return; - const auto group = mGroups.find(actor); + const auto group = mGroups.find(actor.mRef); if (group != mGroups.end()) - mRootNode->removeChild(group->second); + mRootNode->removeChild(group->second.mNode); - const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings); + 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] = newGroup; + mGroups[actor.mRef] = Group {actor.mCell, std::move(newGroup)}; } } void ActorsPaths::remove(const MWWorld::ConstPtr& actor) { - const auto group = mGroups.find(actor); + const auto group = mGroups.find(actor.mRef); if (group != mGroups.end()) { - mRootNode->removeChild(group->second); + mRootNode->removeChild(group->second.mNode); mGroups.erase(group); } } @@ -63,9 +72,9 @@ namespace MWRender { for (auto it = mGroups.begin(); it != mGroups.end(); ) { - if (it->first.getCell() == store) + if (it->second.mCell == store) { - mRootNode->removeChild(it->second); + mRootNode->removeChild(it->second.mNode); it = mGroups.erase(it); } else @@ -75,25 +84,23 @@ namespace MWRender void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { - const auto it = mGroups.find(old); + const auto it = mGroups.find(old.mRef); if (it == mGroups.end()) return; - auto group = std::move(it->second); - mGroups.erase(it); - mGroups.insert(std::make_pair(updated, std::move(group))); + it->second.mCell = updated.mCell; } void ActorsPaths::enable() { std::for_each(mGroups.begin(), mGroups.end(), - [&] (const Groups::value_type& v) { mRootNode->addChild(v.second); }); + [&] (const Groups::value_type& v) { mRootNode->addChild(v.second.mNode); }); mEnabled = true; } void ActorsPaths::disable() { std::for_each(mGroups.begin(), mGroups.end(), - [&] (const Groups::value_type& v) { mRootNode->removeChild(v.second); }); + [&] (const Groups::value_type& v) { mRootNode->removeChild(v.second.mNode); }); mEnabled = false; } } diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp index 1f61834d46..304d5c09b3 100644 --- a/apps/openmw/mwrender/actorspaths.hpp +++ b/apps/openmw/mwrender/actorspaths.hpp @@ -3,18 +3,23 @@ #include -#include - #include #include #include +#include namespace osg { class Group; } +namespace DetourNavigator +{ + struct Settings; + struct AgentBounds; +} + namespace MWRender { class ActorsPaths @@ -26,7 +31,7 @@ namespace MWRender bool toggle(); void update(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const DetourNavigator::Settings& settings); void remove(const MWWorld::ConstPtr& actor); @@ -40,7 +45,13 @@ namespace MWRender void disable(); private: - using Groups = std::map>; + struct Group + { + const MWWorld::CellStore* mCell; + osg::ref_ptr mNode; + }; + + using Groups = std::map; osg::ref_ptr mRootNode; Groups mGroups; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 10a6b2be42..0684550fb3 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -6,8 +6,9 @@ #include #include #include -#include #include +#include +#include #include #include @@ -18,10 +19,10 @@ #include #include +#include #include -#include // KeyframeHolder -#include +#include #include @@ -88,22 +89,22 @@ namespace std::vector > mToRemove; }; - class DayNightCallback : public osg::NodeCallback + class DayNightCallback : public SceneUtil::NodeCallback { public: DayNightCallback() : mCurrentState(0) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Switch* node, osg::NodeVisitor* nv) { unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode(); - const unsigned int newState = node->asGroup()->getNumChildren() > state ? state : 0; + const unsigned int newState = node->getNumChildren() > state ? state : 0; if (newState != mCurrentState) { mCurrentState = newState; - node->asSwitch()->setSingleChildOn(mCurrentState); + node->setSingleChildOn(mCurrentState); } traverse(node, nv); @@ -148,7 +149,7 @@ namespace } }; - float calcAnimVelocity(const NifOsg::TextKeyMap& keys, NifOsg::KeyframeController *nonaccumctrl, + float calcAnimVelocity(const SceneUtil::TextKeyMap& keys, SceneUtil::KeyframeController *nonaccumctrl, const osg::Vec3f& accum, const std::string &groupname) { const std::string start = groupname+": start"; @@ -198,32 +199,6 @@ namespace return 0.0f; } - /// @brief Base class for visitors that remove nodes from a scene graph. - /// Subclasses need to fill the mToRemove vector. - /// To use, node->accept(removeVisitor); removeVisitor.remove(); - class RemoveVisitor : public osg::NodeVisitor - { - public: - RemoveVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - - void remove() - { - for (RemoveVec::iterator it = mToRemove.begin(); it != mToRemove.end(); ++it) - { - if (!it->second->removeChild(it->first)) - Log(Debug::Error) << "Error removing " << it->first->getName(); - } - } - - protected: - // - typedef std::vector > RemoveVec; - std::vector > mToRemove; - }; - class GetExtendedBonesVisitor : public osg::NodeVisitor { public: @@ -246,7 +221,7 @@ namespace std::vector > mFoundBones; }; - class RemoveFinishedCallbackVisitor : public RemoveVisitor + class RemoveFinishedCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -291,7 +266,7 @@ namespace } }; - class RemoveCallbackVisitor : public RemoveVisitor + class RemoveCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -400,89 +375,18 @@ namespace int mEffectId; }; - // Removes all drawables from a graph. - class CleanObjectRootVisitor : public RemoveVisitor + osg::ref_ptr getVFXLightModelInstance() { - public: - void apply(osg::Drawable& drw) override - { - applyDrawable(drw); - } - - void apply(osg::Group& node) override - { - applyNode(node); - } - void apply(osg::MatrixTransform& node) override - { - applyNode(node); - } - void apply(osg::Node& node) override - { - applyNode(node); - } - - void applyNode(osg::Node& node) - { - if (node.getStateSet()) - node.setStateSet(nullptr); - - if (node.getNodeMask() == 0x1 && node.getNumParents() == 1) - mToRemove.emplace_back(&node, node.getParent(0)); - else - traverse(node); - } - void applyDrawable(osg::Node& node) - { - osg::NodePath::iterator parent = getNodePath().end()-2; - // We know that the parent is a Group because only Groups can have children. - osg::Group* parentGroup = static_cast(*parent); - - // Try to prune nodes that would be empty after the removal - if (parent != getNodePath().begin()) - { - // This could be extended to remove the parent's parent, and so on if they are empty as well. - // But for NIF files, there won't be a benefit since only TriShapes can be set to STATIC dataVariance. - osg::Group* parentParent = static_cast(*(parent - 1)); - if (parentGroup->getNumChildren() == 1 && parentGroup->getDataVariance() == osg::Object::STATIC) - { - mToRemove.emplace_back(parentGroup, parentParent); - return; - } - } - - mToRemove.emplace_back(&node, parentGroup); - } - }; - - class RemoveTriBipVisitor : public RemoveVisitor - { - public: - void apply(osg::Drawable& drw) override - { - applyImpl(drw); - } + static osg::ref_ptr lightModel = nullptr; - void apply(osg::Group& node) override - { - traverse(node); - } - void apply(osg::MatrixTransform& node) override + if (!lightModel) { - traverse(node); + lightModel = new osg::LightModel; + lightModel->setAmbientIntensity({1,1,1,1}); } - void applyImpl(osg::Node& node) - { - const std::string toFind = "tri bip"; - if (Misc::StringUtils::ciCompareLen(node.getName(), toFind, toFind.size()) == 0) - { - osg::Group* parent = static_cast(*(getNodePath().end()-2)); - // Not safe to remove in apply(), since the visitor is still iterating the child list - mToRemove.emplace_back(&node, parent); - } - } - }; + return lightModel; + } } namespace MWRender @@ -530,13 +434,13 @@ namespace MWRender struct Animation::AnimSource { - osg::ref_ptr mKeyframes; + osg::ref_ptr mKeyframes; - typedef std::map > ControllerMap; + typedef std::map > ControllerMap; ControllerMap mControllerMap[Animation::sNumBlendMasks]; - const NifOsg::TextKeyMap& getTextKeys() const; + const SceneUtil::TextKeyMap& getTextKeys() const; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) @@ -575,20 +479,18 @@ namespace MWRender } } - class ResetAccumRootCallback : public osg::NodeCallback + class ResetAccumRootCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::MatrixTransform* transform, osg::NodeVisitor* nv) { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix mat = transform->getMatrix(); osg::Vec3f position = mat.getTrans(); position = osg::componentMultiply(mResetAxes, position); mat.setTrans(position); transform->setMatrix(mat); - traverse(node, nv); + traverse(transform, nv); } void setAccumulate(const osg::Vec3f& accumulate) @@ -620,7 +522,7 @@ namespace MWRender , mAlpha(1.f) { for(size_t i = 0;i < sNumBlendMasks;i++) - mAnimationTimePtr[i].reset(new AnimationTime); + mAnimationTimePtr[i] = std::make_shared(); mLightListCallback = new SceneUtil::LightListCallback; } @@ -688,15 +590,13 @@ namespace MWRender return 0; } - const NifOsg::TextKeyMap &Animation::AnimSource::getTextKeys() const + const SceneUtil::TextKeyMap &Animation::AnimSource::getTextKeys() const { return mKeyframes->mTextKeys; } void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -704,21 +604,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-3, 3, "/"); - mResourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) - addSingleAnimSource(name, baseModel); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "kf") + addSingleAnimSource(name, baseModel); } } @@ -729,8 +618,6 @@ namespace MWRender if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) kfname.replace(kfname.size()-4, 4, ".kf"); - else - return; addSingleAnimSource(kfname, baseModel); @@ -744,17 +631,15 @@ namespace MWRender if(!mResourceSystem->getVFS()->exists(kfname)) return; - std::shared_ptr animsrc; - animsrc.reset(new AnimSource); + auto animsrc = std::make_shared(); animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname); if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) return; const NodeMap& nodeMap = getNodeMap(); - - for (NifOsg::KeyframeHolder::KeyframeControllerMap::const_iterator it = animsrc->mKeyframes->mKeyframeControllers.begin(); - it != animsrc->mKeyframes->mKeyframeControllers.end(); ++it) + const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it) { std::string bonename = Misc::StringUtils::lowerCase(it->first); NodeMap::const_iterator found = nodeMap.find(bonename); @@ -769,25 +654,43 @@ namespace MWRender size_t blendMask = detectBlendMask(node); // clone the controller, because each Animation needs its own ControllerSource - osg::ref_ptr cloned = new NifOsg::KeyframeController(*it->second, osg::CopyOp::SHALLOW_COPY); + osg::ref_ptr cloned = osg::clone(it->second.get(), osg::CopyOp::SHALLOW_COPY); cloned->setSource(mAnimationTimePtr[blendMask]); animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); } - mAnimSources.push_back(animsrc); + mAnimSources.push_back(std::move(animsrc)); SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]); mObjectRoot->accept(assignVisitor); + // Determine the movement accumulation bone if necessary if (!mAccumRoot) { - NodeMap::const_iterator found = nodeMap.find("bip01"); - if (found == nodeMap.end()) - found = nodeMap.find("root bone"); - - if (found != nodeMap.end()) - mAccumRoot = found->second; + // Priority matters! bip01 is preferred. + static const std::array accumRootNames = + { + "bip01", + "root bone" + }; + NodeMap::const_iterator found = nodeMap.end(); + for (const std::string& name : accumRootNames) + { + found = nodeMap.find(name); + if (found == nodeMap.end()) + continue; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it) + { + if (Misc::StringUtils::lowerCase(it->first) == name) + { + mAccumRoot = found->second; + break; + } + } + if (mAccumRoot) + break; + } } } @@ -810,7 +713,7 @@ namespace MWRender AnimSourceList::const_iterator iter(mAnimSources.begin()); for(;iter != mAnimSources.end();++iter) { - const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); if (keys.hasGroupStart(anim)) return true; } @@ -822,7 +725,7 @@ namespace MWRender { for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { - const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); const auto found = keys.findGroupStart(groupname); if(found != keys.end()) @@ -835,7 +738,7 @@ namespace MWRender { for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { - const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); for(auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey) { @@ -847,8 +750,8 @@ namespace MWRender return -1.f; } - void Animation::handleTextKey(AnimState &state, const std::string &groupname, NifOsg::TextKeyMap::ConstIterator key, - const NifOsg::TextKeyMap& map) + void Animation::handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, + const SceneUtil::TextKeyMap& map) { const std::string &evt = key->second; @@ -911,7 +814,7 @@ namespace MWRender AnimSourceList::reverse_iterator iter(mAnimSources.rbegin()); for(;iter != mAnimSources.rend();++iter) { - const NifOsg::TextKeyMap &textkeys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap &textkeys = (*iter)->getTextKeys(); if(reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) { state.mSource = *iter; @@ -956,7 +859,7 @@ namespace MWRender resetActiveGroups(); } - bool Animation::reset(AnimState &state, const NifOsg::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback) + bool Animation::reset(AnimState &state, const SceneUtil::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback) { // Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two // separate walkforward keys, and the last one is supposed to be used. @@ -1088,8 +991,9 @@ namespace MWRender { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource - node->addUpdateCallback(it->second); - mActiveControllers.emplace_back(node, it->second); + osg::Callback* callback = it->second->getAsCallback(); + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); if (blendMask == 0 && node == mAccumRoot) { @@ -1186,7 +1090,7 @@ namespace MWRender AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); for(;animsrc != mAnimSources.rend();++animsrc) { - const NifOsg::TextKeyMap &keys = (*animsrc)->getTextKeys(); + const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); if (keys.hasGroupStart(groupname)) break; } @@ -1194,7 +1098,7 @@ namespace MWRender return 0.0f; float velocity = 0.0f; - const NifOsg::TextKeyMap &keys = (*animsrc)->getTextKeys(); + const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); const AnimSource::ControllerMap& ctrls = (*animsrc)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls.begin(); it != ctrls.end(); ++it) @@ -1215,7 +1119,7 @@ namespace MWRender while(!(velocity > 1.0f) && ++animiter != mAnimSources.rend()) { - const NifOsg::TextKeyMap &keys2 = (*animiter)->getTextKeys(); + const SceneUtil::TextKeyMap &keys2 = (*animiter)->getTextKeys(); const AnimSource::ControllerMap& ctrls2 = (*animiter)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls2.begin(); it != ctrls2.end(); ++it) @@ -1265,7 +1169,7 @@ namespace MWRender continue; } - const NifOsg::TextKeyMap &textkeys = state.mSource->getTextKeys(); + const SceneUtil::TextKeyMap &textkeys = state.mSource->getTextKeys(); auto textkey = textkeys.upperBound(state.getTime()); float timepassed = duration * state.mSpeedMult; @@ -1401,8 +1305,6 @@ namespace MWRender if (model.empty()) return; - const std::map& index = resourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -1410,21 +1312,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-4, 4, "/"); - resourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".nif") == 0) - loadBonesFromFile(node, name, resourceSystem); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "nif") + loadBonesFromFile(node, name, resourceSystem); } } @@ -1452,10 +1343,10 @@ namespace MWRender cache.insert(std::make_pair(model, created)); - return sceneMgr->createInstance(created); + return sceneMgr->getInstance(created); } else - return sceneMgr->createInstance(found->second); + return sceneMgr->getInstance(found->second); } else { @@ -1503,7 +1394,7 @@ namespace MWRender MWWorld::LiveCellRef *ref = mPtr.get(); if(ref->mBase->mFlags & ESM::Creature::Bipedal) { - defaultSkeleton = "meshes\\xbase_anim.nif"; + defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); inject = true; } } @@ -1616,7 +1507,8 @@ namespace MWRender { bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); - SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_Lighting, exterior); + mExtraLightSource->setActorFade(mAlpha); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1643,38 +1535,47 @@ namespace MWRender parentNode = mInsert; else { - NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + bonename); parentNode = found->second; } - osg::ref_ptr trans = new osg::PositionAttitudeTransform; + osg::ref_ptr trans = new SceneUtil::PositionAttitudeTransform; if (!mPtr.getClass().isNpc()) { - osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f / Constants::UnitsPerFoot); - float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); - trans->setScale(osg::Vec3f(scale, scale, scale)); + osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f); + float scale = std::max({bounds.x(), bounds.y(), bounds.z() / 2.f}) / 64.f; + if (scale > 1.f) + trans->setScale(osg::Vec3f(scale, scale, scale)); + float offset = 0.f; + if (bounds.z() < 128.f) + offset = bounds.z() - 128.f; + else if (bounds.z() < bounds.x() + bounds.y()) + offset = 128.f - bounds.z(); + if (MWBase::Environment::get().getWorld()->isFlying(mPtr)) + offset /= 20.f; + trans->setPosition(osg::Vec3f(0.f, 0.f, offset * scale)); } parentNode->addChild(trans); osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, trans); - node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + // 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)); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); - // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - node->accept(disableFreezeOnCullVisitor); node->setNodeMask(Mask_Effect); params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); params.mLoop = loop; params.mEffectId = effectId; params.mBoneName = bonename; - params.mAnimTime = std::shared_ptr(new EffectAnimationTime); + params.mAnimTime = std::make_shared(); trans->addUpdateCallback(new UpdateVfxCallback(params)); SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(params.mAnimTime)); @@ -1746,8 +1647,7 @@ namespace MWRender const osg::Node* Animation::getNode(const std::string &name) const { - std::string lowerName = Misc::StringUtils::lowerCase(name); - NodeMap::const_iterator found = getNodeMap().find(lowerName); + NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else @@ -1776,6 +1676,8 @@ namespace MWRender mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } + if (mExtraLightSource) + mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) @@ -1828,7 +1730,7 @@ namespace MWRender mRootController = addRotateController("bip01"); } - RotateController* Animation::addRotateController(std::string bone) + RotateController* Animation::addRotateController(const std::string &bone) { auto iter = getNodeMap().find(bone); if (iter == getNodeMap().end()) @@ -1839,7 +1741,7 @@ namespace MWRender osg::Callback* cb = node->getUpdateCallback(); while (cb) { - if (dynamic_cast(cb)) + if (dynamic_cast(cb)) { foundKeyframeCtrl = true; break; @@ -1920,7 +1822,7 @@ namespace MWRender if (!ptr.getClass().getEnchantment(ptr).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); } - if (ptr.getTypeName() == typeid(ESM::Light).name() && allowLight) + if (ptr.getType() == ESM::Light::sRecordId && allowLight) addExtraLight(getOrCreateObjectRoot(), ptr.get()->mBase); if (!allowLight && mObjectRoot) @@ -1930,13 +1832,13 @@ namespace MWRender visitor.remove(); } - if (SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) + if (Settings::Manager::getBool("day night switches", "Game") && SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) { AddSwitchCallbacksVisitor visitor; mObjectRoot->accept(visitor); } - if (ptr.getRefData().getCustomData() != nullptr && canBeHarvested()) + if (ptr.getRefData().getCustomData() != nullptr && ObjectAnimation::canBeHarvested()) { const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (!store.hasVisibleItems()) @@ -1949,7 +1851,7 @@ namespace MWRender bool ObjectAnimation::canBeHarvested() const { - if (mPtr.getTypeName() != typeid(ESM::Container).name()) + if (mPtr.getType() != ESM::Container::sRecordId) return false; const MWWorld::LiveCellRef* ref = mPtr.get(); @@ -1959,11 +1861,6 @@ namespace MWRender return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } - Animation::AnimState::~AnimState() - { - - } - // ------------------------------ PartHolder::PartHolder(osg::ref_ptr node) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 9d03831be5..ed1ddb78d2 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -4,10 +4,13 @@ #include "../mwworld/ptr.hpp" #include +#include #include -#include +#include +#include #include +#include namespace ESM { @@ -20,14 +23,10 @@ namespace Resource class ResourceSystem; } -namespace NifOsg +namespace SceneUtil { class KeyframeHolder; class KeyframeController; -} - -namespace SceneUtil -{ class LightSource; class LightListCallback; class Skeleton; @@ -62,7 +61,7 @@ public: ~PartHolder(); - osg::ref_ptr getNode() + const osg::ref_ptr& getNode() const { return mNode; } @@ -73,7 +72,7 @@ private: void operator= (const PartHolder&); PartHolder(const PartHolder&); }; -typedef std::shared_ptr PartHolderPtr; +using PartHolderPtr = std::unique_ptr; struct EffectParams { @@ -106,7 +105,7 @@ public: BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody }; /* This is the number of *discrete* blend masks. */ - static const size_t sNumBlendMasks = 4; + static constexpr size_t sNumBlendMasks = 4; /// Holds an animation priority value for each BoneGroup. struct AnimPriority @@ -150,8 +149,8 @@ public: class TextKeyListener { public: - virtual void handleTextKey(const std::string &groupname, NifOsg::TextKeyMap::ConstIterator key, - const NifOsg::TextKeyMap& map) = 0; + virtual void handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, + const SceneUtil::TextKeyMap& map) = 0; virtual ~TextKeyListener() = default; }; @@ -160,6 +159,8 @@ public: virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -211,7 +212,7 @@ protected: mLoopCount(0), mPriority(0), mBlendMask(0), mAutoDisable(true) { } - ~AnimState(); + ~AnimState() = default; float getTime() const { @@ -242,19 +243,17 @@ protected: osg::ref_ptr mAccumRoot; // The controller animating that node. - osg::ref_ptr mAccumCtrl; + osg::ref_ptr mAccumCtrl; // Used to reset the position of the accumulation root every frame - the movement should be applied to the physics system osg::ref_ptr mResetAccumRootCallback; // 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; + std::vector, osg::ref_ptr>> mActiveControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; - // Stored in all lowercase for a case-insensitive lookup - typedef std::map > NodeMap; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; @@ -275,13 +274,14 @@ protected: float mLegsYawRadians; float mBodyPitchRadians; - RotateController* addRotateController(std::string bone); + RotateController* addRotateController(const std::string& bone); bool mHasMagicEffects; osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; + osg::ref_ptr mExtraLightSource; float mAlpha; @@ -306,12 +306,12 @@ protected: * the marker is not found, or if the markers are the same, it returns * false. */ - bool reset(AnimState &state, const NifOsg::TextKeyMap &keys, + bool reset(AnimState &state, const SceneUtil::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback); - void handleTextKey(AnimState &state, const std::string &groupname, NifOsg::TextKeyMap::ConstIterator key, - const NifOsg::TextKeyMap& map); + void handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, + const SceneUtil::TextKeyMap& map); /** Sets the root model of the object. * @@ -509,7 +509,7 @@ public: bool canBeHarvested() const override; }; -class UpdateVfxCallback : public osg::NodeCallback +class UpdateVfxCallback : public SceneUtil::NodeCallback { public: UpdateVfxCallback(EffectParams& params) @@ -522,7 +522,7 @@ public: bool mFinished; EffectParams mParams; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osg::NodeVisitor* nv); private: double mStartingTime; diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp index 61570be452..b14b64e771 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.cpp +++ b/apps/openmw/mwrender/bulletdebugdraw.cpp @@ -4,56 +4,100 @@ #include #include +#include #include #include +#include +#include +#include +#include +#include #include "bulletdebugdraw.hpp" #include "vismask.hpp" +#include +#include + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + namespace MWRender { -DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world) +DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode) : mParentNode(parentNode), - mWorld(world), - mDebugOn(true) + mWorld(world) { - - createGeometry(); + DebugDrawer::setDebugMode(debugMode); } void DebugDrawer::createGeometry() { - if (!mGeometry) + if (!mLinesGeometry) { - mGeometry = new osg::Geometry; - mGeometry->setNodeMask(Mask_Debug); - - mVertices = new osg::Vec3Array; - mColors = new osg::Vec4Array; - - mDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); - - mGeometry->setUseDisplayList(false); - mGeometry->setVertexArray(mVertices); - mGeometry->setColorArray(mColors); - mGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); - mGeometry->setDataVariance(osg::Object::DYNAMIC); - mGeometry->addPrimitiveSet(mDrawArrays); - - mParentNode->addChild(mGeometry); + mLinesGeometry = new osg::Geometry; + mTrisGeometry = new osg::Geometry; + mLinesGeometry->setNodeMask(Mask_Debug); + mTrisGeometry->setNodeMask(Mask_Debug); + + mLinesVertices = new osg::Vec3Array; + mTrisVertices = new osg::Vec3Array; + mLinesColors = new osg::Vec4Array; + + mLinesDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); + mTrisDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES); + + mLinesGeometry->setUseDisplayList(false); + mLinesGeometry->setVertexArray(mLinesVertices); + mLinesGeometry->setColorArray(mLinesColors); + mLinesGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); + mLinesGeometry->setDataVariance(osg::Object::DYNAMIC); + mLinesGeometry->addPrimitiveSet(mLinesDrawArrays); + + mTrisGeometry->setUseDisplayList(false); + mTrisGeometry->setVertexArray(mTrisVertices); + mTrisGeometry->setDataVariance(osg::Object::DYNAMIC); + mTrisGeometry->addPrimitiveSet(mTrisDrawArrays); + + mParentNode->addChild(mLinesGeometry); + mParentNode->addChild(mTrisGeometry); + + auto* stateSet = new osg::StateSet; + stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); + stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0, SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0)); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateSet->setAttribute(material); + mLinesGeometry->setStateSet(stateSet); + mTrisGeometry->setStateSet(stateSet); + mShapesRoot = new osg::Group; + mShapesRoot->setStateSet(stateSet); + mShapesRoot->setDataVariance(osg::Object::DYNAMIC); + mShapesRoot->setNodeMask(Mask_Debug); + mParentNode->addChild(mShapesRoot); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mLinesGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mTrisGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mShapesRoot, "debug"); } } void DebugDrawer::destroyGeometry() { - if (mGeometry) + if (mLinesGeometry) { - mParentNode->removeChild(mGeometry); - mGeometry = nullptr; - mVertices = nullptr; - mDrawArrays = nullptr; + mParentNode->removeChild(mLinesGeometry); + mParentNode->removeChild(mTrisGeometry); + mParentNode->removeChild(mShapesRoot); + mLinesGeometry = nullptr; + mLinesVertices = nullptr; + mLinesColors = nullptr; + mLinesDrawArrays = nullptr; + mTrisGeometry = nullptr; + mTrisVertices = nullptr; + mTrisDrawArrays = nullptr; } } @@ -66,23 +110,60 @@ void DebugDrawer::step() { if (mDebugOn) { - mVertices->clear(); - mColors->clear(); + mLinesVertices->clear(); + mTrisVertices->clear(); + mLinesColors->clear(); + mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); mWorld->debugDrawWorld(); showCollisions(); - mDrawArrays->setCount(mVertices->size()); - mVertices->dirty(); - mColors->dirty(); - mGeometry->dirtyBound(); + mLinesDrawArrays->setCount(mLinesVertices->size()); + mTrisDrawArrays->setCount(mTrisVertices->size()); + mLinesVertices->dirty(); + mTrisVertices->dirty(); + mLinesColors->dirty(); + mLinesGeometry->dirtyBound(); + mTrisGeometry->dirtyBound(); } } void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to, const btVector3 &color) { - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,1,1,1}); - mColors->push_back({1,1,1,1}); + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({1,1,1,1}); + mLinesColors->push_back({1,1,1,1}); + +#if BT_BULLET_VERSION < 317 + size_t size = mLinesVertices->size(); + if (size >= 6 + && (*mLinesVertices)[size - 1] == (*mLinesVertices)[size - 6] + && (*mLinesVertices)[size - 2] == (*mLinesVertices)[size - 3] + && (*mLinesVertices)[size - 4] == (*mLinesVertices)[size - 5]) + { + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + } +#endif +} + +void DebugDrawer::drawTriangle(const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) +{ + mTrisVertices->push_back(Misc::Convert::toOsg(v0)); + mTrisVertices->push_back(Misc::Convert::toOsg(v1)); + mTrisVertices->push_back(Misc::Convert::toOsg(v2)); } void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) @@ -97,10 +178,10 @@ void DebugDrawer::showCollisions() { if (now - created < std::chrono::seconds(2)) { - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,0,0,1}); - mColors->push_back({1,0,0,1}); + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({1,0,0,1}); + mLinesColors->push_back({1,0,0,1}); } } mCollisionViews.erase(std::remove_if(mCollisionViews.begin(), mCollisionViews.end(), @@ -108,12 +189,11 @@ void DebugDrawer::showCollisions() mCollisionViews.end()); } -void DebugDrawer::drawContactPoint(const btVector3 &PointOnB, const btVector3 &normalOnB, btScalar distance, int lifeTime, const btVector3 &color) +void DebugDrawer::drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) { - mVertices->push_back(Misc::Convert::toOsg(PointOnB)); - mVertices->push_back(Misc::Convert::toOsg(PointOnB) + (Misc::Convert::toOsg(normalOnB) * distance * 20)); - mColors->push_back({1,1,1,1}); - mColors->push_back({1,1,1,1}); + auto* geom = new osg::ShapeDrawable(new osg::Sphere(Misc::Convert::toOsg(transform.getOrigin()), radius)); + geom->setColor(osg::Vec4(1, 1, 1, 1)); + mShapesRoot->addChild(geom); } void DebugDrawer::reportErrorWarning(const char *warningString) diff --git a/apps/openmw/mwrender/bulletdebugdraw.hpp b/apps/openmw/mwrender/bulletdebugdraw.hpp index f07ce2e2ed..cea5794ba7 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.hpp +++ b/apps/openmw/mwrender/bulletdebugdraw.hpp @@ -32,14 +32,18 @@ private: CollisionView(btVector3 orig, btVector3 normal) : mOrig(orig), mEnd(orig + normal * 20), mCreated(std::chrono::steady_clock::now()) {}; }; std::vector mCollisionViews; + osg::ref_ptr mShapesRoot; protected: osg::ref_ptr mParentNode; btCollisionWorld *mWorld; - osg::ref_ptr mGeometry; - osg::ref_ptr mVertices; - osg::ref_ptr mColors; - osg::ref_ptr mDrawArrays; + osg::ref_ptr mLinesGeometry; + osg::ref_ptr mTrisGeometry; + osg::ref_ptr mLinesVertices; + osg::ref_ptr mTrisVertices; + osg::ref_ptr mLinesColors; + osg::ref_ptr mLinesDrawArrays; + osg::ref_ptr mTrisDrawArrays; bool mDebugOn; @@ -48,18 +52,21 @@ protected: public: - DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world); + DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode = 1); ~DebugDrawer(); void step(); void drawLine(const btVector3& from,const btVector3& to,const btVector3& color) override; + void drawTriangle(const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) override; + void addCollision(const btVector3& orig, const btVector3& normal); void showCollisions(); - void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override; + void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override {}; + void drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) override; void reportErrorWarning(const char* warningString) override; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index b964eacffb..625b6c3e97 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -25,7 +26,7 @@ namespace { -class UpdateRenderCameraCallback : public osg::NodeCallback +class UpdateRenderCameraCallback : public SceneUtil::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) @@ -33,12 +34,10 @@ public: { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* cam, osg::NodeVisitor* nv) { - osg::Camera* cam = static_cast(node); - // traverse first to update animations, in case the camera is attached to an animated node - traverse(node, nv); + traverse(cam, nv); mCamera->updateCamera(cam); } @@ -54,44 +53,29 @@ namespace MWRender Camera::Camera (osg::Camera* camera) : mHeightScale(1.f), + mCollisionType((MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor) | MWPhysics::CollisionType_CameraOnly), mCamera(camera), mAnimation(nullptr), mFirstPersonView(true), - mMode(Mode::Normal), + mMode(Mode::FirstPerson), mVanityAllowed(true), - mStandingPreviewAllowed(Settings::Manager::getBool("preview if stand still", "Camera")), - mDeferredRotationAllowed(Settings::Manager::getBool("deferred preview rotation", "Camera")), - mNearest(30.f), - mFurthest(800.f), - mIsNearest(false), + mDeferredRotationAllowed(true), + mProcessViewChange(false), mHeight(124.f), - mBaseCameraDistance(Settings::Manager::getFloat("third person camera distance", "Camera")), mPitch(0.f), mYaw(0.f), mRoll(0.f), - mVanityToggleQueued(false), - mVanityToggleQueuedValue(false), - mViewModeToggleQueued(false), mCameraDistance(0.f), - mMaxNextCameraDistance(800.f), + mPreferredCameraDistance(0.f), mFocalPointCurrentOffset(osg::Vec2d()), mFocalPointTargetOffset(osg::Vec2d()), mFocalPointTransitionSpeedCoef(1.f), mSkipFocalPointTransition(true), mPreviousTransitionInfluence(0.f), - mSmoothedSpeed(0.f), - mZoomOutWhenMoveCoef(Settings::Manager::getFloat("zoom out when move coef", "Camera")), - mDynamicCameraDistanceEnabled(false), - mShowCrosshairInThirdPersonMode(false), - mHeadBobbingEnabled(Settings::Manager::getBool("head bobbing", "Camera")), - mHeadBobbingOffset(0.f), - mHeadBobbingWeight(0.f), - mTotalMovement(0.f), + mShowCrosshair(false), mDeferredRotation(osg::Vec3f()), mDeferredRotationDisabled(false) { - mCameraDistance = mBaseCameraDistance; - mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } @@ -101,7 +85,7 @@ namespace MWRender mCamera->removeUpdateCallback(mUpdateCallback); } - osg::Vec3d Camera::getFocalPoint() const + osg::Vec3d Camera::calculateTrackedPosition() const { if (!mTrackingNode) return osg::Vec3d(); @@ -109,194 +93,147 @@ namespace MWRender if (nodepaths.empty()) return osg::Vec3d(); osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); - - osg::Vec3d position = worldMat.getTrans(); - if (isFirstPerson()) - position.z() += mHeadBobbingOffset; - else - { - position.z() += mHeight * mHeightScale; - - // We subtract 10.f here and add it within focalPointOffset in order to avoid camera clipping through ceiling. - // Needed because character's head can be a bit higher than collision area. - position.z() -= 10.f; - - position += getFocalPointOffset() + mFocalPointAdjustment; - } - return position; + osg::Vec3d res = worldMat.getTrans(); + if (mMode != Mode::FirstPerson) + res.z() += mHeight * mHeightScale; + return res; } osg::Vec3d Camera::getFocalPointOffset() const { - osg::Vec3d offset(0, 0, 10.f); - offset.x() += mFocalPointCurrentOffset.x() * cos(getYaw()); - offset.y() += mFocalPointCurrentOffset.x() * sin(getYaw()); - offset.z() += mFocalPointCurrentOffset.y(); + osg::Vec3d offset; + offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw); + offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw); + offset.z() = mFocalPointCurrentOffset.y(); return offset; } - void Camera::getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const - { - focal = getFocalPoint(); - osg::Vec3d offset(0,0,0); - if (!isFirstPerson()) - { - osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); - offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); - } - camera = focal + offset; - } - void Camera::updateCamera(osg::Camera *cam) { - osg::Vec3d focal, position; - getPosition(focal, position); - - osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); + 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::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); - cam->setViewMatrixAsLookAt(position, position + forward, up); - } - - void Camera::updateHeadBobbing(float duration) { - static const float doubleStepLength = Settings::Manager::getFloat("head bobbing step", "Camera") * 2; - static const float stepHeight = Settings::Manager::getFloat("head bobbing height", "Camera"); - static const float maxRoll = osg::DegreesToRadians(Settings::Manager::getFloat("head bobbing roll", "Camera")); - - if (MWBase::Environment::get().getWorld()->isOnGround(mTrackingPtr)) - mHeadBobbingWeight = std::min(mHeadBobbingWeight + duration * 5, 1.f); - else - mHeadBobbingWeight = std::max(mHeadBobbingWeight - duration * 5, 0.f); - - float doubleStepState = mTotalMovement / doubleStepLength - std::floor(mTotalMovement / doubleStepLength); // from 0 to 1 during 2 steps - float stepState = std::abs(doubleStepState * 4 - 2) - 1; // from -1 to 1 on even steps and from 1 to -1 on odd steps - float effect = (1 - std::cos(stepState * osg::DegreesToRadians(30.f))) * 7.5f; // range from 0 to 1 - float coef = std::min(mSmoothedSpeed / 300.f, 1.f) * mHeadBobbingWeight; - mHeadBobbingOffset = (0.5f - effect) * coef * stepHeight; // range from -stepHeight/2 to stepHeight/2 - mRoll = osg::sign(stepState) * effect * coef * maxRoll; // range from -maxRoll to maxRoll - } - - void Camera::reset() - { - togglePreviewMode(false); - toggleVanityMode(false); - if (!mFirstPersonView) - toggleViewMode(); - } - - void Camera::rotateCamera(float pitch, float yaw, bool adjust) - { - if (adjust) + osg::Vec3d pos = mPosition; + if (mMode == Mode::FirstPerson) { - pitch += getPitch(); - yaw += getYaw(); + // It is a hack. Camera position depends on neck animation. + // Animations are updated in OSG cull traversal and in order to avoid 1 frame delay we + // recalculate the position here. Note that it becomes different from mPosition that + // is used in other parts of the code. + // TODO: detach camera from OSG animation and get rid of this hack. + osg::Vec3d recalculatedTrackedPosition = calculateTrackedPosition(); + pos = calculateFirstPersonPosition(recalculatedTrackedPosition); } - setYaw(yaw); - setPitch(pitch); + cam->setViewMatrixAsLookAt(pos, pos + forward, up); + mViewMatrix = cam->getViewMatrix(); } void Camera::update(float duration, bool paused) { - if (mAnimation->upperBodyReady()) - { - // Now process the view changes we queued earlier - if (mVanityToggleQueued) - { - toggleVanityMode(mVanityToggleQueuedValue); - mVanityToggleQueued = false; - } - if (mViewModeToggleQueued) - { - togglePreviewMode(false); - toggleViewMode(); - mViewModeToggleQueued = false; - } - } + mLockPitch = mLockYaw = false; + if (mQueuedMode && mAnimation->upperBodyReady()) + setMode(*mQueuedMode); + if (mProcessViewChange) + processViewChange(); if (paused) return; // only show the crosshair in game mode MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); - wm->showCrosshair(!wm->isGuiMode() && mMode != Mode::Preview && mMode != Mode::Vanity - && (mFirstPersonView || mShowCrosshairInThirdPersonMode)); - - if(mMode == Mode::Vanity) - rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); - - if (isFirstPerson() && mHeadBobbingEnabled) - updateHeadBobbing(duration); - else - mRoll = mHeadBobbingOffset = 0; + wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair); updateFocalPointOffset(duration); updatePosition(); + } - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - mTotalMovement += speed * duration; - speed /= (1.f + speed / 500.f); - float maxDelta = 300.f * duration; - mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); - - mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); - updateStandingPreviewMode(); + osg::Vec3d Camera::calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const + { + osg::Vec3d res = trackedPosition; + osg::Vec2f horizontalOffset = Misc::rotateVec2f(osg::Vec2f(mFirstPersonOffset.x(), mFirstPersonOffset.y()), mYaw); + res.x() += horizontalOffset.x(); + res.y() += horizontalOffset.y(); + res.z() += mFirstPersonOffset.z(); + return res; } void Camera::updatePosition() { - mFocalPointAdjustment = osg::Vec3d(); - if (isFirstPerson()) + mTrackedPosition = calculateTrackedPosition(); + if (mMode == Mode::Static) + return; + if (mMode == Mode::FirstPerson) + { + mPosition = calculateFirstPersonPosition(mTrackedPosition); + mCameraDistance = 0; return; + } - const float cameraObstacleLimit = 5.0f; - const float focalObstacleLimit = 10.f; + constexpr float cameraObstacleLimit = 5.0f; + constexpr float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); // Adjust focal point to prevent clipping. - osg::Vec3d focal = getFocalPoint(); osg::Vec3d focalOffset = getFocalPointOffset(); + osg::Vec3d focal = mTrackedPosition + focalOffset; + focalOffset.z() += 10.f; // Needed to avoid camera clipping through the ceiling because + // character's head can be a bit higher than the collision area. float offsetLen = focalOffset.length(); if (offsetLen > 0) { - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit, mCollisionType); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; - mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef); + focal += focalOffset * std::max(-1.0, adjustmentCoef); } } - // Calculate camera distance. - mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection(); - if (mDynamicCameraDistanceEnabled) - mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); - osg::Vec3d cameraPos; - getPosition(focal, cameraPos); - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit); + // Adjust camera distance. + mCameraDistance = mPreferredCameraDistance; + osg::Quat orient = osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1,0,0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0,0,1)); + osg::Vec3d offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, focal + offset, cameraObstacleLimit, mCollisionType); if (result.mHit) + { mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); + offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + } + + mPosition = focal + offset; } - void Camera::updateStandingPreviewMode() + void Camera::setMode(Mode newMode, bool force) { - if (!mStandingPreviewAllowed) + if (mMode == newMode) + return; + Mode oldMode = mMode; + if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && mAnimation && !mAnimation->upperBodyReady()) + { + // Changing the view will stop all playing animations, so if we are playing + // anything important, queue the view change for later + mQueuedMode = newMode; return; - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - bool combat = mTrackingPtr.getClass().isActor() && - mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing; - bool standingStill = speed == 0 && !combat && !mFirstPersonView; - if (!standingStill && mMode == Mode::StandingPreview) + } + mMode = newMode; + mQueuedMode = std::nullopt; + if (newMode == Mode::FirstPerson) + mFirstPersonView = true; + else if (newMode == Mode::ThirdPerson) + mFirstPersonView = false; + calculateDeferredRotation(); + if (oldMode == Mode::FirstPerson || newMode == Mode::FirstPerson) { - mMode = Mode::Normal; - calculateDeferredRotation(); + instantTransition(); + mProcessViewChange = true; } - else if (standingStill && mMode == Mode::Normal) - mMode = Mode::StandingPreview; } - void Camera::setFocalPointTargetOffset(osg::Vec2d v) + void Camera::setFocalPointTargetOffset(const osg::Vec2d& v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; @@ -346,81 +283,16 @@ namespace MWRender void Camera::toggleViewMode(bool force) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (!mAnimation->upperBodyReady() && !force) - { - mViewModeToggleQueued = true; - return; - } - else - mViewModeToggleQueued = false; - - if (mTrackingPtr.getClass().isActor()) - mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).setSideMovementAngle(0); - - mFirstPersonView = !mFirstPersonView; - updateStandingPreviewMode(); - instantTransition(); - processViewChange(); - } - - void Camera::allowVanityMode(bool allow) - { - if (!allow && mMode == Mode::Vanity) - { - disableDeferredPreviewRotation(); - toggleVanityMode(false); - } - mVanityAllowed = allow; + setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force); } bool Camera::toggleVanityMode(bool enable) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (mFirstPersonView && !mAnimation->upperBodyReady()) - { - mVanityToggleQueued = true; - mVanityToggleQueuedValue = enable; - return false; - } - - if (!mVanityAllowed && enable) - return false; - - if ((mMode == Mode::Vanity) == enable) - return true; - mMode = enable ? Mode::Vanity : Mode::Normal; - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); if (!enable) - calculateDeferredRotation(); - - processViewChange(); - return true; - } - - void Camera::togglePreviewMode(bool enable) - { - if (mFirstPersonView && !mAnimation->upperBodyReady()) - return; - - if((mMode == Mode::Preview) == enable) - return; - - mMode = enable ? Mode::Preview : Mode::Normal; - if (mMode == Mode::Normal) - updateStandingPreviewMode(); - else if (mFirstPersonView) - instantTransition(); - if (mMode == Mode::Normal) - { - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); - calculateDeferredRotation(); - } - processViewChange(); + setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false); + else if (mVanityAllowed) + setMode(Mode::Vanity, false); + return (mMode == Mode::Vanity) == enable; } void Camera::setSneakOffset(float offset) @@ -428,67 +300,42 @@ namespace MWRender mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } - void Camera::setYaw(float angle) + void Camera::setYaw(float angle, bool force) { - mYaw = Misc::normalizeAngle(angle); + if (!mLockYaw || force) + mYaw = Misc::normalizeAngle(angle); + if (force) + mLockYaw = true; } - void Camera::setPitch(float angle) + void Camera::setPitch(float angle, bool force) { const float epsilon = 0.000001f; - float limit = osg::PI_2 - epsilon; - mPitch = osg::clampBetween(angle, -limit, limit); - } - - float Camera::getCameraDistance() const - { - if (isFirstPerson()) - return 0.f; - return mCameraDistance; - } - - void Camera::adjustCameraDistance(float delta) - { - if (!isFirstPerson()) - { - if(isNearest() && delta < 0.f && getMode() != Mode::Preview && getMode() != Mode::Vanity) - toggleViewMode(); - else - mBaseCameraDistance = std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance) + delta; - } - else if (delta > 0.f) - { - toggleViewMode(); - mBaseCameraDistance = 0; - } - - mIsNearest = mBaseCameraDistance <= mNearest; - mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); - Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); + float limit = static_cast(osg::PI_2) - epsilon; + if (!mLockPitch || force) + mPitch = std::clamp(angle, -limit, limit); + if (force) + mLockPitch = true; } - float Camera::getCameraDistanceCorrection() const + void Camera::setStaticPosition(const osg::Vec3d& pos) { - if (!mDynamicCameraDistanceEnabled) - return 0; - - float pitchCorrection = std::max(-getPitch(), 0.f) * 50.f; - - float smoothedSpeedSqr = mSmoothedSpeed * mSmoothedSpeed; - float speedCorrection = smoothedSpeedSqr / (smoothedSpeedSqr + 300.f*300.f) * mZoomOutWhenMoveCoef; - - return pitchCorrection + speedCorrection; + if (mMode != Mode::Static) + throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode"); + mPosition = pos; } void Camera::setAnimation(NpcAnimation *anim) { mAnimation = anim; - processViewChange(); + mProcessViewChange = true; } void Camera::processViewChange() { - if(isFirstPerson()) + if (mTrackingPtr.isEmpty()) + return; + if (mMode == Mode::FirstPerson) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); @@ -506,12 +353,12 @@ namespace MWRender else mHeightScale = 1.f; } - rotateCamera(getPitch(), getYaw(), false); + mProcessViewChange = false; } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { - if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty()) + if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; @@ -544,6 +391,8 @@ namespace MWRender void Camera::rotateCameraToTrackingPtr() { + if (mMode == Mode::Static || mTrackingPtr.isEmpty()) + return; setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x()); setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z()); } @@ -558,8 +407,13 @@ namespace MWRender void Camera::calculateDeferredRotation() { + if (mMode == Mode::Static) + { + mDeferredRotation = osg::Vec3f(); + return; + } MWWorld::Ptr ptr = mTrackingPtr; - if (isVanityOrPreviewModeEnabled() || ptr.isEmpty()) + if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty()) return; if (mFirstPersonView) { @@ -569,6 +423,8 @@ namespace MWRender mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); + if (!mDeferredRotationAllowed) + mDeferredRotationDisabled = true; } } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 9e2b608dfd..e17e63ddaf 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -1,9 +1,11 @@ #ifndef GAME_MWRENDER_CAMERA_H #define GAME_MWRENDER_CAMERA_H +#include #include #include +#include #include #include @@ -12,7 +14,7 @@ namespace osg { class Camera; - class NodeCallback; + class Callback; class Node; } @@ -24,73 +26,8 @@ namespace MWRender class Camera { public: - enum class Mode { Normal, Vanity, Preview, StandingPreview }; + enum class Mode : int {Static = 0, FirstPerson = 1, ThirdPerson = 2, Vanity = 3, Preview = 4}; - private: - MWWorld::Ptr mTrackingPtr; - osg::ref_ptr mTrackingNode; - float mHeightScale; - - osg::ref_ptr mCamera; - - NpcAnimation *mAnimation; - - bool mFirstPersonView; - Mode mMode; - bool mVanityAllowed; - bool mStandingPreviewAllowed; - bool mDeferredRotationAllowed; - - float mNearest; - float mFurthest; - bool mIsNearest; - - float mHeight, mBaseCameraDistance; - float mPitch, mYaw, mRoll; - - bool mVanityToggleQueued; - bool mVanityToggleQueuedValue; - bool mViewModeToggleQueued; - - float mCameraDistance; - float mMaxNextCameraDistance; - - osg::Vec3d mFocalPointAdjustment; - osg::Vec2d mFocalPointCurrentOffset; - osg::Vec2d mFocalPointTargetOffset; - float mFocalPointTransitionSpeedCoef; - bool mSkipFocalPointTransition; - - // This fields are used to make focal point transition smooth if previous transition was not finished. - float mPreviousTransitionInfluence; - osg::Vec2d mFocalPointTransitionSpeed; - osg::Vec2d mPreviousTransitionSpeed; - osg::Vec2d mPreviousExtraOffset; - - float mSmoothedSpeed; - float mZoomOutWhenMoveCoef; - bool mDynamicCameraDistanceEnabled; - bool mShowCrosshairInThirdPersonMode; - - bool mHeadBobbingEnabled; - float mHeadBobbingOffset; - float mHeadBobbingWeight; // Value from 0 to 1 for smooth enabling/disabling. - float mTotalMovement; // Needed for head bobbing. - void updateHeadBobbing(float duration); - - void updateFocalPointOffset(float duration); - void updatePosition(); - float getCameraDistanceCorrection() const; - - osg::ref_ptr mUpdateCallback; - - // Used to rotate player to the direction of view after exiting preview or vanity mode. - osg::Vec3f mDeferredRotation; - bool mDeferredRotationDisabled; - void calculateDeferredRotation(); - void updateStandingPreviewMode(); - - public: Camera(osg::Camera* camera); ~Camera(); @@ -99,36 +36,38 @@ namespace MWRender MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; } void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } - void setFocalPointTargetOffset(osg::Vec2d v); + float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; } + void setFocalPointTargetOffset(const osg::Vec2d& v); + osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; } void instantTransition(); - void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; } - void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; } + void showCrosshair(bool v) { mShowCrosshair = v; } /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); /// Reset to defaults - void reset(); + void reset() { setMode(Mode::FirstPerson); } - /// Set where the camera is looking at. Uses Morrowind (euler) angles - /// \param rot Rotation angles in radians - void rotateCamera(float pitch, float yaw, bool adjust); void rotateCameraToTrackingPtr(); + float getPitch() const { return mPitch; } float getYaw() const { return mYaw; } - void setYaw(float angle); + float getRoll() const { return mRoll; } - float getPitch() const { return mPitch; } - void setPitch(float angle); + void setPitch(float angle, bool force = false); + void setYaw(float angle, bool force = false); + void setRoll(float angle) { mRoll = angle; } + + float getExtraPitch() const { return mExtraPitch; } + float getExtraYaw() const { return mExtraYaw; } + float getExtraRoll() const { return mExtraRoll; } + void setExtraPitch(float angle) { mExtraPitch = angle; } + void setExtraYaw(float angle) { mExtraYaw = angle; } + void setExtraRoll(float angle) { mExtraRoll = angle; } /// @param Force view mode switch, even if currently not allowed by the animation. void toggleViewMode(bool force=false); - bool toggleVanityMode(bool enable); - void allowVanityMode(bool allow); - - /// @note this may be ignored if an important animation is currently playing - void togglePreviewMode(bool enable); void applyDeferredPreviewRotationToPlayer(float dt); void disableDeferredPreviewRotation() { mDeferredRotationDisabled = true; } @@ -136,29 +75,91 @@ namespace MWRender /// \brief Lowers the camera for sneak. void setSneakOffset(float offset); - bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; } - void processViewChange(); void update(float duration, bool paused=false); - /// Adds distDelta to the camera distance. Switches 3rd/1st person view if distance is less than limit. - void adjustCameraDistance(float distDelta); - - float getCameraDistance() const; + float getCameraDistance() const { return mCameraDistance; } + void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; } void setAnimation(NpcAnimation *anim); - osg::Vec3d getFocalPoint() const; - osg::Vec3d getFocalPointOffset() const; - - /// Stores focal and camera world positions in passed arguments - void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; + osg::Vec3d getTrackedPosition() const { return mTrackedPosition; } + const osg::Vec3d& getPosition() const { return mPosition; } + void setStaticPosition(const osg::Vec3d& pos); - bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; } + bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::Vanity || mMode == Mode::Preview; } Mode getMode() const { return mMode; } + std::optional getQueuedMode() const { return mQueuedMode; } + void setMode(Mode mode, bool force = true); + + void allowCharacterDeferredRotation(bool v) { mDeferredRotationAllowed = v; } + void calculateDeferredRotation(); + void setFirstPersonOffset(const osg::Vec3f& v) { mFirstPersonOffset = v; } + osg::Vec3f getFirstPersonOffset() const { return mFirstPersonOffset; } + + int getCollisionType() const { return mCollisionType; } + void setCollisionType(int collisionType) { mCollisionType = collisionType; } + + const osg::Matrixf& getViewMatrix() const { return mViewMatrix; } + + private: + MWWorld::Ptr mTrackingPtr; + osg::ref_ptr mTrackingNode; + osg::Vec3d mTrackedPosition; + float mHeightScale; + int mCollisionType; - bool isNearest() const { return mIsNearest; } + osg::ref_ptr mCamera; + + NpcAnimation *mAnimation; + + // Always 'true' if mMode == `FirstPerson`. Also it is 'true' in `Vanity` or `Preview` modes if + // the camera should return to `FirstPerson` view after it. + bool mFirstPersonView; + + Mode mMode; + std::optional mQueuedMode; + bool mVanityAllowed; + bool mDeferredRotationAllowed; + + bool mProcessViewChange; + + float mHeight; + float mPitch, mYaw, mRoll; + float mExtraPitch = 0, mExtraYaw = 0, mExtraRoll = 0; + bool mLockPitch = false, mLockYaw = false; + osg::Vec3d mPosition; + osg::Matrixf mViewMatrix; + + float mCameraDistance, mPreferredCameraDistance; + + osg::Vec3f mFirstPersonOffset{0, 0, 0}; + + osg::Vec2d mFocalPointCurrentOffset; + osg::Vec2d mFocalPointTargetOffset; + float mFocalPointTransitionSpeedCoef; + bool mSkipFocalPointTransition; + + // This fields are used to make focal point transition smooth if previous transition was not finished. + float mPreviousTransitionInfluence; + osg::Vec2d mFocalPointTransitionSpeed; + osg::Vec2d mPreviousTransitionSpeed; + osg::Vec2d mPreviousExtraOffset; + + bool mShowCrosshair; + + osg::Vec3d calculateTrackedPosition() const; + osg::Vec3d calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const; + osg::Vec3d getFocalPointOffset() const; + void updateFocalPointOffset(float duration); + void updatePosition(); + + osg::ref_ptr mUpdateCallback; + + // Used to rotate player to the direction of view after exiting preview or vanity mode. + osg::Vec3f mDeferredRotation; + bool mDeferredRotationDisabled; }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 89db3e5f46..73bd9db484 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -5,20 +5,28 @@ #include #include #include +#include #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/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -32,16 +40,17 @@ namespace MWRender { - class DrawOnceCallback : public osg::NodeCallback + class DrawOnceCallback : public SceneUtil::NodeCallback { public: - DrawOnceCallback () + DrawOnceCallback(osg::Node* subgraph) : mRendered(false) , mLastRenderedFrame(0) + , mSubgraph(subgraph) { } - void operator () (osg::Node* node, osg::NodeVisitor* nv) override + void operator () (osg::Node* node, osg::NodeVisitor* nv) { if (!mRendered) { @@ -55,6 +64,9 @@ namespace MWRender nv->setFrameStamp(fs); + // Update keyframe controllers in the scene graph first... + // RTTNode does not continue update traversal, so manually continue the update traversal since we need it. + mSubgraph->accept(*nv); traverse(node, nv); nv->setFrameStamp(previousFramestamp); @@ -78,10 +90,12 @@ namespace MWRender private: bool mRendered; unsigned int mLastRenderedFrame; + osg::ref_ptr mSubgraph; }; - // Set up alpha blending to Additive mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO + // Set up alpha blending mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO + // This makes the RTT have premultiplied alpha, though, so the source blend factor must be GL_ONE when it's applied class SetUpBlendVisitor : public osg::NodeVisitor { public: @@ -91,23 +105,117 @@ namespace MWRender void apply(osg::Node& node) override { - if (osg::StateSet* stateset = node.getStateSet()) + if (osg::ref_ptr stateset = node.getStateSet()) { + osg::ref_ptr newStateSet; if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN) { - osg::ref_ptr newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); osg::BlendFunc* blendFunc = static_cast(stateset->getAttribute(osg::StateAttribute::BLENDFUNC)); - osg::ref_ptr newBlendFunc = blendFunc ? new osg::BlendFunc(*blendFunc) : new osg::BlendFunc; - newBlendFunc->setDestinationAlpha(osg::BlendFunc::ONE); - newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON); - node.setStateSet(newStateSet); - } + if (blendFunc) + { + newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); + node.setStateSet(newStateSet); + osg::ref_ptr newBlendFunc = new osg::BlendFunc(*blendFunc); + newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON); + // I *think* (based on some by-hand maths) that the RGB and dest alpha factors are unchanged, and only dest determines source alpha factor + // This has the benefit of being idempotent if we assume nothing used glBlendFuncSeparate before we touched it + if (blendFunc->getDestination() == osg::BlendFunc::ONE_MINUS_SRC_ALPHA) + newBlendFunc->setSourceAlpha(osg::BlendFunc::ONE); + else if (blendFunc->getDestination() == osg::BlendFunc::ONE) + newBlendFunc->setSourceAlpha(osg::BlendFunc::ZERO); + // Other setups barely exist in the wild and aren't worth supporting as they're not equippable gear + else + Log(Debug::Info) << "Unable to adjust blend mode for character preview. Source factor 0x" << std::hex << blendFunc->getSource() << ", destination factor 0x" << blendFunc->getDestination() << std::dec; + } + } + if (stateset->getMode(GL_BLEND) & osg::StateAttribute::ON) + { + if (!newStateSet) + { + newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); + node.setStateSet(newStateSet); + } + // Disable noBlendAlphaEnv + newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); + newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON); + } } traverse(node); } }; + class CharacterPreviewRTTNode : public SceneUtil::RTTNode + { + static constexpr float fovYDegrees = 12.3f; + static constexpr float znear = 4.0f; + static constexpr float zfar = 10000.f; + + public: + CharacterPreviewRTTNode(uint32_t sizeX, uint32_t sizeY) + : RTTNode(sizeX, sizeY, Settings::Manager::getInt("antialiasing", "Video"), false, 0, StereoAwareness::Unaware_MultiViewShaders) + , mAspectRatio(static_cast(sizeX) / static_cast(sizeY)) + { + if (SceneUtil::AutoDepth::isReversed()) + mPerspectiveMatrix = static_cast(SceneUtil::getReversedZProjectionMatrixAsPerspective(fovYDegrees, mAspectRatio, znear, zfar)); + else + mPerspectiveMatrix = osg::Matrixf::perspective(fovYDegrees, mAspectRatio, znear, zfar); + mGroup->getOrCreateStateSet()->addUniform(new osg::Uniform("projectionMatrix", mPerspectiveMatrix)); + mViewMatrix = osg::Matrixf::identity(); + setColorBufferInternalFormat(GL_RGBA); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + } + + void setDefaults(osg::Camera* camera) override + { + camera->setName("CharacterPreview"); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); + camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f)); + camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + camera->setProjectionMatrixAsPerspective(fovYDegrees, mAspectRatio, znear, zfar); + camera->setViewport(0, 0, width(), height()); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + camera->setCullMask(~(Mask_UpdateVisitor)); + camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); + SceneUtil::setCameraClearDepth(camera); + + camera->setNodeMask(Mask_RenderToTexture); + camera->addChild(mGroup); + }; + + void apply(osg::Camera* camera) override + { + if(mCameraStateset) + camera->setStateSet(mCameraStateset); + camera->setViewMatrix(mViewMatrix); + + if (shouldDoTextureArray()) + Stereo::setMultiviewMatrices(mGroup->getOrCreateStateSet(), { mPerspectiveMatrix, mPerspectiveMatrix }); + }; + + void addChild(osg::Node* node) + { + mGroup->addChild(node); + } + + void setCameraStateset(osg::StateSet* stateset) + { + mCameraStateset = stateset; + } + + void setViewMatrix(const osg::Matrixf& viewMatrix) + { + mViewMatrix = viewMatrix; + } + + osg::ref_ptr mGroup = new osg::Group; + osg::Matrixf mPerspectiveMatrix; + osg::Matrixf mViewMatrix; + osg::ref_ptr mCameraStateset; + float mAspectRatio; + }; + CharacterPreview::CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character, int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt) : mParent(parent) @@ -119,32 +227,18 @@ namespace MWRender , mSizeX(sizeX) , mSizeY(sizeY) { - mTexture = new osg::Texture2D; - mTexture->setTextureSize(sizeX, sizeY); - mTexture->setInternalFormat(GL_RGBA); - mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - mCamera = new osg::Camera; - // hints that the camera is not relative to the master camera - mCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - mCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); - mCamera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f)); - mCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - const float fovYDegrees = 12.3f; - mCamera->setProjectionMatrixAsPerspective(fovYDegrees, sizeX/static_cast(sizeY), 0.1f, 10000.f); // zNear and zFar are autocomputed - mCamera->setViewport(0, 0, sizeX, sizeY); - mCamera->setRenderOrder(osg::Camera::PRE_RENDER); - mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture); - mCamera->setName("CharacterPreview"); - mCamera->setComputeNearFarMode(osg::Camera::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES); - mCamera->setCullMask(~(Mask_UpdateVisitor)); - - mCamera->setNodeMask(Mask_RenderToTexture); - - osg::ref_ptr lightManager = new SceneUtil::LightManager; + mTextureStateSet = new osg::StateSet; + mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); + + 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); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); + stateset->setDefine("FORCE_OPAQUE", "1", osg::StateAttribute::ON); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); stateset->setMode(GL_NORMALIZE, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::ON); @@ -164,6 +258,31 @@ namespace MWRender fog->setEnd(10000000); stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + // TODO: Clean up this mess of loose uniforms that shaders depend on. + // turn off sky blending + stateset->addUniform(new osg::Uniform("far", 10000000.0f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); + stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{1, 1})); + + stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); + + // Opaque stuff must have 1 as its fragment alpha as the FBO is translucent, so having blending off isn't enough + osg::ref_ptr noBlendAlphaEnv = new osg::TexEnvCombine(); + noBlendAlphaEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + noBlendAlphaEnv->setSource0_Alpha(osg::TexEnvCombine::CONSTANT); + noBlendAlphaEnv->setConstantColor(osg::Vec4(0.0, 0.0, 0.0, 1.0)); + noBlendAlphaEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + noBlendAlphaEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + osg::ref_ptr dummyTexture = new osg::Texture2D(); + dummyTexture->setInternalFormat(GL_DEPTH_COMPONENT); + dummyTexture->setTextureSize(1, 1); + // This might clash with a shadow map, so make sure it doesn't cast shadows + dummyTexture->setShadowComparison(true); + dummyTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); + stateset->setTextureAttributeAndModes(7, dummyTexture, osg::StateAttribute::ON); + stateset->setTextureAttribute(7, noBlendAlphaEnv, osg::StateAttribute::ON); + osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.0, 0.0, 0.0, 1.0)); stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); @@ -182,12 +301,22 @@ namespace MWRender float positionZ = std::cos(altitude); light->setPosition(osg::Vec4(positionX,positionY,positionZ, 0.0)); light->setDiffuse(osg::Vec4(diffuseR,diffuseG,diffuseB,1)); - light->setAmbient(osg::Vec4(ambientR,ambientG,ambientB,1)); + osg::Vec4 ambientRGBA = osg::Vec4(ambientR,ambientG,ambientB,1); + if (mResourceSystem->getSceneManager()->getForceShaders()) + { + // When using shaders, we now skip the ambient sun calculation as this is the only place it's used. + // Using the scene ambient will give identical results. + lightmodel->setAmbientIntensity(ambientRGBA); + light->setAmbient(osg::Vec4(0,0,0,1)); + } + else + light->setAmbient(ambientRGBA); light->setSpecular(osg::Vec4(0,0,0,0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); + lightManager->setSunlight(light); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); @@ -196,23 +325,22 @@ namespace MWRender lightManager->addChild(lightSource); - mCamera->addChild(lightManager); + mRTTNode->addChild(lightManager); mNode = new osg::PositionAttitudeTransform; lightManager->addChild(mNode); - mDrawOnceCallback = new DrawOnceCallback; - mCamera->addUpdateCallback(mDrawOnceCallback); + mDrawOnceCallback = new DrawOnceCallback(mRTTNode->mGroup); + mRTTNode->addUpdateCallback(mDrawOnceCallback); - mParent->addChild(mCamera); + mParent->addChild(mRTTNode); mCharacter.mCell = nullptr; } CharacterPreview::~CharacterPreview () { - mCamera->removeChildren(0, mCamera->getNumChildren()); - mParent->removeChild(mCamera); + mParent->removeChild(mRTTNode); } int CharacterPreview::getTextureWidth() const @@ -238,7 +366,7 @@ namespace MWRender osg::ref_ptr CharacterPreview::getTexture() { - return mTexture; + return static_cast(mRTTNode->getColorTexture(nullptr)); } void CharacterPreview::rebuild() @@ -255,7 +383,7 @@ namespace MWRender void CharacterPreview::redraw() { - mCamera->setNodeMask(Mask_RenderToTexture); + mRTTNode->setNodeMask(Mask_RenderToTexture); mDrawOnceCallback->redrawNextFrame(); } @@ -276,7 +404,7 @@ namespace MWRender osg::ref_ptr stateset = new osg::StateSet; mViewport = new osg::Viewport(0, mSizeY-sizeY, std::min(mSizeX, sizeX), std::min(mSizeY, sizeY)); stateset->setAttributeAndModes(mViewport); - mCamera->setStateSet(stateset); + mRTTNode->setCameraStateset(stateset); redraw(); } @@ -296,7 +424,7 @@ namespace MWRender if(iter != inv.end()) { groupname = "inventoryweapononehand"; - if(iter->getTypeName() == typeid(ESM::Weapon).name()) + if(iter->getType() == ESM::Weapon::sRecordId) { MWWorld::LiveCellRef *ref = iter->get(); int type = ref->mBase->mData.mType; @@ -329,7 +457,7 @@ namespace MWRender mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && showCarriedLeft) + if(torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { if(!mAnimation->getInfo("torch")) mAnimation->play("torch", 2, Animation::BlendMask_LeftArm, false, @@ -363,10 +491,11 @@ namespace MWRender // Set the traversal number from the last draw, so that the frame switch used for RigGeometry double buffering works correctly visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); - osg::Node::NodeMask nodeMask = mCamera->getNodeMask(); - mCamera->setNodeMask(~0); - mCamera->accept(visitor); - mCamera->setNodeMask(nodeMask); + auto* camera = mRTTNode->getCamera(nullptr); + osg::Node::NodeMask nodeMask = camera->getNodeMask(); + camera->setNodeMask(~0u); + camera->accept(visitor); + camera->setNodeMask(nodeMask); if (intersector->containsIntersections()) { @@ -389,7 +518,8 @@ namespace MWRender mNode->setScale(scale); - mCamera->setViewMatrixAsLookAt(mPosition * scale.z(), mLookAt * scale.z(), osg::Vec3f(0,0,1)); + auto viewMatrix = osg::Matrixf::lookAt(mPosition * scale.z(), mLookAt * scale.z(), osg::Vec3f(0, 0, 1)); + mRTTNode->setViewMatrix(viewMatrix); } // -------------------------------------------------------------------------------------------------- @@ -422,7 +552,7 @@ namespace MWRender rebuild(); } - class UpdateCameraCallback : public osg::NodeCallback + class UpdateCameraCallback : public SceneUtil::NodeCallback { public: UpdateCameraCallback(osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) @@ -432,10 +562,8 @@ namespace MWRender { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(CharacterPreviewRTTNode* node, osg::NodeVisitor* nv) { - osg::Camera* cam = static_cast(node); - // Update keyframe controllers in the scene graph first... traverse(node, nv); @@ -446,7 +574,8 @@ namespace MWRender osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3 headOffset = worldMat.getTrans(); - cam->setViewMatrixAsLookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0,0,1)); + auto viewMatrix = osg::Matrixf::lookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0, 0, 1)); + node->setViewMatrix(viewMatrix); } private: @@ -463,13 +592,13 @@ namespace MWRender // attach camera to follow the head node if (mUpdateCameraCallback) - mCamera->removeUpdateCallback(mUpdateCameraCallback); + mRTTNode->removeUpdateCallback(mUpdateCameraCallback); const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (head) { mUpdateCameraCallback = new UpdateCameraCallback(head, mPosition, mLookAt); - mCamera->addUpdateCallback(mUpdateCameraCallback); + mRTTNode->addUpdateCallback(mUpdateCameraCallback); } else Log(Debug::Error) << "Error: Bip01 Head node not found"; diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 3eb9688465..a8777d8548 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -6,7 +6,7 @@ #include -#include +#include #include @@ -18,6 +18,7 @@ namespace osg class Camera; class Group; class Viewport; + class StateSet; } namespace MWRender @@ -25,6 +26,7 @@ namespace MWRender class NpcAnimation; class DrawOnceCallback; + class CharacterPreviewRTTNode; class CharacterPreview { @@ -41,6 +43,8 @@ namespace MWRender void rebuild(); osg::ref_ptr getTexture(); + /// Get the osg::StateSet required to render the texture correctly, if any. + osg::StateSet* getTextureStateSet() { return mTextureStateSet; } private: CharacterPreview(const CharacterPreview&); @@ -53,9 +57,9 @@ namespace MWRender osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mTexture; - osg::ref_ptr mCamera; + osg::ref_ptr mTextureStateSet; osg::ref_ptr mDrawOnceCallback; + osg::ref_ptr mRTTNode; osg::Vec3f mPosition; osg::Vec3f mLookAt; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index f1df6c90fc..174ea036e1 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include @@ -10,7 +10,7 @@ #include #include #include - +#include #include #include "../mwbase/environment.hpp" @@ -35,7 +35,7 @@ CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr, setObjectRoot(model, false, false, true); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) - addAnimSource("meshes\\xbase_anim.nif", model); + addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); addAnimSource(model, model); } } @@ -54,7 +54,7 @@ CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const if((ref->mBase->mFlags&ESM::Creature::Bipedal)) { - addAnimSource("meshes\\xbase_anim.nif", model); + addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); } addAnimSource(model, model); @@ -63,7 +63,7 @@ CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const updateParts(); } - mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); + mWeaponAnimationTime = std::make_shared(this); } void CreatureWeaponAnimation::showWeapons(bool showWeapon) @@ -119,14 +119,14 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) std::string itemModel = item.getClass().getModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { - if(item.getTypeName() == typeid(ESM::Weapon).name()) + if(item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; bonename = MWMechanics::getWeaponType(type)->mAttachBone; if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) bonename = "Weapon Bone"; } @@ -137,36 +137,17 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) else { bonename = "Shield Bone"; - if (item.getTypeName() == typeid(ESM::Armor).name()) + if (item.getType() == ESM::Armor::sRecordId) { - // Shield body part model should be used if possible. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const auto& part : item.get()->mBase->mParts.mParts) - { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = store.get().search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) - { - itemModel = "meshes\\" + bodypart->mModel; - break; - } - } + itemModel = getShieldMesh(item, false); } } try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(itemModel); + osg::ref_ptr attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); - - scene.reset(new PartHolder(attached)); + scene = std::make_unique(attached); if (!item.getClass().getEnchantment(item).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, item.getClass().getEnchantmentColor(item)); @@ -174,7 +155,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) // Crossbows start out with a bolt attached // FIXME: code duplicated from NpcAnimation if (slot == MWWorld::InventoryStore::Slot_CarriedRight && - item.getTypeName() == typeid(ESM::Weapon).name() && + item.getType() == ESM::Weapon::sRecordId && item.get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow); @@ -192,7 +173,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) if (slot == MWWorld::InventoryStore::Slot_CarriedRight) source = mWeaponAnimationTime; else - source.reset(new NullAnimationTime); + source = std::make_shared(); SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); attached->accept(assignVisitor); @@ -246,11 +227,13 @@ osg::Group *CreatureWeaponAnimation::getArrowBone() const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return nullptr; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + if (ammoType == ESM::Weapon::None) + return nullptr; // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 3e785a769e..371f488c3d 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -11,6 +11,8 @@ #include "vismask.hpp" #include "util.hpp" +#include + namespace MWRender { @@ -32,7 +34,7 @@ void EffectManager::addEffect(const std::string &model, const std::string& textu node->setNodeMask(Mask_Effect); Effect effect; - effect.mAnimTime.reset(new EffectAnimationTime); + effect.mAnimTime = std::make_shared(); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); @@ -43,6 +45,8 @@ void EffectManager::addEffect(const std::string &model, const std::string& textu trans->setScale(osg::Vec3f(scale, scale, scale)); trans->addChild(node); + effect.mTransform = trans; + SceneUtil::AssignControllerSourcesVisitor assignVisitor(effect.mAnimTime); node->accept(assignVisitor); @@ -53,30 +57,32 @@ void EffectManager::addEffect(const std::string &model, const std::string& textu mParentNode->addChild(trans); - mEffects[trans] = effect; + mEffects.push_back(std::move(effect)); } void EffectManager::update(float dt) { - for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ) - { - it->second.mAnimTime->addTime(dt); - - if (it->second.mAnimTime->getTime() >= it->second.mMaxControllerLength) - { - mParentNode->removeChild(it->first); - mEffects.erase(it++); - } - else - ++it; - } + mEffects.erase( + std::remove_if( + mEffects.begin(), + mEffects.end(), + [dt, this](Effect& effect) + { + effect.mAnimTime->addTime(dt); + const auto remove = effect.mAnimTime->getTime() >= effect.mMaxControllerLength; + if (remove) + mParentNode->removeChild(effect.mTransform); + return remove; + }), + mEffects.end() + ); } void EffectManager::clear() { - for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ++it) + for(const auto& effect : mEffects) { - mParentNode->removeChild(it->first); + mParentNode->removeChild(effect.mTransform); } mEffects.clear(); } diff --git a/apps/openmw/mwrender/effectmanager.hpp b/apps/openmw/mwrender/effectmanager.hpp index 5873c00dd8..0d94a63c8f 100644 --- a/apps/openmw/mwrender/effectmanager.hpp +++ b/apps/openmw/mwrender/effectmanager.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_MWRENDER_EFFECTMANAGER_H #define OPENMW_MWRENDER_EFFECTMANAGER_H -#include #include #include +#include #include @@ -29,6 +29,7 @@ namespace MWRender { public: EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem); + EffectManager(const EffectManager&) = delete; ~EffectManager(); /// Add an effect. When it's finished playing, it will be removed automatically. @@ -44,16 +45,13 @@ namespace MWRender { float mMaxControllerLength; std::shared_ptr mAnimTime; + osg::ref_ptr mTransform; }; - typedef std::map, Effect> EffectMap; - EffectMap mEffects; + std::vector mEffects; osg::ref_ptr mParentNode; Resource::ResourceSystem* mResourceSystem; - - EffectManager(const EffectManager&); - void operator=(const EffectManager&); }; } diff --git a/apps/openmw/mwrender/fogmanager.cpp b/apps/openmw/mwrender/fogmanager.cpp index 837e6ad041..b68b846851 100644 --- a/apps/openmw/mwrender/fogmanager.cpp +++ b/apps/openmw/mwrender/fogmanager.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include @@ -76,8 +76,8 @@ namespace MWRender mLandFogStart = viewDistance * (1 - fogDepth); mLandFogEnd = viewDistance; } - mUnderwaterFogStart = std::min(viewDistance, 6666.f) * (1 - underwaterFog); - mUnderwaterFogEnd = std::min(viewDistance, 6666.f); + mUnderwaterFogStart = std::min(viewDistance, 7168.f) * (1 - underwaterFog); + mUnderwaterFogEnd = std::min(viewDistance, 7168.f); } mFogColor = color; } diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index ba300accbc..dca26a220f 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -4,20 +4,20 @@ #include #include #include -#include #include #include -#include #include #include #include #include +#include +#include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -25,6 +25,7 @@ #include "../mwworld/esmstore.hpp" #include "vismask.hpp" +#include "util.hpp" namespace { @@ -61,7 +62,7 @@ namespace } - class CameraUpdateGlobalCallback : public osg::NodeCallback + class CameraUpdateGlobalCallback : public SceneUtil::NodeCallback { public: CameraUpdateGlobalCallback(MWRender::GlobalMap* parent) @@ -70,14 +71,14 @@ namespace { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* node, osg::NodeVisitor* nv) { if (mRendered) { - if (mParent->copyResult(static_cast(node), nv->getTraversalNumber())) + if (mParent->copyResult(node, nv->getTraversalNumber())) { node->setNodeMask(0); - mParent->markForRemoval(static_cast(node)); + mParent->markForRemoval(node); } return; } @@ -92,6 +93,26 @@ namespace MWRender::GlobalMap* mParent; }; + std::vector writePng(const osg::Image& overlayImage) + { + std::ostringstream ostream; + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Can't write map overlay: no png readerwriter found"; + return std::vector(); + } + + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(overlayImage, ostream); + if (!result.success()) + { + Log(Debug::Warning) << "Error: Can't write map overlay: " << result.message() << " code " << result.status(); + return std::vector(); + } + + std::string data = ostream.str(); + return std::vector(data.begin(), data.end()); + } } namespace MWRender @@ -220,6 +241,20 @@ namespace MWRender osg::ref_ptr mOverlayTexture; }; + struct GlobalMap::WritePng final : public SceneUtil::WorkItem + { + osg::ref_ptr mOverlayImage; + std::vector mImageData; + + explicit WritePng(osg::ref_ptr overlayImage) + : mOverlayImage(std::move(overlayImage)) {} + + void doWork() override + { + mImageData = writePng(*mOverlayImage); + } + }; + GlobalMap::GlobalMap(osg::Group* root, SceneUtil::WorkQueue* workQueue) : mRoot(root) , mWorkQueue(workQueue) @@ -271,17 +306,9 @@ namespace MWRender void GlobalMap::worldPosToImageSpace(float x, float z, float& imageX, float& imageY) { - imageX = float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1); - - imageY = 1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1); - } - - void GlobalMap::cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY) - { - imageX = float(x - mMinX) / (mMaxX - mMinX + 1); + imageX = (float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1)) * getWidth(); - // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - imageY = 1.f-float(y - mMinY + 1) / (mMaxY - mMinY + 1); + imageY = (1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1)) * getHeight(); } void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, @@ -332,8 +359,8 @@ namespace MWRender if (texture) { osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); - osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask(0); + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); @@ -407,23 +434,15 @@ namespace MWRender map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; - std::ostringstream ostream; - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (!readerwriter) + if (mWritePng != nullptr) { - Log(Debug::Error) << "Error: Can't write map overlay: no png readerwriter found"; + mWritePng->waitTillDone(); + map.mImageData = std::move(mWritePng->mImageData); + mWritePng = nullptr; return; } - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mOverlayImage, ostream); - if (!result.success()) - { - Log(Debug::Warning) << "Error: Can't write map overlay: " << result.message() << " code " << result.status(); - return; - } - - std::string data = ostream.str(); - map.mImageData = std::vector(data.begin(), data.end()); + map.mImageData = writePng(*mOverlayImage); } struct Box @@ -434,7 +453,7 @@ namespace MWRender : mLeft(left), mTop(top), mRight(right), mBottom(bottom) { } - bool operator == (const Box& other) + bool operator == (const Box& other) const { return mLeft == other.mLeft && mTop == other.mTop && mRight == other.mRight && mBottom == other.mBottom; } @@ -583,7 +602,7 @@ namespace MWRender } mOverlayImage->copySubImage(imageDest.mX, imageDest.mY, 0, imageDest.mImage); - it = mPendingImageDest.erase(it); + mPendingImageDest.erase(it); return true; } } @@ -613,4 +632,13 @@ namespace MWRender cam->removeChildren(0, cam->getNumChildren()); mRoot->removeChild(cam); } + + void GlobalMap::asyncWritePng() + { + if (mOverlayImage == nullptr) + return; + // Use deep copy to avoid any sychronization + mWritePng = new WritePng(new osg::Image(*mOverlayImage, osg::CopyOp::DEEP_COPY_ALL)); + mWorkQueue->addWorkItem(mWritePng, /*front=*/true); + } } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index b359c852be..28531f14df 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -41,12 +41,8 @@ namespace MWRender int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } - int getCellSize() const { return mCellSize; } - void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); - void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); - void exploreCell (int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay @@ -76,7 +72,11 @@ namespace MWRender void ensureLoaded(); + void asyncWritePng(); + private: + struct WritePng; + /** * Request rendering a 2d quad onto mOverlayTexture. * x, y, width and height are the destination coordinates (top-left coordinate origin) @@ -125,6 +125,7 @@ namespace MWRender osg::ref_ptr mWorkQueue; osg::ref_ptr mWorkItem; + osg::ref_ptr mWritePng; int mWidth; int mHeight; diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp new file mode 100644 index 0000000000..194227f758 --- /dev/null +++ b/apps/openmw/mwrender/groundcover.cpp @@ -0,0 +1,263 @@ +#include "groundcover.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwworld/groundcoverstore.hpp" + +#include "vismask.hpp" + +namespace MWRender +{ + class InstancingVisitor : public osg::NodeVisitor + { + public: + InstancingVisitor(std::vector& instances, osg::Vec3f& chunkPosition) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mInstances(instances) + , mChunkPosition(chunkPosition) + { + } + + void apply(osg::Geometry& geom) override + { + for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) + { + geom.getPrimitiveSet(i)->setNumInstances(mInstances.size()); + } + + osg::ref_ptr transforms = new osg::Vec4Array(mInstances.size()); + osg::BoundingBox box; + float radius = geom.getBoundingBox().radius(); + for (unsigned int i = 0; i < transforms->getNumElements(); i++) + { + osg::Vec3f pos(mInstances[i].mPos.asVec3()); + osg::Vec3f relativePos = pos - mChunkPosition; + (*transforms)[i] = osg::Vec4f(relativePos, mInstances[i].mScale); + + // Use an additional margin due to groundcover animation + float instanceRadius = radius * mInstances[i].mScale * 1.1f; + osg::BoundingSphere instanceBounds(relativePos, instanceRadius); + box.expandBy(instanceBounds); + } + + geom.setInitialBound(box); + + osg::ref_ptr rotations = new osg::Vec3Array(mInstances.size()); + for (unsigned int i = 0; i < rotations->getNumElements(); i++) + { + (*rotations)[i] = mInstances[i].mPos.asRotationVec3(); + } + + // Display lists do not support instancing in OSG 3.4 + geom.setUseDisplayList(false); + geom.setUseVertexBufferObjects(true); + + geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); + geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); + } + private: + std::vector mInstances; + osg::Vec3f mChunkPosition; + }; + + class DensityCalculator + { + public: + DensityCalculator(float density) + : mDensity(density) + { + } + + bool isInstanceEnabled() + { + if (mDensity >= 1.f) return true; + + mCurrentGroundcover += mDensity; + if (mCurrentGroundcover < 1.f) return false; + + mCurrentGroundcover -= 1.f; + + return true; + } + void reset() { mCurrentGroundcover = 0.f; } + + private: + float mCurrentGroundcover = 0.f; + float mDensity = 0.f; + }; + + class ViewDistanceCallback : public SceneUtil::NodeCallback + { + public: + ViewDistanceCallback(float dist, const osg::BoundingBox& box) : mViewDistance(dist), mBox(box) {} + void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (Terrain::distance(mBox, nv->getEyePoint()) <= mViewDistance) + traverse(node, nv); + } + private: + float mViewDistance; + osg::BoundingBox mBox; + }; + + inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound) + { + osg::Vec2f size = maxBound - minBound; + if (size.x() >=1 && size.y() >=1) return true; + + osg::Vec3f pos = ref.mPos.asVec3(); + osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; + 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())) + return false; + + return true; + } + + osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + { + if (lod > getMaxLodLevel()) + return nullptr; + GroundcoverChunkId id = std::make_tuple(center, size); + osg::ref_ptr obj = mCache->getRefFromObjectCache(id); + if (obj) + return static_cast(obj.get()); + else + { + InstanceMap instances; + collectInstances(instances, size, center); + osg::ref_ptr node = createChunk(instances, center); + mCache->addEntryToObjectCache(id, node.get()); + return node; + } + } + + Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store) + : GenericResourceManager(nullptr) + , mSceneManager(sceneManager) + , mDensity(density) + , mStateset(new osg::StateSet) + , mGroundcoverStore(store) + { + setViewDistance(viewDistance); + // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties + // Force a unified alpha handling instead of data from meshes + osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); + mStateset->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + mStateset->setAttributeAndModes(new osg::BlendFunc, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + mStateset->setRenderBinDetails(0, "RenderBin", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1)); + mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1)); + + mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? Shader::ShaderManager::cloneProgram(mSceneManager->getShaderManager().getProgramTemplate()) : osg::ref_ptr(new osg::Program); + mProgramTemplate->addBindAttribLocation("aOffset", 6); + mProgramTemplate->addBindAttribLocation("aRotation", 7); + } + + Groundcover::~Groundcover() + { + } + + void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) + { + if (mDensity <=0.f) return; + + osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f)); + osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f)); + DensityCalculator calculator(mDensity); + ESM::ReadersCache readers; + osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f)); + for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) + { + for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) + { + ESM::Cell cell; + mGroundcoverStore.initCell(cell, cellX, cellY); + if (cell.mContextList.empty()) continue; + + calculator.reset(); + std::map refs; + for (size_t i=0; i(cell.mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = readers.get(index); + cell.restore(*reader, i); + ESM::CellRef ref; + ref.mRefNum.unset(); + bool deleted = false; + while (cell.getNextRef(*reader, ref, deleted)) + { + if (!deleted && refs.find(ref.mRefNum) == refs.end() && !calculator.isInstanceEnabled()) deleted = true; + if (!deleted && !isInChunkBorders(ref, minBound, maxBound)) deleted = true; + + if (deleted) { refs.erase(ref.mRefNum); continue; } + refs[ref.mRefNum] = std::move(ref); + } + } + + for (auto& pair : refs) + { + ESM::CellRef& ref = pair.second; + const std::string& model = mGroundcoverStore.getGroundcoverModel(ref.mRefID); + if (!model.empty()) + instances[model].emplace_back(std::move(ref)); + } + } + } + } + + osg::ref_ptr Groundcover::createChunk(InstanceMap& instances, const osg::Vec2f& center) + { + 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) + { + const osg::Node* temp = mSceneManager->getTemplate(pair.first); + 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)); + + // Keep link to original mesh to keep it in cache + group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); + + InstancingVisitor visitor(pair.second, worldCenter); + node->accept(visitor); + group->addChild(node); + } + + osg::ComputeBoundsVisitor cbv; + group->accept(cbv); + osg::BoundingBox box = cbv.getBoundingBox(); + group->addCullCallback(new ViewDistanceCallback(getViewDistance(), box)); + + group->setStateSet(mStateset); + group->setNodeMask(Mask_Groundcover); + if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) + group->addCullCallback(new SceneUtil::LightListCallback); + mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate); + mSceneManager->shareState(group); + group->getBound(); + return group; + } + + unsigned int Groundcover::getNodeMask() + { + return Mask_Groundcover; + } + + void Groundcover::reportStats(unsigned int frameNumber, osg::Stats *stats) const + { + stats->setAttribute(frameNumber, "Groundcover Chunk", mCache->getCacheSize()); + } +} diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp new file mode 100644 index 0000000000..d6d3ac52a7 --- /dev/null +++ b/apps/openmw/mwrender/groundcover.hpp @@ -0,0 +1,55 @@ +#ifndef OPENMW_MWRENDER_GROUNDCOVER_H +#define OPENMW_MWRENDER_GROUNDCOVER_H + +#include +#include +#include + +namespace MWWorld +{ + class ESMStore; + class GroundcoverStore; +} +namespace osg +{ + class Program; +} + +namespace MWRender +{ + typedef std::tuple GroundcoverChunkId; // Center, Size + class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager + { + public: + Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store); + ~Groundcover(); + + osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; + + unsigned int getNodeMask() override; + + void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; + + struct GroundcoverEntry + { + ESM::Position mPos; + float mScale; + + GroundcoverEntry(const ESM::CellRef& ref) : mPos(ref.mPos), mScale(ref.mScale) + {} + }; + + private: + 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); + }; +} + +#endif diff --git a/apps/openmw/mwrender/hdr.cpp b/apps/openmw/mwrender/hdr.cpp new file mode 100644 index 0000000000..95015da83a --- /dev/null +++ b/apps/openmw/mwrender/hdr.cpp @@ -0,0 +1,123 @@ +#include "hdr.hpp" + +#include +#include + +#include "pingpongcanvas.hpp" + +namespace MWRender +{ + HDRDriver::HDRDriver(Shader::ShaderManager& shaderManager) + : mCompiled(false) + , mEnabled(false) + , mWidth(1) + , mHeight(1) + { + const float hdrExposureTime = std::clamp(Settings::Manager::getFloat("hdr exposure time", "Post Processing"), 0.f, 1.f); + + constexpr float minLog = -9.0; + constexpr float maxLog = 4.0; + constexpr float logLumRange = (maxLog - minLog); + constexpr float invLogLumRange = 1.0 / logLumRange; + constexpr float epsilon = 0.004; + + Shader::ShaderManager::DefineMap defines = { + {"minLog", std::to_string(minLog)}, + {"maxLog", std::to_string(maxLog)}, + {"logLumRange", std::to_string(logLumRange)}, + {"invLogLumRange", std::to_string(invLogLumRange)}, + {"hdrExposureTime", std::to_string(hdrExposureTime)}, + {"epsilon", std::to_string(epsilon)}, + }; + + auto vertex = shaderManager.getShader("fullscreen_tri_vertex.glsl", {}, osg::Shader::VERTEX); + auto hdrLuminance = shaderManager.getShader("hdr_luminance_fragment.glsl", defines, osg::Shader::FRAGMENT); + auto hdr = shaderManager.getShader("hdr_fragment.glsl", defines, osg::Shader::FRAGMENT); + + mProgram = shaderManager.getProgram(vertex, hdr); + mLuminanceProgram = shaderManager.getProgram(vertex, hdrLuminance); + } + + void HDRDriver::compile() + { + int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + + for (auto& buffer : mBuffers) + { + buffer.texture = new osg::Texture2D; + buffer.texture->setInternalFormat(GL_R16F); + buffer.texture->setSourceFormat(GL_RED); + buffer.texture->setSourceType(GL_FLOAT); + buffer.texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST); + buffer.texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR); + buffer.texture->setTextureSize(mWidth, mHeight); + buffer.texture->setNumMipmapLevels(mipmapLevels); + + buffer.finalTexture = new osg::Texture2D; + buffer.finalTexture->setInternalFormat(GL_R16F); + buffer.finalTexture->setSourceFormat(GL_RED); + buffer.finalTexture->setSourceType(GL_FLOAT); + buffer.finalTexture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST); + buffer.finalTexture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::NEAREST); + buffer.finalTexture->setTextureSize(1, 1); + + buffer.finalFbo = new osg::FrameBufferObject; + buffer.finalFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.finalTexture)); + + buffer.fullscreenFbo = new osg::FrameBufferObject; + buffer.fullscreenFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.texture)); + + buffer.mipmapFbo = new osg::FrameBufferObject; + buffer.mipmapFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.texture, mipmapLevels - 1)); + + buffer.fullscreenStateset = new osg::StateSet; + buffer.fullscreenStateset->setAttributeAndModes(mLuminanceProgram); + buffer.fullscreenStateset->addUniform(new osg::Uniform("sceneTex", 0)); + + buffer.mipmapStateset = new osg::StateSet; + buffer.mipmapStateset->setAttributeAndModes(mProgram); + buffer.mipmapStateset->setTextureAttributeAndModes(0, buffer.texture); + buffer.mipmapStateset->addUniform(new osg::Uniform("luminanceSceneTex", 0)); + buffer.mipmapStateset->addUniform(new osg::Uniform("prevLuminanceSceneTex", 1)); + } + + mBuffers[0].mipmapStateset->setTextureAttributeAndModes(1, mBuffers[1].finalTexture); + mBuffers[1].mipmapStateset->setTextureAttributeAndModes(1, mBuffers[0].finalTexture); + + mCompiled = true; + } + + void HDRDriver::draw(const PingPongCanvas& canvas, osg::RenderInfo& renderInfo, osg::State& state, osg::GLExtensions* ext, size_t frameId) + { + if (!mEnabled) + return; + + if (!mCompiled) + compile(); + + auto& hdrBuffer = mBuffers[frameId]; + hdrBuffer.fullscreenFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + hdrBuffer.fullscreenStateset->setTextureAttributeAndModes(0, canvas.getSceneTexture(frameId)); + + state.apply(hdrBuffer.fullscreenStateset); + canvas.drawGeometry(renderInfo); + + state.applyTextureAttribute(0, hdrBuffer.texture); + ext->glGenerateMipmap(GL_TEXTURE_2D); + + hdrBuffer.mipmapFbo->apply(state, osg::FrameBufferObject::READ_FRAMEBUFFER); + hdrBuffer.finalFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_LINEAR); + + state.apply(hdrBuffer.mipmapStateset); + canvas.drawGeometry(renderInfo); + + ext->glBindFramebuffer(GL_FRAMEBUFFER_EXT, state.getGraphicsContext() ? state.getGraphicsContext()->getDefaultFboId() : 0); + } + + osg::ref_ptr HDRDriver::getLuminanceTexture(size_t frameId) const + { + return mBuffers[frameId].finalTexture; + } +} \ No newline at end of file diff --git a/apps/openmw/mwrender/hdr.hpp b/apps/openmw/mwrender/hdr.hpp new file mode 100644 index 0000000000..95bdc6aa0a --- /dev/null +++ b/apps/openmw/mwrender/hdr.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_MWRENDER_HDR_H +#define OPENMW_MWRENDER_HDR_H + +#include + +#include +#include +#include + +namespace Shader +{ + class ShaderManager; +} + +namespace MWRender +{ + class PingPongCanvas; + + class HDRDriver + { + + public: + + HDRDriver() = default; + + HDRDriver(Shader::ShaderManager& shaderManager); + + void draw(const PingPongCanvas& canvas, osg::RenderInfo& renderInfo, osg::State& state, osg::GLExtensions* ext, size_t frameId); + + bool isEnabled() const { return mEnabled; } + + void enable() { mEnabled = true; } + void disable() { mEnabled = false; } + + void dirty(int w, int h) + { + mWidth = w; + mHeight = h; + mCompiled = false; + } + + osg::ref_ptr getLuminanceTexture(size_t frameId) const; + + private: + + void compile(); + + struct HDRContainer + { + osg::ref_ptr fullscreenFbo; + osg::ref_ptr mipmapFbo; + osg::ref_ptr finalFbo; + osg::ref_ptr texture; + osg::ref_ptr finalTexture; + osg::ref_ptr fullscreenStateset; + osg::ref_ptr mipmapStateset; + }; + + std::array mBuffers; + osg::ref_ptr mLuminanceProgram; + osg::ref_ptr mProgram; + + bool mCompiled; + bool mEnabled; + + int mWidth; + int mHeight; + }; +} + +#endif diff --git a/apps/openmw/mwrender/landmanager.cpp b/apps/openmw/mwrender/landmanager.cpp index 560c1ba720..2395eeab69 100644 --- a/apps/openmw/mwrender/landmanager.cpp +++ b/apps/openmw/mwrender/landmanager.cpp @@ -25,7 +25,10 @@ osg::ref_ptr LandManager::getLand(int x, int y) return static_cast(obj.get()); else { - const ESM::Land* land = MWBase::Environment::get().getWorld()->getStore().get().search(x,y); + const auto world = MWBase::Environment::get().getWorld(); + if (!world) + return nullptr; + const ESM::Land* land = world->getStore().get().search(x,y); if (!land) return nullptr; osg::ref_ptr landObj (new ESMTerrain::LandObject(land, mLoadFlags)); diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp index ea73f11c2b..e4b068eb76 100644 --- a/apps/openmw/mwrender/landmanager.hpp +++ b/apps/openmw/mwrender/landmanager.hpp @@ -1,11 +1,10 @@ -#ifndef OPENMW_COMPONENTS_ESMTERRAIN_LANDMANAGER_H -#define OPENMW_COMPONENTS_ESMTERRAIN_LANDMANAGER_H +#ifndef OPENMW_MWRENDER_LANDMANAGER_H +#define OPENMW_MWRENDER_LANDMANAGER_H #include -#include #include -#include +#include namespace ESM { diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 401e21ae46..63784d2dae 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -1,6 +1,6 @@ #include "localmap.hpp" -#include +#include #include #include @@ -12,15 +12,22 @@ #include #include -#include -#include +#include +#include #include +#include #include #include #include +#include +#include +#include +#include #include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -29,46 +36,46 @@ namespace { - - class CameraLocalUpdateCallback : public osg::NodeCallback - { - public: - CameraLocalUpdateCallback(MWRender::LocalMap* parent) - : mRendered(false) - , mParent(parent) - { - } - - void operator()(osg::Node* node, osg::NodeVisitor*) override - { - if (mRendered) - node->setNodeMask(0); - - if (!mRendered) - { - mRendered = true; - mParent->markForRemoval(static_cast(node)); - } - - // Note, we intentionally do not traverse children here. The map camera's scene data is the same as the master camera's, - // so it has been updated already. - //traverse(node, nv); - } - - private: - bool mRendered; - MWRender::LocalMap* mParent; - }; - float square(float val) { return val*val; } + std::pair divideIntoSegments(const osg::BoundingBox& bounds, float mapSize) + { + osg::Vec2f min(bounds.xMin(), bounds.yMin()); + osg::Vec2f max(bounds.xMax(), bounds.yMax()); + osg::Vec2f length = max - min; + const int segsX = static_cast(std::ceil(length.x() / mapSize)); + const int segsY = static_cast(std::ceil(length.y() / mapSize)); + return {segsX, segsY}; + } } namespace MWRender { + class LocalMapRenderToTexture: public SceneUtil::RTTNode + { + public: + LocalMapRenderToTexture(osg::Node* sceneRoot, int res, int mapWorldSize, + float x, float y, const osg::Vec3d& upVector, float zmin, float zmax); + + void setDefaults(osg::Camera* camera) override; + + bool isActive() { return mActive; } + void setIsActive(bool active) { mActive = active; } + + osg::Node* mSceneRoot; + osg::Matrix mProjectionMatrix; + osg::Matrix mViewMatrix; + bool mActive; + }; + + class CameraLocalUpdateCallback : public SceneUtil::NodeCallback + { + public: + void operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv); + }; LocalMap::LocalMap(osg::Group* root) : mRoot(root) @@ -79,9 +86,8 @@ LocalMap::LocalMap(osg::Group* root) , mInterior(false) { // Increase map resolution, if use UI scaling - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 1.0) - mMapResolution *= uiScale; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mMapResolution *= uiScale; SceneUtil::FindByNameVisitor find("Scene Root"); mRoot->accept(find); @@ -92,10 +98,8 @@ LocalMap::LocalMap(osg::Group* root) LocalMap::~LocalMap() { - for (auto& camera : mActiveCameras) - removeCamera(camera); - for (auto& camera : mCamerasPendingRemoval) - removeCamera(camera); + for (auto& rtt : mLocalMapRTTs) + mRoot->removeChild(rtt); } const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle) @@ -106,35 +110,31 @@ const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f void LocalMap::clear() { - mSegments.clear(); + mExteriorSegments.clear(); + mInteriorSegments.clear(); } void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) { if (!mInterior) { - const MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + const MapSegment& segment = mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (segment.mFogOfWarImage && segment.mHasFogState) { - std::unique_ptr fog (new ESM::FogState()); + auto fog = std::make_unique(); fog->mFogTextures.emplace_back(); segment.saveFogOfWar(fog->mFogTextures.back()); - cell->setFog(fog.release()); + cell->setFog(std::move(fog)); } } else { - // FIXME: segmenting code duplicated from requestMap - osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - osg::Vec2f max(mBounds.xMax(), mBounds.yMax()); - osg::Vec2f length = max-min; - const int segsX = static_cast(std::ceil(length.x() / mMapWorldSize)); - const int segsY = static_cast(std::ceil(length.y() / mMapWorldSize)); + auto segments = divideIntoSegments(mBounds, mMapWorldSize); - std::unique_ptr fog (new ESM::FogState()); + auto fog = std::make_unique(); fog->mBounds.mMinX = mBounds.xMin(); fog->mBounds.mMaxX = mBounds.xMax(); @@ -142,13 +142,13 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) fog->mBounds.mMaxY = mBounds.yMax(); fog->mNorthMarkerAngle = mAngle; - fog->mFogTextures.reserve(segsX*segsY); + fog->mFogTextures.reserve(segments.first * segments.second); - for (int x=0; xmFogTextures.emplace_back(); @@ -161,102 +161,18 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) } } - cell->setFog(fog.release()); + cell->setFog(std::move(fog)); } } -osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax) -{ - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10); - camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); - camera->setViewMatrixAsLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); - camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); - camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); - camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 1.f)); - camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - camera->setRenderOrder(osg::Camera::PRE_RENDER); - - camera->setCullMask(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); - camera->setNodeMask(Mask_RenderToTexture); - - // Disable small feature culling, it's not going to be reliable for this camera - osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::CullStack::SMALL_FEATURE_CULLING); - camera->setCullingMode(cullingMode); - - osg::ref_ptr stateset = new osg::StateSet; - stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); - - // assign large value to effectively turn off fog - // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); - fog->setStart(10000000); - fog->setEnd(10000000); - stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); - - osg::ref_ptr lightmodel = new osg::LightModel; - lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); - stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::ref_ptr light = new osg::Light; - light->setPosition(osg::Vec4(-0.3f, -0.3f, 0.7f, 0.f)); - light->setDiffuse(osg::Vec4(0.7f, 0.7f, 0.7f, 1.f)); - light->setAmbient(osg::Vec4(0,0,0,1)); - light->setSpecular(osg::Vec4(0,0,0,0)); - light->setLightNum(0); - light->setConstantAttenuation(1.f); - light->setLinearAttenuation(0.f); - light->setQuadraticAttenuation(0.f); - - osg::ref_ptr lightSource = new osg::LightSource; - lightSource->setLight(light); - - lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); - - camera->addChild(lightSource); - camera->setStateSet(stateset); - camera->setViewport(0, 0, mMapResolution, mMapResolution); - camera->setUpdateCallback(new CameraLocalUpdateCallback(this)); - - return camera; -} - -void LocalMap::setupRenderToTexture(osg::ref_ptr camera, int x, int y) +void LocalMap::setupRenderToTexture(int segment_x, int segment_y, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax) { - osg::ref_ptr texture (new osg::Texture2D); - texture->setTextureSize(mMapResolution, mMapResolution); - texture->setInternalFormat(GL_RGB); - 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); + mLocalMapRTTs.emplace_back(new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax)); - camera->attach(osg::Camera::COLOR_BUFFER, texture); + mRoot->addChild(mLocalMapRTTs.back()); - camera->addChild(mSceneRoot); - mRoot->addChild(camera); - mActiveCameras.push_back(camera); - - MapSegment& segment = mSegments[std::make_pair(x, y)]; - segment.mMapTexture = texture; -} - -bool needUpdate(std::set >& renderedGrid, std::set >& currentGrid, int cellX, int cellY) -{ - // if all the cells of the current grid are contained in the rendered grid then we can keep the old render - for (int dx=-1;dx<2;dx+=1) - { - for (int dy=-1;dy<2;dy+=1) - { - bool haveInRenderedGrid = renderedGrid.find(std::make_pair(cellX+dx,cellY+dy)) != renderedGrid.end(); - bool haveInCurrentGrid = currentGrid.find(std::make_pair(cellX+dx,cellY+dy)) != currentGrid.end(); - if (haveInCurrentGrid && !haveInRenderedGrid) - return true; - } - } - return false; + MapSegment& segment = mInterior? mInteriorSegments[std::make_pair(segment_x, segment_y)] : mExteriorSegments[std::make_pair(segment_x, segment_y)]; + segment.mMapTexture = static_cast(mLocalMapRTTs.back()->getColorTexture(nullptr)); } void LocalMap::requestMap(const MWWorld::CellStore* cell) @@ -266,13 +182,13 @@ void LocalMap::requestMap(const MWWorld::CellStore* cell) int cellX = cell->getCell()->getGridX(); int cellY = cell->getCell()->getGridY(); - MapSegment& segment = mSegments[std::make_pair(cellX, cellY)]; - if (!needUpdate(segment.mGrid, mCurrentGrid, cellX, cellY)) + MapSegment& segment = mExteriorSegments[std::make_pair(cellX, cellY)]; + if (!segment.needUpdate) return; else { - segment.mGrid = mCurrentGrid; requestExteriorMap(cell); + segment.needUpdate = false; } } else @@ -282,27 +198,27 @@ void LocalMap::requestMap(const MWWorld::CellStore* cell) void LocalMap::addCell(MWWorld::CellStore *cell) { if (cell->isExterior()) - mCurrentGrid.emplace(cell->getCell()->getGridX(), cell->getCell()->getGridY()); + mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())].needUpdate = true; +} + +void LocalMap::removeExteriorCell(int x, int y) +{ + mExteriorSegments.erase({ x, y }); } void LocalMap::removeCell(MWWorld::CellStore *cell) { saveFogOfWar(cell); - if (cell->isExterior()) - { - std::pair coords = std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()); - mSegments.erase(coords); - mCurrentGrid.erase(coords); - } - else - mSegments.clear(); + if (!cell->isExterior()) + mInteriorSegments.clear(); } osg::ref_ptr LocalMap::getMapTexture(int x, int y) { - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) return osg::ref_ptr(); else return found->second.mMapTexture; @@ -310,40 +226,27 @@ osg::ref_ptr LocalMap::getMapTexture(int x, int y) osg::ref_ptr LocalMap::getFogOfWarTexture(int x, int y) { - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) return osg::ref_ptr(); else return found->second.mFogOfWarTexture; } -void LocalMap::removeCamera(osg::Camera *cam) -{ - cam->removeChildren(0, cam->getNumChildren()); - mRoot->removeChild(cam); -} - -void LocalMap::markForRemoval(osg::Camera *cam) +void LocalMap::cleanupCameras() { - CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), cam); - if (found == mActiveCameras.end()) + auto it = mLocalMapRTTs.begin(); + while (it != mLocalMapRTTs.end()) { - Log(Debug::Error) << "Error: trying to remove an inactive camera"; - return; + if (!(*it)->isActive()) + { + mRoot->removeChild(*it); + it = mLocalMapRTTs.erase(it); + } + else + it++; } - mActiveCameras.erase(found); - mCamerasPendingRemoval.push_back(cam); -} - -void LocalMap::cleanupCameras() -{ - if (mCamerasPendingRemoval.empty()) - return; - - for (auto& camera : mCamerasPendingRemoval) - removeCamera(camera); - - mCamerasPendingRemoval.clear(); } void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell) @@ -357,11 +260,11 @@ void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell) float zmin = bound.center().z() - bound.radius(); float zmax = bound.center().z() + bound.radius(); - osg::ref_ptr camera = createOrthographicCamera(x*mMapWorldSize + mMapWorldSize/2.f, y*mMapWorldSize + mMapWorldSize/2.f, mMapWorldSize, mMapWorldSize, - osg::Vec3d(0,1,0), zmin, zmax); - setupRenderToTexture(camera, cell->getCell()->getGridX(), cell->getCell()->getGridY()); + setupRenderToTexture(cell->getCell()->getGridX(), cell->getCell()->getGridY(), + x * mMapWorldSize + mMapWorldSize / 2.f, y * mMapWorldSize + mMapWorldSize / 2.f, + osg::Vec3d(0, 1, 0), zmin, zmax); - MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + MapSegment& segment = mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (!segment.mFogOfWarImage) { if (cell->getFog()) @@ -422,87 +325,102 @@ void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) // 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 (for bounds, < padding is considered acceptable) then parts of the interior might not - // be covered by the map anymore. + // 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. - bool cellHasValidFog = false; + std::vector> segmentMappings; if (cell->getFog()) { ESM::FogState* fog = cell->getFog(); - osg::Vec3f newMin (fog->mBounds.mMinX, fog->mBounds.mMinY, zMin); - osg::Vec3f newMax (fog->mBounds.mMaxX, fog->mBounds.mMaxY, zMax); - - osg::Vec3f minDiff = newMin - mBounds._min; - osg::Vec3f maxDiff = newMax - mBounds._max; - - if (std::abs(minDiff.x()) > padding || std::abs(minDiff.y()) > padding - || std::abs(maxDiff.x()) > padding || std::abs(maxDiff.y()) > padding - || std::abs(mAngle - fog->mNorthMarkerAngle) > osg::DegreesToRadians(5.f)) - { - // Nuke it - cellHasValidFog = false; - } - else + if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) { - // Looks sane, use it - mBounds = osg::BoundingBox(newMin, newMax); + // 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; + } + else if(fog->mBounds.mMinX > mBounds.xMin()) + { + float diff = fog->mBounds.mMinX - mBounds.xMin(); + xOffset += diff / mMapWorldSize; + xOffset++; + mBounds.xMin() = fog->mBounds.mMinX - xOffset * mMapWorldSize; + } + if(fog->mBounds.mMinY < mBounds.yMin()) + { + mBounds.yMin() = fog->mBounds.mMinY; + } + else if(fog->mBounds.mMinY > mBounds.yMin()) + { + float diff = fog->mBounds.mMinY - mBounds.yMin(); + yOffset += diff / mMapWorldSize; + yOffset++; + mBounds.yMin() = fog->mBounds.mMinY - yOffset * mMapWorldSize; + } + if (fog->mBounds.mMaxX > mBounds.xMax()) + mBounds.xMax() = fog->mBounds.mMaxX; + if (fog->mBounds.mMaxY > mBounds.yMax()) + mBounds.yMax() = fog->mBounds.mMaxY; + + 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; - cellHasValidFog = true; } } osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - osg::Vec2f max(mBounds.xMax(), mBounds.yMax()); - - osg::Vec2f length = max-min; - - osg::Vec2f center(bounds.center().x(), bounds.center().y()); - // divide into segments - const int segsX = static_cast(std::ceil(length.x() / mMapWorldSize)); - const int segsY = static_cast(std::ceil(length.y() / mMapWorldSize)); + osg::Vec2f center(mBounds.center().x(), mBounds.center().y()); + osg::Quat cameraOrient (mAngle, osg::Vec3d(0,0,-1)); - int i = 0; - for (int x=0; x camera = createOrthographicCamera(pos.x(), pos.y(), - mMapWorldSize, mMapWorldSize, - osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); + setupRenderToTexture(x, y, pos.x(), pos.y(), + osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); - setupRenderToTexture(camera, x, y); - - MapSegment& segment = mSegments[std::make_pair(x,y)]; + auto coords = std::make_pair(x,y); + MapSegment& segment = mInteriorSegments[coords]; if (!segment.mFogOfWarImage) { - if (!cellHasValidFog) - segment.initFogOfWar(); - else + bool loaded = false; + for(size_t index{}; index < segmentMappings.size(); index++) { - ESM::FogState* fog = cell->getFog(); - - // We are using the same bounds and angle as we were using when the textures were originally made. Segments should come out the same. - if (i >= int(fog->mFogTextures.size())) + if(segmentMappings[index] == coords) { - Log(Debug::Warning) << "Warning: fog texture count mismatch"; + ESM::FogState* fog = cell->getFog(); + segment.loadFogOfWar(fog->mFogTextures[index]); + loaded = true; break; } - - segment.loadFogOfWar(fog->mFogTextures[i]); } + if(!loaded) + segment.initFogOfWar(); } - ++i; } } } @@ -532,12 +450,13 @@ osg::Vec2f LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) { - const MapSegment& segment = mSegments[std::make_pair(x, y)]; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + const MapSegment& segment = segments[std::make_pair(x, y)]; if (!segment.mFogOfWarImage) return false; - nX = std::max(0.f, std::min(1.f, nX)); - nY = std::max(0.f, std::min(1.f, nY)); + nX = std::clamp(nX, 0.f, 1.f); + nY = std::clamp(nY, 0.f, 1.f); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); @@ -604,7 +523,8 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient int texX = x + mx; int texY = y + my*-1; - MapSegment& segment = mSegments[std::make_pair(texX, texY)]; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + MapSegment& segment = segments[std::make_pair(texX, texY)]; if (!segment.mFogOfWarImage || !segment.mMapTexture) continue; @@ -620,8 +540,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient uint32_t clr = *(uint32_t*)data; uint8_t alpha = (clr >> 24); - - alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); + alpha = std::min(alpha, (uint8_t)(std::clamp(sqrDist/sqrExploreRadius, 0.f, 1.f) * 255)); uint32_t val = (uint32_t) (alpha << 24); if ( *data != val) { @@ -647,11 +566,6 @@ LocalMap::MapSegment::MapSegment() { } -LocalMap::MapSegment::~MapSegment() -{ - -} - void LocalMap::MapSegment::createFogOfWarTexture() { if (mFogOfWarTexture) @@ -664,6 +578,7 @@ void LocalMap::MapSegment::createFogOfWarTexture() mFogOfWarTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mFogOfWarTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mFogOfWarTexture->setUnRefImageDataAfterApply(false); + mFogOfWarTexture->setImage(mFogOfWarImage); } void LocalMap::MapSegment::initFogOfWar() @@ -679,7 +594,6 @@ void LocalMap::MapSegment::initFogOfWar() memcpy(mFogOfWarImage->data(), &data[0], data.size()*4); createFogOfWarTexture(); - mFogOfWarTexture->setImage(mFogOfWarImage); } void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture &esm) @@ -712,7 +626,6 @@ void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture &esm) mFogOfWarImage->dirty(); createFogOfWarTexture(); - mFogOfWarTexture->setImage(mFogOfWarImage); mHasFogState = true; } @@ -744,4 +657,107 @@ void LocalMap::MapSegment::saveFogOfWar(ESM::FogTexture &fog) const fog.mImageData = std::vector(data.begin(), data.end()); } +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) + , mSceneRoot(sceneRoot) + , mActive(true) +{ + setNodeMask(Mask_RenderToTexture); + + if (SceneUtil::AutoDepth::isReversed()) + mProjectionMatrix = SceneUtil::getReversedZProjectionMatrixAsOrtho(-mapWorldSize / 2, mapWorldSize / 2, -mapWorldSize / 2, mapWorldSize / 2, 5, (zmax - zmin) + 10); + else + mProjectionMatrix.makeOrtho(-mapWorldSize / 2, mapWorldSize / 2, -mapWorldSize / 2, mapWorldSize / 2, 5, (zmax - zmin) + 10); + + mViewMatrix.makeLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); + + setUpdateCallback(new CameraLocalUpdateCallback); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); +} + +void LocalMapRenderToTexture::setDefaults(osg::Camera* camera) +{ + // Disable small feature culling, it's not going to be reliable for this camera + osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING) & ~(osg::Camera::SMALL_FEATURE_CULLING); + camera->setCullingMode(cullingMode); + + SceneUtil::setCameraClearDepth(camera); + camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); + camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 1.f)); + camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + + camera->setCullMask(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setCullMaskLeft(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setCullMaskRight(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setNodeMask(Mask_RenderToTexture); + camera->setProjectionMatrix(mProjectionMatrix); + camera->setViewMatrix(mViewMatrix); + + auto* stateset = camera->getOrCreateStateSet(); + + stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast(mProjectionMatrix)), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + if (Stereo::getMultiview()) + Stereo::setMultiviewMatrices(stateset, { mProjectionMatrix, mProjectionMatrix }); + + // assign large value to effectively turn off fog + // shaders don't respect glDisable(GL_FOG) + osg::ref_ptr fog(new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + + // turn of sky blending + stateset->addUniform(new osg::Uniform("far", 10000000.0f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); + stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{1, 1})); + + osg::ref_ptr lightmodel = new osg::LightModel; + lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); + stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + osg::ref_ptr light = new osg::Light; + light->setPosition(osg::Vec4(-0.3f, -0.3f, 0.7f, 0.f)); + light->setDiffuse(osg::Vec4(0.7f, 0.7f, 0.7f, 1.f)); + light->setAmbient(osg::Vec4(0, 0, 0, 1)); + light->setSpecular(osg::Vec4(0, 0, 0, 0)); + light->setLightNum(0); + light->setConstantAttenuation(1.f); + light->setLinearAttenuation(0.f); + light->setQuadraticAttenuation(0.f); + + osg::ref_ptr lightSource = new osg::LightSource; + lightSource->setLight(light); + + lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + + // override sun for local map + SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot), light, stateset); + + camera->addChild(lightSource); + camera->addChild(mSceneRoot); +} + +void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) +{ + if (!node->isActive()) + node->setNodeMask(0); + + if (node->isActive()) + { + node->setIsActive(false); + } + + // Rtt-nodes do not forward update traversal to their cameras so we can traverse safely. + // Traverse in case there are nested callbacks. + traverse(node, nv); +} + } diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 83a975aeda..911671aee2 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -30,6 +30,8 @@ namespace osg namespace MWRender { + class LocalMapRenderToTexture; + /// /// \brief Local map rendering /// @@ -50,6 +52,7 @@ namespace MWRender void requestMap (const MWWorld::CellStore* cell); void addCell(MWWorld::CellStore* cell); + void removeExteriorCell(int x, int y); void removeCell (MWWorld::CellStore* cell); @@ -57,13 +60,6 @@ namespace MWRender osg::ref_ptr getFogOfWarTexture (int x, int y); - void removeCamera(osg::Camera* cam); - - /** - * Indicates a camera has been queued for rendering and can be cleaned up in the next frame. For internal use only. - */ - void markForRemoval(osg::Camera* cam); - /** * Removes cameras that have already been rendered. Should be called every frame to ensure that * we do not render the same map more than once. Note, this cleanup is difficult to implement in an @@ -103,11 +99,8 @@ namespace MWRender osg::ref_ptr mRoot; osg::ref_ptr mSceneRoot; - typedef std::vector< osg::ref_ptr > CameraVector; - - CameraVector mActiveCameras; - - CameraVector mCamerasPendingRemoval; + typedef std::vector< osg::ref_ptr > RTTVector; + RTTVector mLocalMapRTTs; typedef std::set > Grid; Grid mCurrentGrid; @@ -115,7 +108,7 @@ namespace MWRender struct MapSegment { MapSegment(); - ~MapSegment(); + ~MapSegment() = default; void initFogOfWar(); void loadFogOfWar(const ESM::FogTexture& fog); @@ -126,13 +119,14 @@ namespace MWRender osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; - Grid mGrid; // the grid that was active at the time of rendering this segment + bool needUpdate = true; bool mHasFogState; }; typedef std::map, MapSegment> SegmentMap; - SegmentMap mSegments; + SegmentMap mExteriorSegments; + SegmentMap mInteriorSegments; int mMapResolution; @@ -150,8 +144,7 @@ namespace MWRender void requestExteriorMap(const MWWorld::CellStore* cell); void requestInteriorMap(const MWWorld::CellStore* cell); - osg::ref_ptr createOrthographicCamera(float left, float top, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax); - void setupRenderToTexture(osg::ref_ptr camera, int x, int y); + 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; diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 791c41a1a0..47c06abca1 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -2,16 +2,175 @@ #include "vismask.hpp" #include +#include +#include +#include +#include +#include +#include #include +#include + +#include + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + +#include namespace MWRender { - NavMesh::NavMesh(const osg::ref_ptr& root, bool enabled) + struct NavMesh::LessByTilePosition + { + bool operator()(const DetourNavigator::TilePosition& lhs, + const std::pair& rhs) const + { + return lhs < rhs.first; + } + + bool operator()(const std::pair& lhs, + const DetourNavigator::TilePosition& rhs) const + { + return lhs.first < rhs; + } + }; + + struct NavMesh::CreateNavMeshTileGroups final : SceneUtil::WorkItem + { + std::size_t mId; + DetourNavigator::Version mVersion; + const std::weak_ptr mNavMesh; + const osg::ref_ptr mGroupStateSet; + const osg::ref_ptr mDebugDrawStateSet; + const DetourNavigator::Settings mSettings; + std::map mTiles; + NavMeshMode mMode; + std::atomic_bool mAborted {false}; + std::mutex mMutex; + bool mStarted = false; + std::vector> mUpdatedTiles; + std::vector mRemovedTiles; + + explicit CreateNavMeshTileGroups(std::size_t id, DetourNavigator::Version version, + std::weak_ptr navMesh, + const osg::ref_ptr& groupStateSet, const osg::ref_ptr& debugDrawStateSet, + const DetourNavigator::Settings& settings, const std::map& tiles, + NavMeshMode mode) + : mId(id) + , mVersion(version) + , mNavMesh(navMesh) + , mGroupStateSet(groupStateSet) + , mDebugDrawStateSet(debugDrawStateSet) + , mSettings(settings) + , mTiles(tiles) + , mMode(mode) + { + } + + void doWork() final + { + using DetourNavigator::TilePosition; + using DetourNavigator::Version; + + const std::lock_guard lock(mMutex); + mStarted = true; + + if (mAborted.load(std::memory_order_acquire)) + return; + + const auto navMeshPtr = mNavMesh.lock(); + if (navMeshPtr == nullptr) + return; + + std::vector> existingTiles; + unsigned minSalt = std::numeric_limits::max(); + unsigned maxSalt = 0; + + navMeshPtr->lockConst()->forEachUsedTile([&] (const TilePosition& position, const Version& version, const dtMeshTile& meshTile) + { + existingTiles.emplace_back(position, version); + minSalt = std::min(minSalt, meshTile.salt); + maxSalt = std::max(maxSalt, meshTile.salt); + }); + + if (mAborted.load(std::memory_order_acquire)) + return; + + std::sort(existingTiles.begin(), existingTiles.end()); + + std::vector removedTiles; + + for (const auto& [position, tile] : mTiles) + if (!std::binary_search(existingTiles.begin(), existingTiles.end(), position, LessByTilePosition {})) + removedTiles.push_back(position); + + std::vector> updatedTiles; + + const unsigned char flags = SceneUtil::NavMeshTileDrawFlagsOffMeshConnections + | SceneUtil::NavMeshTileDrawFlagsClosedList + | (mMode == NavMeshMode::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) + continue; + + osg::ref_ptr group; + { + const auto navMesh = navMeshPtr->lockConst(); + const dtMeshTile* meshTile = DetourNavigator::getTile(navMesh->getImpl(), position); + if (meshTile == nullptr) + continue; + + if (mAborted.load(std::memory_order_acquire)) + return; + + group = SceneUtil::createNavMeshTileGroup(navMesh->getImpl(), *meshTile, mSettings, mGroupStateSet, + mDebugDrawStateSet, flags, minSalt, maxSalt); + } + if (group == nullptr) + { + removedTiles.push_back(position); + continue; + } + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + group->setNodeMask(Mask_Debug); + updatedTiles.emplace_back(position, Tile {version, std::move(group)}); + } + + if (mAborted.load(std::memory_order_acquire)) + return; + + mUpdatedTiles = std::move(updatedTiles); + mRemovedTiles = std::move(removedTiles); + } + + void abort() final + { + mAborted.store(true, std::memory_order_release); + } + }; + + struct NavMesh::DeallocateCreateNavMeshTileGroups final : SceneUtil::WorkItem + { + osg::ref_ptr mWorkItem; + + explicit DeallocateCreateNavMeshTileGroups(osg::ref_ptr&& workItem) + : mWorkItem(std::move(workItem)) {} + }; + + NavMesh::NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, + bool enabled, NavMeshMode mode) : mRootNode(root) + , mWorkQueue(workQueue) + , mGroupStateSet(SceneUtil::makeNavMeshTileStateSet()) + , mDebugDrawStateSet(SceneUtil::DebugDraw::makeStateSet()) , mEnabled(enabled) - , mGeneration(0) - , mRevision(0) + , mMode(mode) + , mId(std::numeric_limits::max()) { } @@ -19,6 +178,8 @@ namespace MWRender { if (mEnabled) disable(); + for (const auto& workItem : mWorkItems) + workItem->abort(); } bool NavMesh::toggle() @@ -31,45 +192,129 @@ namespace MWRender return mEnabled; } - void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id, - const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings) + void NavMesh::update(const std::shared_ptr& navMesh, std::size_t id, + const DetourNavigator::Settings& settings) { - if (!mEnabled || (mGroup && mId == id && mGeneration == generation && mRevision == revision)) + using DetourNavigator::TilePosition; + using DetourNavigator::Version; + + if (!mEnabled) + return; + + { + std::pair lastest {0, Version {}}; + osg::ref_ptr latestCandidate; + for (auto it = mWorkItems.begin(); it != mWorkItems.end();) + { + if (!(*it)->isDone()) + { + ++it; + continue; + } + const std::pair order {(*it)->mId, (*it)->mVersion}; + if (lastest < order) + { + lastest = order; + std::swap(latestCandidate, *it); + } + if (*it != nullptr) + mWorkQueue->addWorkItem(new DeallocateCreateNavMeshTileGroups(std::move(*it))); + it = mWorkItems.erase(it); + } + + if (latestCandidate != nullptr) + { + for (const TilePosition& position : latestCandidate->mRemovedTiles) + { + const auto it = mTiles.find(position); + if (it == mTiles.end()) + continue; + mRootNode->removeChild(it->second.mGroup); + mTiles.erase(it); + } + + for (auto& [position, tile] : latestCandidate->mUpdatedTiles) + { + const auto it = mTiles.find(position); + if (it == mTiles.end()) + { + mRootNode->addChild(tile.mGroup); + mTiles.emplace_hint(it, position, std::move(tile)); + } + else + { + mRootNode->replaceChild(it->second.mGroup, tile.mGroup); + std::swap(it->second, tile); + } + } + + mWorkQueue->addWorkItem(new DeallocateCreateNavMeshTileGroups(std::move(latestCandidate))); + } + } + + const auto version = navMesh->lock()->getVersion(); + + if (!mTiles.empty() && mId == id && mVersion == version) return; - mId = id; - mGeneration = generation; - mRevision = revision; - if (mGroup) - mRootNode->removeChild(mGroup); - mGroup = SceneUtil::createNavMeshGroup(navMesh, settings); - if (mGroup) + if (mId != id) { - mGroup->setNodeMask(Mask_Debug); - mRootNode->addChild(mGroup); + reset(); + mId = id; } + + mVersion = version; + + for (auto& workItem : mWorkItems) + { + const std::unique_lock lock(workItem->mMutex, std::try_to_lock); + + if (!lock.owns_lock()) + continue; + + if (workItem->mStarted) + continue; + + workItem->mId = id; + workItem->mVersion = version; + workItem->mTiles = mTiles; + workItem->mMode = mMode; + + return; + } + + osg::ref_ptr workItem = new CreateNavMeshTileGroups(id, version, navMesh, + mGroupStateSet, mDebugDrawStateSet, settings, mTiles, mMode); + mWorkQueue->addWorkItem(workItem); + mWorkItems.push_back(std::move(workItem)); } void NavMesh::reset() { - if (mGroup) - { - mRootNode->removeChild(mGroup); - mGroup = nullptr; - } + for (auto& workItem : mWorkItems) + workItem->abort(); + mWorkItems.clear(); + for (auto& [position, tile] : mTiles) + mRootNode->removeChild(tile.mGroup); + mTiles.clear(); } void NavMesh::enable() { - if (mGroup) - mRootNode->addChild(mGroup); mEnabled = true; } void NavMesh::disable() { - if (mGroup) - mRootNode->removeChild(mGroup); + reset(); mEnabled = false; } + + void NavMesh::setMode(NavMeshMode value) + { + if (mMode == value) + return; + reset(); + mMode = value; + } } diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp index d329b895d7..f4d3f07e94 100644 --- a/apps/openmw/mwrender/navmesh.hpp +++ b/apps/openmw/mwrender/navmesh.hpp @@ -1,14 +1,38 @@ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H -#include +#include "navmeshmode.hpp" + +#include +#include +#include #include +#include +#include +#include +#include +#include + +class dtNavMesh; + namespace osg { class Group; class Geometry; + class StateSet; +} + +namespace DetourNavigator +{ + class NavMeshCacheItem; + struct Settings; +} + +namespace SceneUtil +{ + class WorkQueue; } namespace MWRender @@ -16,13 +40,14 @@ namespace MWRender class NavMesh { public: - NavMesh(const osg::ref_ptr& root, bool enabled); + explicit NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, + bool enabled, NavMeshMode mode); ~NavMesh(); bool toggle(); - void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation, - const std::size_t revision, const DetourNavigator::Settings& settings); + void update(const std::shared_ptr>& navMesh, + std::size_t id, const DetourNavigator::Settings& settings); void reset(); @@ -35,13 +60,29 @@ namespace MWRender return mEnabled; } + void setMode(NavMeshMode value); + private: + struct Tile + { + DetourNavigator::Version mVersion; + osg::ref_ptr mGroup; + }; + + struct LessByTilePosition; + struct CreateNavMeshTileGroups; + struct DeallocateCreateNavMeshTileGroups; + osg::ref_ptr mRootNode; + osg::ref_ptr mWorkQueue; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; bool mEnabled; - std::size_t mId = std::numeric_limits::max(); - std::size_t mGeneration; - std::size_t mRevision; - osg::ref_ptr mGroup; + NavMeshMode mMode; + std::size_t mId; + DetourNavigator::Version mVersion; + std::map mTiles; + std::vector> mWorkItems; }; } diff --git a/apps/openmw/mwrender/navmeshmode.cpp b/apps/openmw/mwrender/navmeshmode.cpp new file mode 100644 index 0000000000..d08e7cf693 --- /dev/null +++ b/apps/openmw/mwrender/navmeshmode.cpp @@ -0,0 +1,16 @@ +#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 new file mode 100644 index 0000000000..9401479e21 --- /dev/null +++ b/apps/openmw/mwrender/navmeshmode.hpp @@ -0,0 +1,17 @@ +#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 36213fc96d..bd9fe543c4 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -19,11 +19,11 @@ #include #include #include +#include +#include #include -#include // TextKeyMapHolder - #include #include "../mwworld/esmstore.hpp" @@ -44,11 +44,13 @@ #include "rotatecontroller.hpp" #include "renderbin.hpp" #include "vismask.hpp" +#include "util.hpp" +#include "postprocessor.hpp" namespace { -std::string getVampireHead(const std::string& race, bool female) +std::string getVampireHead(const std::string& race, bool female, const VFS::Manager& vfs) { static std::map , const ESM::BodyPart* > sVampireMapping; @@ -78,35 +80,7 @@ std::string getVampireHead(const std::string& race, bool female) const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; if (!bodyPart) return std::string(); - return "meshes\\" + bodyPart->mModel; -} - -std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); - for (const auto& part : bodyparts) - { - if (part.mPart != ESM::PRT_Shield) - continue; - - std::string bodypartName; - if (female && !part.mFemale.empty()) - bodypartName = part.mFemale; - else if (!part.mMale.empty()) - bodypartName = part.mMale; - - if (!bodypartName.empty()) - { - const ESM::BodyPart *bodypart = partStore.search(bodypartName); - if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return std::string(); - if (!bodypart->mModel.empty()) - return "meshes\\" + bodypart->mModel; - } - } - - return std::string(); + return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel, &vfs); } } @@ -148,44 +122,6 @@ public: float getValue(osg::NodeVisitor* nv) override; }; -// -------------------------------------------------------------------------------- - -/// Subclass RotateController to add a Z-offset for sneaking in first person mode. -/// @note We use inheritance instead of adding another controller, so that we do not have to compute the worldOrient twice. -/// @note Must be set on a MatrixTransform. -class NeckController : public RotateController -{ -public: - NeckController(osg::Node* relativeTo) - : RotateController(relativeTo) - { - } - - void setOffset(const osg::Vec3f& offset) - { - mOffset = offset; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); - - osg::Quat worldOrient = getWorldOrientation(node); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); - - matrix.setRotate(orient); - matrix.setTrans(matrix.getTrans() + worldOrient.inverse() * mOffset); - - transform->setMatrix(matrix); - - traverse(node,nv); - } - -private: - osg::Vec3f mOffset; -}; - // -------------------------------------------------------------------------------------------------------------- HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) @@ -206,7 +142,8 @@ void HeadAnimationTime::setEnabled(bool enabled) void HeadAnimationTime::resetBlinkTimer() { - mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6)); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6, prng)); } void HeadAnimationTime::update(float dt) @@ -287,37 +224,36 @@ NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr) return curType; } -static NpcAnimation::PartBoneMap createPartListMap() +static const inline NpcAnimation::PartBoneMap createPartListMap() { - NpcAnimation::PartBoneMap result; - result.insert(std::make_pair(ESM::PRT_Head, "Head")); - result.insert(std::make_pair(ESM::PRT_Hair, "Head")); // note it uses "Head" as attach bone, but "Hair" as filter - result.insert(std::make_pair(ESM::PRT_Neck, "Neck")); - result.insert(std::make_pair(ESM::PRT_Cuirass, "Chest")); - result.insert(std::make_pair(ESM::PRT_Groin, "Groin")); - result.insert(std::make_pair(ESM::PRT_Skirt, "Groin")); - result.insert(std::make_pair(ESM::PRT_RHand, "Right Hand")); - result.insert(std::make_pair(ESM::PRT_LHand, "Left Hand")); - result.insert(std::make_pair(ESM::PRT_RWrist, "Right Wrist")); - result.insert(std::make_pair(ESM::PRT_LWrist, "Left Wrist")); - result.insert(std::make_pair(ESM::PRT_Shield, "Shield Bone")); - result.insert(std::make_pair(ESM::PRT_RForearm, "Right Forearm")); - result.insert(std::make_pair(ESM::PRT_LForearm, "Left Forearm")); - result.insert(std::make_pair(ESM::PRT_RUpperarm, "Right Upper Arm")); - result.insert(std::make_pair(ESM::PRT_LUpperarm, "Left Upper Arm")); - result.insert(std::make_pair(ESM::PRT_RFoot, "Right Foot")); - result.insert(std::make_pair(ESM::PRT_LFoot, "Left Foot")); - result.insert(std::make_pair(ESM::PRT_RAnkle, "Right Ankle")); - result.insert(std::make_pair(ESM::PRT_LAnkle, "Left Ankle")); - result.insert(std::make_pair(ESM::PRT_RKnee, "Right Knee")); - result.insert(std::make_pair(ESM::PRT_LKnee, "Left Knee")); - result.insert(std::make_pair(ESM::PRT_RLeg, "Right Upper Leg")); - result.insert(std::make_pair(ESM::PRT_LLeg, "Left Upper Leg")); - result.insert(std::make_pair(ESM::PRT_RPauldron, "Right Clavicle")); - result.insert(std::make_pair(ESM::PRT_LPauldron, "Left Clavicle")); - result.insert(std::make_pair(ESM::PRT_Weapon, "Weapon Bone")); // Fallback. The real node name depends on the current weapon type. - result.insert(std::make_pair(ESM::PRT_Tail, "Tail")); - return result; + return { + {ESM::PRT_Head, "Head"}, + {ESM::PRT_Hair, "Head"}, // note it uses "Head" as attach bone, but "Hair" as filter + {ESM::PRT_Neck, "Neck"}, + {ESM::PRT_Cuirass, "Chest"}, + {ESM::PRT_Groin, "Groin"}, + {ESM::PRT_Skirt, "Groin"}, + {ESM::PRT_RHand, "Right Hand"}, + {ESM::PRT_LHand, "Left Hand"}, + {ESM::PRT_RWrist, "Right Wrist"}, + {ESM::PRT_LWrist, "Left Wrist"}, + {ESM::PRT_Shield, "Shield Bone"}, + {ESM::PRT_RForearm, "Right Forearm"}, + {ESM::PRT_LForearm, "Left Forearm"}, + {ESM::PRT_RUpperarm, "Right Upper Arm"}, + {ESM::PRT_LUpperarm, "Left Upper Arm"}, + {ESM::PRT_RFoot, "Right Foot"}, + {ESM::PRT_LFoot, "Left Foot"}, + {ESM::PRT_RAnkle, "Right Ankle"}, + {ESM::PRT_LAnkle, "Left Ankle"}, + {ESM::PRT_RKnee, "Right Knee"}, + {ESM::PRT_LKnee, "Left Knee"}, + {ESM::PRT_RLeg, "Right Upper Leg"}, + {ESM::PRT_LLeg, "Left Upper Leg"}, + {ESM::PRT_RPauldron, "Right Clavicle"}, + {ESM::PRT_LPauldron, "Left Clavicle"}, + {ESM::PRT_Weapon, "Weapon Bone"}, // Fallback. The real node name depends on the current weapon type. + {ESM::PRT_Tail, "Tail"}}; } const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap(); @@ -340,8 +276,8 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr par { mNpc = mPtr.get()->mBase; - mHeadAnimationTime = std::shared_ptr(new HeadAnimationTime(mPtr)); - mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); + mHeadAnimationTime = std::make_shared(mPtr); + mWeaponAnimationTime = std::make_shared(this); for(size_t i = 0;i < ESM::PRT_Count;i++) { @@ -361,7 +297,7 @@ void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) return; mViewMode = viewMode; - MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale()); // apply race height after view change + MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale(), true); // apply race height after view change mAmmunition.reset(); rebuild(); @@ -369,25 +305,67 @@ void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) } /// @brief A RenderBin callback to clear the depth buffer before rendering. +/// Switches depth attachments to a proxy renderbuffer, reattaches original depth then redraws first person root. +/// This gives a complete depth buffer which can be used for postprocessing, buffer resolves as if depth was never cleared. class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { public: DepthClearCallback() { - mDepth = new osg::Depth; + mDepth = new SceneUtil::AutoDepth; mDepth->setWriteMask(true); + + mStateSet = new osg::StateSet; + mStateSet->setAttributeAndModes(new osg::ColorMask(false, false, false, false), osg::StateAttribute::ON); + mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); } void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override { - renderInfo.getState()->applyAttribute(mDepth); + osg::State* state = renderInfo.getState(); - glClear(GL_DEPTH_BUFFER_BIT); + PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); - bin->drawImplementation(renderInfo, previous); + 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); + + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + // color accumulation pass + bin->drawImplementation(renderInfo, previous); + + auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + + if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) + postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); + else + primaryFBO->apply(*state); + + // 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); + } + + state->checkGLErrors("after DepthClearCallback::drawImplementation"); } osg::ref_ptr mDepth; + osg::ref_ptr mStateSet; }; /// Overrides Field of View to given value for rendering the subgraph. @@ -404,7 +382,7 @@ public: { osgUtil::CullVisitor* cv = static_cast(nv); float fov, aspect, zNear, zFar; - if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar)) + if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar) && std::abs(fov-mFov) > 0.001) { fov = mFov; osg::ref_ptr newProjectionMatrix = new osg::RefMatrix(); @@ -457,8 +435,8 @@ int NpcAnimation::getSlot(const osg::NodePath &path) const { for (int i=0; igetNode().get()) != path.end()) { @@ -491,7 +469,7 @@ void NpcAnimation::updateNpcBase() { const ESM::BodyPart* bp = store.get().search(headName); if (bp) - mHeadModel = "meshes\\" + bp->mModel; + mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); else Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; } @@ -500,12 +478,12 @@ void NpcAnimation::updateNpcBase() { const ESM::BodyPart* bp = store.get().search(hairName); if (bp) - mHairModel = "meshes\\" + bp->mModel; + mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); else Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; } - const std::string& vampireHead = getVampireHead(mNpc->mRace, isFemale); + const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale, *mResourceSystem->getVFS()); if (!isWerewolf && isVampire && !vampireHead.empty()) mHeadModel = vampireHead; @@ -517,7 +495,8 @@ void NpcAnimation::updateNpcBase() std::string smodel = defaultSkeleton; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) - smodel = Misc::ResourceHelpers::correctActorModelPath("meshes\\" + mNpc->mModel, mResourceSystem->getVFS()); + smodel = Misc::ResourceHelpers::correctActorModelPath( + Misc::ResourceHelpers::correctMeshPath(mNpc->mModel, mResourceSystem->getVFS()), mResourceSystem->getVFS()); setObjectRoot(smodel, true, true, false); @@ -525,7 +504,7 @@ void NpcAnimation::updateNpcBase() if(!is1stPerson) { - const std::string base = "meshes\\xbase_anim.nif"; + const std::string base = Settings::Manager::getString("xbaseanim", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); @@ -539,7 +518,7 @@ void NpcAnimation::updateNpcBase() } else { - const std::string base = "meshes\\xbase_anim.1st.nif"; + const std::string base = Settings::Manager::getString("xbaseanim1st", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); @@ -552,14 +531,9 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } -std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - // Try to recover the body part model, use ground model as a fallback otherwise. - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + std::string mesh = getShieldMesh(shield, !mNpc->isMale()); if (mesh.empty()) return std::string(); @@ -633,13 +607,13 @@ void NpcAnimation::updateParts() int prio = 1; bool enchantedGlow = !store->getClass().getEnchantment(*store).empty(); osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store); - if(store->getTypeName() == typeid(ESM::Clothing).name()) + if(store->getType() == ESM::Clothing::sRecordId) { prio = ((slotlist[i].mBasePriority+1)<<1) + 0; const ESM::Clothing *clothes = store->get()->mBase; addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor); } - else if(store->getTypeName() == typeid(ESM::Armor).name()) + else if(store->getType() == ESM::Armor::sRecordId) { prio = ((slotlist[i].mBasePriority+1)<<1) + 1; const ESM::Armor *armor = store->get()->mBase; @@ -679,11 +653,12 @@ void NpcAnimation::updateParts() { MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); MWWorld::ConstPtr part; - if(store != inv.end() && (part=*store).getTypeName() == typeid(ESM::Light).name()) + if(store != inv.end() && (part=*store).getType() == ESM::Light::sRecordId) { const ESM::Light *light = part.get()->mBase; - addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, - 1, "meshes\\"+light->mModel); + 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); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); } @@ -702,8 +677,11 @@ void NpcAnimation::updateParts() { const ESM::BodyPart* bodypart = parts[part]; if(bodypart) - addOrReplaceIndividualPart((ESM::PartReferenceType)part, -1, 1, - "meshes\\"+bodypart->mModel); + { + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + addOrReplaceIndividualPart(static_cast(part), -1, 1, + Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs)); + } } } @@ -713,16 +691,9 @@ void NpcAnimation::updateParts() -PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) +PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - - osg::ref_ptr attached = SceneUtil::attach(instance, mObjectRoot, bonefilter, found->second); + osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); @@ -795,7 +766,7 @@ bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } -bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) +bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if(priority <= mPartPriorities[type]) return false; @@ -810,7 +781,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + if(weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId) { int weaponType = weapon->get()->mBase->mData.mType; const std::string weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone; @@ -818,7 +789,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g if (weaponBonename != bonename) { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); + NodeMap::const_iterator found = nodeMap.find(weaponBonename); if (found != nodeMap.end()) bonename = weaponBonename; } @@ -827,7 +798,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; - mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); + mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight); } catch (std::exception& e) { @@ -864,7 +835,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g for (unsigned int i=0; igetUserDataContainer()->getNumUserObjects(); ++i) { osg::Object* obj = node->getUserDataContainer()->getUserObject(i); - if (NifOsg::TextKeyMapHolder* keys = dynamic_cast(obj)) + if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast(obj)) { for (const auto &key : keys->mTextKeys) { @@ -882,14 +853,18 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g } } } + SceneUtil::ForceControllerSourcesVisitor assignVisitor(src); + node->accept(assignVisitor); } - else if (type == ESM::PRT_Weapon) - src = mWeaponAnimationTime; else - src.reset(new NullAnimationTime); - - SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); - node->accept(assignVisitor); + { + if (type == ESM::PRT_Weapon) + src = mWeaponAnimationTime; + else + src = std::make_shared(); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); + node->accept(assignVisitor); + } } return true; @@ -936,7 +911,11 @@ void NpcAnimation::addPartGroup(int group, int priority, const std::vectormModel, enchantedGlow, glowColor); + { + 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); + } else reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); } @@ -955,7 +934,7 @@ void NpcAnimation::addControllers() if (found != mNodeMap.end()) { osg::MatrixTransform* node = found->second.get(); - mFirstPersonNeckController = new NeckController(mObjectRoot.get()); + mFirstPersonNeckController = new RotateController(mObjectRoot.get()); node->addUpdateCallback(mFirstPersonNeckController); mActiveControllers.emplace_back(node, mFirstPersonNeckController); } @@ -982,7 +961,7 @@ void NpcAnimation::showWeapons(bool showWeapon) mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); // Crossbows start out with a bolt attached - if (weapon->getTypeName() == typeid(ESM::Weapon).name() && + if (weapon->getType() == ESM::Weapon::sRecordId && weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType; @@ -997,7 +976,7 @@ void NpcAnimation::showWeapons(bool showWeapon) removeIndividualPart(ESM::PRT_Weapon); // If we remove/hide weapon from player, we should reset attack animation as well if (mPtr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); + mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(false); } updateHolsteredWeapon(!mShowWeapons); @@ -1014,19 +993,16 @@ void NpcAnimation::showCarriedLeft(bool show) osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); std::string mesh = iter->getClass().getModel(*iter); // For shields we must try to use the body part model - if (iter->getTypeName() == typeid(ESM::Armor).name()) + if (iter->getType() == ESM::Armor::sRecordId) { - const ESM::Armor *armor = iter->get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + mesh = getShieldMesh(*iter, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) + mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId)) { if (mesh.empty()) reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); - if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield]) + if (iter->getType() == ESM::Light::sRecordId && mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get()->mBase); } } @@ -1066,17 +1042,19 @@ void NpcAnimation::releaseArrow(float attackStrength) osg::Group* NpcAnimation::getArrowBone() { - PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; - if (!part) + const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get(); + if (part == nullptr) return nullptr; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return nullptr; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + if (ammoType == ESM::Weapon::None) + return nullptr; // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); @@ -1091,8 +1069,8 @@ osg::Group* NpcAnimation::getArrowBone() osg::Node* NpcAnimation::getWeaponNode() { - PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; - if (!part) + const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get(); + if (part == nullptr) return nullptr; return part->getNode(); } @@ -1102,34 +1080,6 @@ Resource::ResourceSystem* NpcAnimation::getResourceSystem() return mResourceSystem; } -void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) -{ - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - if (isNew) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - } - - if (!magicEffect->mHit.empty()) - { - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Don't play particle VFX unless the effect is new or it should be looping. - if (isNew || loop) - addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } -} - void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 7e55001daf..2dcfac3036 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -24,14 +24,13 @@ namespace MWSound namespace MWRender { -class NeckController; +class RotateController; class HeadAnimationTime; class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: void equipmentChanged() override; - void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; public: typedef std::map PartBoneMap; @@ -84,20 +83,20 @@ private: NpcType getNpcType() const; PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, - const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); + const std::string &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 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); void setRenderBin(); - osg::ref_ptr mFirstPersonNeckController; + osg::ref_ptr mFirstPersonNeckController; static bool isFirstPersonPart(const ESM::BodyPart* bodypart); static bool isFemalePart(const ESM::BodyPart* bodypart); @@ -106,7 +105,7 @@ private: protected: void addControllers() override; bool isArrowAttached() const override; - std::string getShieldMesh(MWWorld::ConstPtr shield) const override; + std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override; public: /** diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index a5015f377b..20d69bcf11 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -2,26 +2,29 @@ #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 @@ -32,6 +35,8 @@ #include "vismask.hpp" +#include + namespace MWRender { @@ -64,7 +69,7 @@ namespace MWRender case ESM::REC_CONT: return store.get().searchStatic(id)->mModel; default: - return std::string(); + return {}; } } @@ -77,7 +82,7 @@ namespace MWRender osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile); @@ -105,6 +110,7 @@ namespace MWRender bool mOptimizeBillboards = true; float mSqrDistance = 0.f; osg::Vec3f mViewVector; + osg::Node::NodeMask mCopyMask = ~0u; mutable std::vector mNodePath; void copy(const osg::Node* toCopy, osg::Group* attachTo) @@ -121,6 +127,9 @@ namespace MWRender 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); @@ -147,6 +156,13 @@ namespace MWRender 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); @@ -224,9 +240,14 @@ namespace MWRender } 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)) @@ -249,15 +270,6 @@ namespace MWRender } }; - class TemplateRef : public osg::Object - { - public: - TemplateRef() {} - TemplateRef(const TemplateRef& copy, const osg::CopyOp&) : mObjects(copy.mObjects) {} - META_Object(MWRender, TemplateRef) - std::vector> mObjects; - }; - class RefnumSet : public osg::Object { public: @@ -270,9 +282,11 @@ namespace MWRender class AnalyzeVisitor : public osg::NodeVisitor { public: - AnalyzeVisitor() + AnalyzeVisitor(osg::Node::NodeMask analyzeMask) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mCurrentStateSet(nullptr) {} + , mCurrentStateSet(nullptr) + , mCurrentDistance(0.f) + { setTraversalMask(analyzeMask); } typedef std::unordered_map StateSetCounter; struct Result @@ -285,6 +299,27 @@ namespace MWRender { if (node.getStateSet()) mCurrentStateSet = node.getStateSet(); + + if (osg::Switch* sw = node.asSwitch()) + { + for (unsigned int i=0; igetNumChildren(); ++i) + if (sw->getValue(i)) + traverse(*sw->getChild(i)); + return; + } + if (osg::LOD* lod = dynamic_cast(&node)) + { + for (unsigned int i=0; igetNumChildren(); ++i) + if (lod->getMinRange(i) * lod->getMinRange(i) <= mCurrentDistance && mCurrentDistance < lod->getMaxRange(i) * lod->getMaxRange(i)) + traverse(*lod->getChild(i)); + return; + } + if (osg::Sequence* sq = dynamic_cast(&node)) + { + traverse(*sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0)); + return; + } + traverse(node); } void apply(osg::Geometry& geom) override @@ -322,6 +357,7 @@ namespace MWRender Result mResult; osg::StateSet* mCurrentStateSet; StateSetCounter mGlobalStateSetCounter; + float mCurrentDistance; }; class DebugVisitor : public osg::NodeVisitor @@ -340,6 +376,8 @@ namespace MWRender 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); } }; @@ -365,7 +403,7 @@ namespace MWRender , mRefTrackerLocked(false) { mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); - mDebugBatches = Settings::Manager::getBool("object paging debug batches", "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"); @@ -380,7 +418,7 @@ namespace MWRender osg::Vec3f relativeViewPoint = viewPoint - worldCenter; std::map refs; - std::vector esm; + ESM::ReadersCache readers; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) @@ -393,37 +431,46 @@ namespace MWRender { try { - unsigned int index = cell->mContextList.at(i).index; - if (esm.size()<=index) - esm.resize(index+1); - cell->restore(esm[index], 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; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); + ESM::MovedCellRef cMRef; + cMRef.mRefNum.mIndex = 0; bool deleted = false; - while(cell->getNextRef(esm[index], ref, deleted)) + bool moved = false; + while (ESM::Cell::getNextRef(*reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { + if (moved) + continue; + + if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) + continue; + Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - 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[ref.mRefNum] = ref; + refs[ref.mRefNum] = std::move(ref); } } - catch (std::exception& e) + catch (std::exception&) { continue; } } - for (ESM::CellRefTracker::const_iterator it = cell->mLeasedRefs.begin(); it != cell->mLeasedRefs.end(); ++it) + for (auto [ref, deleted] : cell->mLeasedRefs) { - ESM::CellRef ref = it->first; + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - bool deleted = it->second; - if (deleted) { refs.erase(ref.mRefNum); continue; } int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; - refs[ref.mRefNum] = ref; + refs[ref.mRefNum] = std::move(ref); } } } @@ -446,7 +493,15 @@ namespace MWRender typedef std::map, InstanceList> NodeMap; NodeMap nodes; osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; - AnalyzeVisitor analyzeVisitor; + + // Mask_UpdateVisitor is used in such cases in NIF loader: + // 1. For collision nodes, which is not supposed to be rendered. + // 2. For nodes masked via Flag_Hidden (VisController can change this flag value at runtime). + // Since ObjectPaging does not handle VisController, we can just ignore both types of nodes. + constexpr auto copyMask = ~Mask_UpdateVisitor; + + AnalyzeVisitor analyzeVisitor(copyMask); + analyzeVisitor.mCurrentDistance = (viewPoint - worldCenter).length2(); float minSize = mMinSize; if (mMinSizeMergeFactor) minSize *= mMinSizeMergeFactor; @@ -459,7 +514,7 @@ namespace MWRender { osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; 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()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (maxBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) continue; } @@ -472,13 +527,13 @@ namespace MWRender continue; } - if (ref.mRefID == "prisonmarker" || ref.mRefID == "divinemarker" || ref.mRefID == "templemarker" || ref.mRefID == "northmarker") - continue; // marker objects that have a hardcoded function in the game logic, should be hidden from the player + if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefID)) + continue; int type = store.findStatic(ref.mRefID); std::string model = getModel(type, ref.mRefID, store); if (model.empty()) continue; - model = "meshes/" + model; + model = Misc::ResourceHelpers::correctMeshPath(model, mSceneManager->getVFS()); if (activeGrid && type != ESM::REC_STAT) { @@ -530,9 +585,10 @@ namespace MWRender osg::ref_ptr group = new osg::Group; osg::ref_ptr mergeGroup = new osg::Group; - osg::ref_ptr templateRefs = new TemplateRef; + osg::ref_ptr templateRefs = new Resource::TemplateMultiRef; osgUtil::StateToCompile stateToCompile(0, nullptr); CopyOp copyop; + copyop.mCopyMask = copyMask; for (const auto& pair : nodes) { const osg::Node* cnode = pair.first; @@ -558,15 +614,36 @@ namespace MWRender if (!activeGrid && minSizeMerged != minSize && cnode->getBound().radius2() * cref->mScale*cref->mScale < (viewPoint-pos).length2()*minSizeMerged*minSizeMerged) continue; - osg::Matrixf matrix; - matrix.preMultTranslate(pos - worldCenter); - matrix.preMultRotate( osg::Quat(ref.mPos.rot[2], osg::Vec3f(0,0,-1)) * + 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)) ); - matrix.preMultScale(osg::Vec3f(ref.mScale, ref.mScale, ref.mScale)); - osg::ref_ptr trans = new osg::MatrixTransform(matrix); - trans->setDataVariance(osg::Object::STATIC); + osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1,0,0)); + osg::Vec3f nodeScale = osg::Vec3f(ref.mScale, ref.mScale, ref.mScale); + + osg::ref_ptr trans; + if (merge) + { + // Optimizer currently supports only MatrixTransforms. + osg::Matrixf matrix; + matrix.preMultTranslate(nodePos); + matrix.preMultRotate(nodeAttitude); + matrix.preMultScale(nodeScale); + trans = new osg::MatrixTransform(matrix); + trans->setDataVariance(osg::Object::STATIC); + } + else + { + trans = new SceneUtil::PositionAttitudeTransform; + SceneUtil::PositionAttitudeTransform* pat = static_cast(trans.get()); + pat->setPosition(nodePos); + pat->setScale(nodeScale); + pat->setAttitude(nodeAttitude); + } + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateMultiRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by needvbo() in optimizer.cpp) copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); copyop.mOptimizeBillboards = (size > 1/4.f); copyop.mNodePath.push_back(trans); @@ -595,8 +672,9 @@ namespace MWRender } if (numinstances > 0) { - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache - templateRefs->mObjects.emplace_back(cnode); + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, we hint to the cache that it's still being used and should be kept in cache + templateRefs->addRef(cnode); if (pair.second.mNeedCompile) { @@ -619,6 +697,7 @@ namespace MWRender } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS|SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES|SceneUtil::Optimizer::MERGE_GEOMETRY; + optimizer.optimize(mergeGroup, options); group->addChild(mergeGroup); @@ -679,12 +758,8 @@ namespace MWRender } void clampToCell(osg::Vec3f& cellPos) { - osg::Vec2i min (mCell.x(), mCell.y()); - osg::Vec2i max (mCell.x()+1, mCell.y()+1); - if (cellPos.x() < min.x()) cellPos.x() = min.x(); - if (cellPos.x() > max.x()) cellPos.x() = max.x(); - if (cellPos.y() < min.y()) cellPos.y() = min.y(); - if (cellPos.y() > max.y()) cellPos.y() = max.y(); + 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; @@ -709,7 +784,7 @@ namespace MWRender ccf.mCell = cell; mCache->call(ccf); if (ccf.mToClear.empty()) return false; - for (auto chunk : ccf.mToClear) + for (const auto& chunk : ccf.mToClear) mCache->removeFromObjectCache(chunk); return true; } @@ -731,7 +806,7 @@ namespace MWRender ccf.mActiveGridOnly = true; mCache->call(ccf); if (ccf.mToClear.empty()) return false; - for (auto chunk : ccf.mToClear) + for (const auto& chunk : ccf.mToClear) mCache->removeFromObjectCache(chunk); return true; } diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp index ff32dadd4d..940760ff6c 100644 --- a/apps/openmw/mwrender/objectpaging.hpp +++ b/apps/openmw/mwrender/objectpaging.hpp @@ -1,9 +1,9 @@ -#ifndef OPENMW_COMPONENTS_ESMPAGING_CHUNKMANAGER_H -#define OPENMW_COMPONENTS_ESMPAGING_CHUNKMANAGER_H +#ifndef OPENMW_MWRENDER_OBJECTPAGING_H +#define OPENMW_MWRENDER_OBJECTPAGING_H #include #include -#include +#include #include @@ -63,7 +63,7 @@ namespace MWRender { std::set mDisabled; std::set mBlacklist; - bool operator==(const RefTracker&other) { return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; } + bool operator==(const RefTracker&other) const { return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; } }; RefTracker mRefTracker; RefTracker mRefTrackerNew; diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index ec1c4397bf..9dc4e04e24 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -4,7 +4,6 @@ #include #include -#include #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" @@ -18,10 +17,9 @@ namespace MWRender { -Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue) +Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode) : mRootNode(rootNode) , mResourceSystem(resourceSystem) - , mUnrefQueue(unrefQueue) { } @@ -36,7 +34,7 @@ Objects::~Objects() void Objects::insertBegin(const MWWorld::Ptr& ptr) { - assert(mObjects.find(ptr) == mObjects.end()); + assert(mObjects.find(ptr.mRef) == mObjects.end()); osg::ref_ptr cellnode; @@ -75,7 +73,7 @@ void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh, bool osg::ref_ptr anim (new ObjectAnimation(ptr, mesh, mResourceSystem, animated, allowLight)); - mObjects.insert(std::make_pair(ptr, anim)); + mObjects.emplace(ptr.mRef, std::move(anim)); } void Objects::insertCreature(const MWWorld::Ptr &ptr, const std::string &mesh, bool weaponsShields) @@ -91,7 +89,7 @@ void Objects::insertCreature(const MWWorld::Ptr &ptr, const std::string &mesh, b else anim = new CreatureAnimation(ptr, mesh, mResourceSystem); - if (mObjects.insert(std::make_pair(ptr, anim)).second) + if (mObjects.emplace(ptr.mRef, anim).second) ptr.getClass().getContainerStore(ptr).setContListener(static_cast(anim.get())); } @@ -102,7 +100,7 @@ void Objects::insertNPC(const MWWorld::Ptr &ptr) osg::ref_ptr anim (new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); - if (mObjects.insert(std::make_pair(ptr, anim)).second) + if (mObjects.emplace(ptr.mRef, anim).second) { ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get(), ptr); ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); @@ -114,12 +112,9 @@ bool Objects::removeObject (const MWWorld::Ptr& ptr) if(!ptr.getRefData().getBaseNode()) return true; - PtrAnimationMap::iterator iter = mObjects.find(ptr); + const auto iter = mObjects.find(ptr.mRef); if(iter != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); - mObjects.erase(iter); if (ptr.getClass().isActor()) @@ -146,14 +141,11 @@ void Objects::removeCell(const MWWorld::CellStore* store) MWWorld::Ptr ptr = iter->second->getPtr(); if(ptr.getCell() == store) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); - - if (ptr.getClass().isNpc() && ptr.getRefData().getCustomData()) + if (ptr.getClass().isActor() && ptr.getRefData().getCustomData()) { - MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); - invStore.setInvListener(nullptr, ptr); - invStore.setContListener(nullptr); + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr, ptr); + ptr.getClass().getContainerStore(ptr).setContListener(nullptr); } mObjects.erase(iter++); @@ -166,15 +158,13 @@ void Objects::removeCell(const MWWorld::CellStore* store) if(cell != mCellSceneNodes.end()) { cell->second->getParent(0)->removeChild(cell->second); - if (mUnrefQueue.get()) - mUnrefQueue->push(cell->second); mCellSceneNodes.erase(cell); } } void Objects::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) { - osg::Node* objectNode = cur.getRefData().getBaseNode(); + osg::ref_ptr objectNode = cur.getRefData().getBaseNode(); if (!objectNode) return; @@ -201,19 +191,14 @@ void Objects::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) objectNode->getParent(0)->removeChild(objectNode); cellnode->addChild(objectNode); - PtrAnimationMap::iterator iter = mObjects.find(old); + PtrAnimationMap::iterator iter = mObjects.find(old.mRef); if(iter != mObjects.end()) - { - osg::ref_ptr anim = iter->second; - mObjects.erase(iter); - anim->updatePtr(cur); - mObjects[cur] = anim; - } + iter->second->updatePtr(cur); } Animation* Objects::getAnimation(const MWWorld::Ptr &ptr) { - PtrAnimationMap::const_iterator iter = mObjects.find(ptr); + PtrAnimationMap::const_iterator iter = mObjects.find(ptr.mRef); if(iter != mObjects.end()) return iter->second; @@ -222,7 +207,7 @@ Animation* Objects::getAnimation(const MWWorld::Ptr &ptr) const Animation* Objects::getAnimation(const MWWorld::ConstPtr &ptr) const { - PtrAnimationMap::const_iterator iter = mObjects.find(ptr); + PtrAnimationMap::const_iterator iter = mObjects.find(ptr.mRef); if(iter != mObjects.end()) return iter->second; diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 98ebb95d98..793239c26e 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -24,11 +24,6 @@ namespace MWWorld class CellStore; } -namespace SceneUtil -{ - class UnrefQueue; -} - namespace MWRender{ class Animation; @@ -55,8 +50,9 @@ public: MWWorld::Ptr mPtr; }; -class Objects{ - typedef std::map > PtrAnimationMap; +class Objects +{ + using PtrAnimationMap = std::map>; typedef std::map > CellMap; CellMap mCellSceneNodes; @@ -66,12 +62,10 @@ class Objects{ Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mUnrefQueue; - void insertBegin(const MWWorld::Ptr& ptr); public: - Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue); + Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode); ~Objects(); /// @param animated Attempt to load separate keyframes from a .kf file matching the model file? diff --git a/apps/openmw/mwrender/pathgrid.cpp b/apps/openmw/mwrender/pathgrid.cpp index c20e81bb2d..ec7d2c15e0 100644 --- a/apps/openmw/mwrender/pathgrid.cpp +++ b/apps/openmw/mwrender/pathgrid.cpp @@ -6,9 +6,12 @@ #include #include -#include +#include #include +#include #include +#include +#include #include "../mwbase/world.hpp" // these includes can be removed once the static-hack is gone #include "../mwbase/environment.hpp" @@ -112,6 +115,8 @@ void Pathgrid::enableCellPathgrid(const MWWorld::CellStore *store) osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(geometry, "debug"); + cellPathGrid->addChild(geometry); mPathGridRoot->addChild(cellPathGrid); diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp new file mode 100644 index 0000000000..f6b8e464fe --- /dev/null +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -0,0 +1,321 @@ +#include "pingpongcanvas.hpp" + +#include +#include +#include +#include + +#include + +#include "postprocessor.hpp" + +namespace MWRender +{ + PingPongCanvas::PingPongCanvas(Shader::ShaderManager& shaderManager) + : mFallbackStateSet(new osg::StateSet) + , mMultiviewResolveStateSet(new osg::StateSet) + { + setUseDisplayList(false); + setUseVertexBufferObjects(true); + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-1, -1, 0)); + verts->push_back(osg::Vec3f(-1, 3, 0)); + verts->push_back(osg::Vec3f(3, -1, 0)); + + setVertexArray(verts); + + addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); + + mHDRDriver = HDRDriver(shaderManager); + mHDRDriver.disable(); + + Shader::ShaderManager::DefineMap defines; + Stereo::Manager::instance().shaderStereoDefines(defines); + + auto fallbackVertex = shaderManager.getShader("fullscreen_tri_vertex.glsl", defines, osg::Shader::VERTEX); + auto fallbackFragment = shaderManager.getShader("fullscreen_tri_fragment.glsl", defines, osg::Shader::FRAGMENT); + mFallbackProgram = shaderManager.getProgram(fallbackVertex, fallbackFragment); + + mFallbackStateSet->setAttributeAndModes(mFallbackProgram); + mFallbackStateSet->addUniform(new osg::Uniform("omw_SamplerLastShader", 0)); + + auto multiviewResolveVertex = shaderManager.getShader("multiview_resolve_vertex.glsl", {}, osg::Shader::VERTEX); + auto multiviewResolveFragment = shaderManager.getShader("multiview_resolve_fragment.glsl", {}, osg::Shader::FRAGMENT); + mMultiviewResolveProgram = shaderManager.getProgram(multiviewResolveVertex, multiviewResolveFragment); + mMultiviewResolveStateSet->setAttributeAndModes(mMultiviewResolveProgram); + mMultiviewResolveStateSet->addUniform(new osg::Uniform("omw_SamplerLastShader", 0)); + } + + void PingPongCanvas::setCurrentFrameData(size_t frameId, fx::DispatchArray&& data) + { + mBufferData[frameId].data = std::move(data); + } + + void PingPongCanvas::setMask(size_t frameId, 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; + } + + void PingPongCanvas::drawGeometry(osg::RenderInfo& renderInfo) const + { + osg::Geometry::drawImplementation(renderInfo); + } + + static void attachCloneOfTemplate(osg::FrameBufferObject* fbo, osg::Camera::BufferComponent component, osg::Texture* tex) + { + osg::ref_ptr clone = static_cast(tex->clone(osg::CopyOp::SHALLOW_COPY)); + fbo->setAttachment(component, Stereo::createMultiviewCompatibleAttachment(clone)); + } + + void PingPongCanvas::drawImplementation(osg::RenderInfo& renderInfo) const + { + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + + size_t frameId = state.getFrameStamp()->getFrameNumber() % 2; + + auto& bufferData = mBufferData[frameId]; + + const auto& data = bufferData.data; + + std::vector filtered; + + filtered.reserve(data.size()); + + for (size_t i = 0; i < data.size(); ++i) + { + const auto& node = data[i]; + + if (bufferData.mask & node.mFlags) + continue; + + filtered.push_back(i); + } + + auto* resolveViewport = state.getCurrentViewport(); + + if (filtered.empty() || !bufferData.postprocessing) + { + if (bufferData.postprocessing) + { + if (!mLoggedLastError) + { + Log(Debug::Error) << "Critical error, postprocess shaders failed to compile. Using default shader."; + mLoggedLastError = true; + } + } + else + mLoggedLastError = false; + + state.pushStateSet(mFallbackStateSet); + state.apply(); + + if (Stereo::getMultiview() && mMultiviewResolveProgram) + { + state.pushStateSet(mMultiviewResolveStateSet); + state.apply(); + } + + state.applyTextureAttribute(0, bufferData.sceneTex); + resolveViewport->apply(state); + + drawGeometry(renderInfo); + state.popStateSet(); + + if (Stereo::getMultiview() && mMultiviewResolveProgram) + { + state.popStateSet(); + } + + return; + } + + const unsigned int handle = mFbos[0] ? mFbos[0]->getHandle(state.getContextID()) : 0; + + if (handle == 0 || bufferData.dirty) + { + for (auto& fbo : mFbos) + { + fbo = new osg::FrameBufferObject; + attachCloneOfTemplate(fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + fbo->apply(state); + glClearColor(0.5, 0.5, 0.5, 1); + glClear(GL_COLOR_BUFFER_BIT); + } + + if (Stereo::getMultiview()) + { + mMultiviewResolveFramebuffer = new osg::FrameBufferObject(); + attachCloneOfTemplate(mMultiviewResolveFramebuffer, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + mMultiviewResolveFramebuffer->apply(state); + glClearColor(0.5, 0.5, 0.5, 1); + glClear(GL_COLOR_BUFFER_BIT); + + mMultiviewResolveStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, (osg::Texture*)mMultiviewResolveFramebuffer->getAttachment(osg::Camera::COLOR_BUFFER0).getTexture()); + } + + mHDRDriver.dirty(bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + + if (Stereo::getStereo()) + mRenderViewport = new osg::Viewport(0, 0, bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + else + mRenderViewport = nullptr; + + bufferData.dirty = false; + } + + constexpr std::array, 3> buffers = {{ + {GL_COLOR_ATTACHMENT1_EXT, GL_COLOR_ATTACHMENT2_EXT}, + {GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT2_EXT}, + {GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT} + }}; + + (bufferData.hdr) ? mHDRDriver.enable() : mHDRDriver.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. + mHDRDriver.draw(*this, renderInfo, state, ext, frameId); + + auto buffer = buffers[0]; + + int lastDraw = 0; + int lastShader = 0; + + unsigned int lastApplied = handle; + + const unsigned int cid = state.getContextID(); + + const osg::ref_ptr& destinationFbo = bufferData.destination ? bufferData.destination : nullptr; + unsigned int destinationHandle = destinationFbo ? destinationFbo->getHandle(cid) : 0; + + auto bindDestinationFbo = [&]() { + if (destinationFbo) + { + destinationFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastApplied = destinationHandle; + } + else if (Stereo::getMultiview()) + { + mMultiviewResolveFramebuffer->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastApplied = mMultiviewResolveFramebuffer->getHandle(cid); + } + else + { + ext->glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, 0); + + lastApplied = 0; + } + }; + + for (const size_t& index : filtered) + { + const auto& node = data[index]; + + node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, bufferData.depthTex); + + if (bufferData.hdr) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_EyeAdaptation, mHDRDriver.getLuminanceTexture(frameId)); + + if (bufferData.normalsTex) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, bufferData.normalsTex); + + state.pushStateSet(node.mRootStateSet); + state.apply(); + + for (size_t passIndex = 0; passIndex < node.mPasses.size(); ++passIndex) + { + if (mRenderViewport) + mRenderViewport->apply(state); + const auto& pass = node.mPasses[passIndex]; + + bool lastPass = passIndex == node.mPasses.size() - 1; + + //VR-TODO: This won't actually work for tex2darrays + if (lastShader == 0) + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, bufferData.sceneTex); + else + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, (osg::Texture*)mFbos[lastShader - GL_COLOR_ATTACHMENT0_EXT]->getAttachment(osg::Camera::COLOR_BUFFER0).getTexture()); + + if (lastDraw == 0) + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, bufferData.sceneTex); + else + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, (osg::Texture*)mFbos[lastDraw - GL_COLOR_ATTACHMENT0_EXT]->getAttachment(osg::Camera::COLOR_BUFFER0).getTexture()); + + if (pass.mRenderTarget) + { + pass.mRenderTarget->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + if (pass.mRenderTexture->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture()); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + + lastApplied = pass.mRenderTarget->getHandle(state.getContextID());; + } + else if (pass.mResolve && index == filtered.back()) + { + bindDestinationFbo(); + if (!destinationFbo && !Stereo::getMultiview()) + { + resolveViewport->apply(state); + } + } + else if (lastPass) + { + lastDraw = buffer[0]; + lastShader = buffer[0]; + mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + buffer = buffers[lastShader - GL_COLOR_ATTACHMENT0_EXT]; + + lastApplied = mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->getHandle(cid); + } + else + { + mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastDraw = buffer[0]; + std::swap(buffer[0], buffer[1]); + + lastApplied = mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->getHandle(cid); + } + + state.pushStateSet(pass.mStateSet); + state.apply(); + + if (!state.getLastAppliedProgramObject()) + mFallbackProgram->apply(state); + + drawGeometry(renderInfo); + + state.popStateSet(); + state.apply(); + } + + state.popStateSet(); + } + + if (Stereo::getMultiview() && mMultiviewResolveProgram) + { + ext->glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, 0); + lastApplied = 0; + + resolveViewport->apply(state); + state.pushStateSet(mMultiviewResolveStateSet); + state.apply(); + + drawGeometry(renderInfo); + + state.popStateSet(); + state.apply(); + } + + if (lastApplied != destinationHandle) + { + bindDestinationFbo(); + } + } +} diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp new file mode 100644 index 0000000000..8df141f587 --- /dev/null +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -0,0 +1,91 @@ +#ifndef OPENMW_MWRENDER_PINGPONGCANVAS_H +#define OPENMW_MWRENDER_PINGPONGCANVAS_H + +#include +#include + +#include +#include +#include + +#include + +#include "postprocessor.hpp" +#include "hdr.hpp" + +namespace Shader +{ + class ShaderManager; +} + +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; } + + void drawGeometry(osg::RenderInfo& renderInfo) const; + + private: + void copyNewFrameData(size_t frameId) const; + + mutable HDRDriver mHDRDriver; + + 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; + + 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 osg::ref_ptr mRenderViewport; + + mutable bool mLoggedLastError = false; + }; +} + +#endif diff --git a/apps/openmw/mwrender/pingpongcull.cpp b/apps/openmw/mwrender/pingpongcull.cpp new file mode 100644 index 0000000000..c3affa531a --- /dev/null +++ b/apps/openmw/mwrender/pingpongcull.cpp @@ -0,0 +1,92 @@ +#include "pingpongcull.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "postprocessor.hpp" +#include "pingpongcanvas.hpp" + +namespace MWRender +{ + + PingPongCull::PingPongCull(PostProcessor* pp) + : mViewportStateset(nullptr) + , mPostProcessor(pp) + { + if (Stereo::getStereo()) + { + mViewportStateset = new osg::StateSet(); + mViewport = new osg::Viewport(0, 0, pp->renderWidth(), pp->renderHeight()); + mViewportStateset->setAttribute(mViewport); + } + } + + PingPongCull::~PingPongCull() + { + // Instantiate osg::ref_ptr<> destructor + } + + void PingPongCull::operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); + 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.computeEyeViewOffset(index) * sm.computeEyeProjection(index, true); + postProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); + } + + postProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); + postProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); + mLastViewMatrix[0] = cv->getCurrentCamera()->getViewMatrix(); + + postProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); + postProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); + + if (!postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)) + { + renderStage->setMultisampleResolveFramebufferObject(nullptr); + renderStage->setFrameBufferObject(nullptr); + } + else if (!postProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) + { + renderStage->setFrameBufferObject(postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + } + else + { + renderStage->setMultisampleResolveFramebufferObject(postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(postProcessor->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 + if (Stereo::getMultiview() && !renderStage->getDrawCallback()) + Stereo::setMultiviewMSAAResolveCallback(renderStage); + } + + if (mViewportStateset) + { + mViewport->setViewport(0, 0, mPostProcessor->renderWidth(), mPostProcessor->renderHeight()); + renderStage->setViewport(mViewport); + cv->pushStateSet(mViewportStateset.get()); + traverse(node, cv); + cv->popStateSet(); + } + else + traverse(node, cv); + } +} diff --git a/apps/openmw/mwrender/pingpongcull.hpp b/apps/openmw/mwrender/pingpongcull.hpp new file mode 100644 index 0000000000..da6c0c3c68 --- /dev/null +++ b/apps/openmw/mwrender/pingpongcull.hpp @@ -0,0 +1,34 @@ +#ifndef OPENMW_MWRENDER_PINGPONGCULL_H +#define OPENMW_MWRENDER_PINGPONGCULL_H + +#include + +#include + +#include "postprocessor.hpp" + +namespace osg +{ + class StateSet; + class Viewport; +} + +namespace MWRender +{ + class PostProcessor; + class PingPongCull : public SceneUtil::NodeCallback + { + public: + PingPongCull(PostProcessor* pp); + ~PingPongCull(); + + void operator()(osg::Node* node, osgUtil::CullVisitor* nv); + private: + std::array mLastViewMatrix; + osg::ref_ptr mViewportStateset; + osg::ref_ptr mViewport; + PostProcessor* mPostProcessor; + }; +} + +#endif diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp new file mode 100644 index 0000000000..0991ede7f2 --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -0,0 +1,897 @@ +#include "postprocessor.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwgui/postprocessorhud.hpp" + +#include "transparentpass.hpp" +#include "pingpongcull.hpp" +#include "renderingmanager.hpp" +#include "vismask.hpp" +#include "sky.hpp" + +namespace +{ + struct ResizedCallback : osg::GraphicsContext::ResizedCallback + { + ResizedCallback(MWRender::PostProcessor* postProcessor) + : mPostProcessor(postProcessor) + { } + + void resizedImplementation(osg::GraphicsContext* gc, int x, int y, int width, int height) override + { + gc->resizedImplementation(x, y, width, height); + + mPostProcessor->setRenderTargetSize(width, height); + mPostProcessor->resize(); + } + + MWRender::PostProcessor* mPostProcessor; + }; + + class HUDCullCallback : public SceneUtil::NodeCallback + { + public: + void operator()(osg::Camera* camera, osgUtil::CullVisitor* cv) + { + osg::ref_ptr stateset = new osg::StateSet; + auto& sm = Stereo::Manager::instance(); + auto* fullViewport = camera->getViewport(); + if (sm.getEye(cv) == Stereo::Eye::Left) + stateset->setAttributeAndModes(new osg::Viewport(0, 0, fullViewport->width() / 2, fullViewport->height())); + if (sm.getEye(cv) == Stereo::Eye::Right) + stateset->setAttributeAndModes(new osg::Viewport(fullViewport->width() / 2, 0, fullViewport->width() / 2, fullViewport->height())); + + cv->pushStateSet(stateset); + traverse(camera, cv); + cv->popStateSet(); + } + }; + + enum class Usage + { + RENDER_BUFFER, + TEXTURE, + }; + + static osg::FrameBufferAttachment createFrameBufferAttachmentFromTemplate(Usage usage, int width, int height, osg::Texture* template_, int samples) + { + if (usage == Usage::RENDER_BUFFER && !Stereo::getMultiview()) + { + osg::ref_ptr attachment = new osg::RenderBuffer(width, height, template_->getInternalFormat(), samples); + return osg::FrameBufferAttachment(attachment); + } + + auto texture = Stereo::createMultiviewCompatibleTexture(width, height, samples); + texture->setSourceFormat(template_->getSourceFormat()); + texture->setSourceType(template_->getSourceType()); + texture->setInternalFormat(template_->getInternalFormat()); + texture->setFilter(osg::Texture2D::MIN_FILTER, template_->getFilter(osg::Texture2D::MIN_FILTER)); + texture->setFilter(osg::Texture2D::MAG_FILTER, template_->getFilter(osg::Texture2D::MAG_FILTER)); + texture->setWrap(osg::Texture::WRAP_S, template_->getWrap(osg::Texture2D::WRAP_S)); + texture->setWrap(osg::Texture::WRAP_T, template_->getWrap(osg::Texture2D::WRAP_T)); + + return Stereo::createMultiviewCompatibleAttachment(texture); + } +} + +namespace MWRender +{ + PostProcessor::PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs) + : osg::Group() + , mRootNode(rootNode) + , mSamples(Settings::Manager::getInt("antialiasing", "Video")) + , mDirty(false) + , mDirtyFrameId(0) + , mRendering(rendering) + , mViewer(viewer) + , mVFS(vfs) + , 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) + , mMainTemplate(new osg::Texture2D) + { + mSoftParticles = Settings::Manager::getBool("soft particles", "Shaders"); + mUsePostProcessing = Settings::Manager::getBool("enabled", "Post Processing"); + + osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); + osg::GLExtensions* ext = gc->getState()->get(); + + mWidth = gc->getTraits()->width; + mHeight = gc->getTraits()->height; + + if (!ext->glDisablei && ext->glDisableIndexedEXT) + ext->glDisablei = ext->glDisableIndexedEXT; + +#ifdef ANDROID + ext->glDisablei = nullptr; +#endif + + if (ext->glDisablei) + mNormalsSupported = true; + 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; + } + } + + mGLSLVersion = ext->glslLanguageVersion * 100; + mUBO = ext->isUniformBufferObjectSupported && mGLSLVersion >= 330; + mStateUpdater = new fx::StateUpdater(mUBO); + + if (!Stereo::getStereo() && !SceneUtil::AutoDepth::isReversed() && !mSoftParticles && !mUsePostProcessing) + return; + + enable(mUsePostProcessing); + } + + PostProcessor::~PostProcessor() + { + if (auto* bin = osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")) + bin->setDrawCallback(nullptr); + } + + void PostProcessor::resize() + { + mHUDCamera->resize(mWidth, mHeight); + mViewer->getCamera()->resize(mWidth, mHeight); + 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); + + dirtyTechniques(); + + mPingPongCanvas->dirty(frameId); + + mDirty = true; + mDirtyFrameId = !frameId; + + } + + void PostProcessor::populateTechniqueFiles() + { + for (const auto& name : mVFS->getRecursiveDirectoryIterator(fx::Technique::sSubdir)) + { + std::filesystem::path path = name; + std::string fileExt = Misc::StringUtils::lowerCase(path.extension().string()); + if (!path.parent_path().has_parent_path() && fileExt == fx::Technique::sExt) + { + auto absolutePath = std::filesystem::path(mVFS->getAbsoluteFileName(name)); + mTechniqueFileMap[absolutePath.stem().string()] = absolutePath; + } + } + } + + void PostProcessor::enable(bool usePostProcessing) + { + 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(); + } + + mMainTemplate->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + mMainTemplate->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + mMainTemplate->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mMainTemplate->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mMainTemplate->setInternalFormat(GL_RGBA); + mMainTemplate->setSourceType(GL_UNSIGNED_BYTE); + mMainTemplate->setSourceFormat(GL_RGBA); + + 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); + } + + 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) + cull(frameId, static_cast(&nv)); + else if (nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) + update(frameId); + + osg::Group::traverse(nv); + } + + void PostProcessor::cull(size_t frameId, osgUtil::CullVisitor* cv) + { + const auto& fbo = getFbo(FBO_Intercept, frameId); + if (fbo) + { + 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()); + + mPingPongCanvas->setSceneTexture(frameId, getTexture(Tex_Scene, frameId)); + if (mDisableDepthPasses) + mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_Depth, frameId)); + else + mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_OpaqueDepth, frameId)); + + mPingPongCanvas->setLDRSceneTexture(frameId, getTexture(Tex_Scene_LDR, frameId)); + + if (mTransparentDepthPostPass) + { + mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; + mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; + mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; + mTransparentDepthPostPass->dirtyFrame(frameId); + } + + size_t frame = cv->getTraversalNumber(); + + mStateUpdater->setResolution(osg::Vec2f(cv->getViewport()->width(), cv->getViewport()->height())); + + // per-frame data + if (frame != mLastFrameNumber) + { + mLastFrameNumber = frame; + auto stamp = cv->getFrameStamp(); + + mStateUpdater->setSimulationTime(static_cast(stamp->getSimulationTime())); + mStateUpdater->setDeltaSimulationTime(static_cast(stamp->getSimulationTime() - mLastSimulationTime)); + mLastSimulationTime = stamp->getSimulationTime(); + + for (const auto& dispatchNode : mPingPongCanvas->getCurrentFrameData(frame)) + { + for (auto& uniform : dispatchNode.mHandle->getUniformMap()) + { + if (uniform->getType().has_value() && !uniform->mSamplerType) + if (auto* u = dispatchNode.mRootStateSet->getUniform(uniform->mName)) + uniform->setUniform(u); + } + } + } + } + + void PostProcessor::updateLiveReload() + { + static const bool liveReload = Settings::Manager::getBool("live reload", "Post Processing"); + if (!liveReload) + return; + + for (auto& technique : mTechniques) + { + if (technique->getStatus() == fx::Technique::Status::File_Not_exists) + continue; + + const auto lastWriteTime = std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()]); + const bool isDirty = technique->setLastModificationTime(lastWriteTime); + + if (!isDirty) + continue; + + if (technique->compile()) + Log(Debug::Info) << "Reloaded technique : " << mTechniqueFileMap[technique->getName()].string(); + + mReload = technique->isValid(); + } + } + + void PostProcessor::reloadIfRequired() + { + if (!mReload) + return; + + mReload = false; + + if (!mTechniques.empty()) + reloadMainPass(*mTechniques[0]); + + reloadTechniques(); + + if (!mUsePostProcessing) + resize(); + } + + void PostProcessor::update(size_t frameId) + { + while (!mQueuedTemplates.empty()) + { + mTemplates.push_back(std::move(mQueuedTemplates.back())); + + mQueuedTemplates.pop_back(); + } + + updateLiveReload(); + + reloadIfRequired(); + + if (mDirty && mDirtyFrameId == frameId) + { + createTexturesAndCamera(frameId); + createObjectsForFrame(frameId); + mDirty = false; + + mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + } + + if ((mNormalsSupported && mNormals != mPrevNormals) || (mPassLights != mPrevPassLights)) + { + mPrevNormals = mNormals; + mPrevPassLights = mPassLights; + + mViewer->stopThreading(); + + 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); + mStateUpdater->reset(); + + mViewer->startThreading(); + + createTexturesAndCamera(frameId); + createObjectsForFrame(frameId); + + mDirty = true; + mDirtyFrameId = !frameId; + } + } + + void PostProcessor::createObjectsForFrame(size_t frameId) + { + auto& fbos = mFbos[frameId]; + auto& textures = mTextures[frameId]; + auto width = renderWidth(); + auto height = renderHeight(); + + for (auto& tex : textures) + { + if (!tex) + continue; + + Stereo::setMultiviewCompatibleTextureSize(tex, width, height); + tex->dirtyTextureObject(); + } + + fbos[FBO_Primary] = new osg::FrameBufferObject; + fbos[FBO_Primary]->setAttachment(osg::Camera::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + if (mNormals && mNormalsSupported) + fbos[FBO_Primary]->setAttachment(osg::Camera::COLOR_BUFFER1, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); + fbos[FBO_Primary]->setAttachment(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Depth])); + + fbos[FBO_FirstPerson] = new osg::FrameBufferObject; + + auto fpDepthRb = createFrameBufferAttachmentFromTemplate(Usage::RENDER_BUFFER, width, height, textures[Tex_Depth], mSamples); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, osg::FrameBufferAttachment(fpDepthRb)); + + // When MSAA is enabled we must first render to a render buffer, then + // blit the result to the FBO which is either passed to the main frame + // buffer for display or used as the entry point for a post process chain. + if (mSamples > 1) + { + fbos[FBO_Multisample] = new osg::FrameBufferObject; + auto colorRB = createFrameBufferAttachmentFromTemplate(Usage::RENDER_BUFFER, width, height, textures[Tex_Scene], mSamples); + if (mNormals && mNormalsSupported) + { + 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); + fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, colorRB); + fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, depthRB); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, colorRB); + + fbos[FBO_Intercept] = new osg::FrameBufferObject; + fbos[FBO_Intercept]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + fbos[FBO_Intercept]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); + } + else + { + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + if (mNormals && mNormalsSupported) + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, 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])); + } + +#ifdef __APPLE__ + if (textures[Tex_OpaqueDepth]) + fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER, osg::FrameBufferAttachment(new osg::RenderBuffer(textures[Tex_OpaqueDepth]->getTextureWidth(), textures[Tex_OpaqueDepth]->getTextureHeight(), textures[Tex_Scene]->getInternalFormat()))); +#endif + } + + void PostProcessor::dirtyTechniques() + { + if (!isEnabled()) + return; + + size_t frameId = frame() % 2; + + mDirty = true; + mDirtyFrameId = !frameId; + + mTemplateData = {}; + + bool sunglare = true; + mHDR = false; + mNormals = false; + mPassLights = false; + + for (const auto& technique : mTechniques) + { + if (!technique->isValid()) + continue; + + if (technique->getGLSLVersion() > mGLSLVersion) + { + Log(Debug::Warning) << "Technique " << technique->getName() << " requires GLSL version " << technique->getGLSLVersion() << " which is unsupported by your hardware."; + continue; + } + + fx::DispatchNode node; + + node.mFlags = technique->getFlags(); + + if (technique->getHDR()) + mHDR = true; + + if (technique->getNormals()) + mNormals = true; + + if (technique->getLights()) + mPassLights = true; + + if (node.mFlags & fx::Technique::Flag_Disable_SunGlare) + sunglare = false; + + // required default samplers available to every shader pass + 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)); + + if (mNormals) + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerNormals", Unit_Normals)); + + if (technique->getHDR()) + node.mRootStateSet->addUniform(new osg::Uniform("omw_EyeAdaptation", Unit_EyeAdaptation)); + + int texUnit = Unit_NextFree; + + // user-defined samplers + for (const osg::Texture* texture : technique->getTextures()) + { + if (const auto* tex1D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture1D(*tex1D)); + else if (const auto* tex2D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture2D(*tex2D)); + else if (const auto* tex3D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture3D(*tex3D)); + + node.mRootStateSet->addUniform(new osg::Uniform(texture->getName().c_str(), texUnit++)); + } + + // user-defined uniforms + for (auto& uniform : technique->getUniformMap()) + { + if (uniform->mSamplerType) continue; + + if (auto type = uniform->getType()) + uniform->setUniform(node.mRootStateSet->getOrCreateUniform(uniform->mName.c_str(), *type, uniform->getNumElements())); + } + + std::unordered_map renderTargetCache; + + for (const auto& pass : technique->getPasses()) + { + int subTexUnit = texUnit; + fx::DispatchNode::SubPass subPass; + + pass->prepareStateSet(subPass.mStateSet, technique->getName()); + + node.mHandle = technique; + + if (!pass->getTarget().empty()) + { + const auto& rt = technique->getRenderTargetsMap()[pass->getTarget()]; + + const auto [w, h] = rt.mSize.get(renderWidth(), renderHeight()); + + 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.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)); + } + + for (const auto& whitelist : pass->getRenderTargets()) + { + auto it = technique->getRenderTargetsMap().find(whitelist); + if (it != technique->getRenderTargetsMap().end() && renderTargetCache[it->second.mTarget]) + { + subPass.mStateSet->setTextureAttribute(subTexUnit, renderTargetCache[it->second.mTarget]); + subPass.mStateSet->addUniform(new osg::Uniform(std::string(it->first).c_str(), subTexUnit++)); + } + } + + node.mPasses.emplace_back(std::move(subPass)); + } + + node.compile(); + + mTemplateData.emplace_back(std::move(node)); + } + + mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + + if (auto hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) + hud->updateTechniques(); + + mRendering.getSkyManager()->setSunglare(sunglare); + } + + 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()); + + mTechniques.insert(mTechniques.begin() + pos, technique); + dirtyTechniques(); + + return Status_Toggled; + } + + PostProcessor::Status PostProcessor::disableTechnique(std::shared_ptr technique, bool dirty) + { + if (technique->getLocked()) + return Status_Error; + + auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); + if (it == std::end(mTechniques)) + return Status_Unchanged; + + mTechniques.erase(it); + if (dirty) + dirtyTechniques(); + + return Status_Toggled; + } + + bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const + { + 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); + + if (mMainTemplate) + { + textures[Tex_Scene]->setSourceFormat(mMainTemplate->getSourceFormat()); + textures[Tex_Scene]->setSourceType(mMainTemplate->getSourceType()); + textures[Tex_Scene]->setInternalFormat(mMainTemplate->getInternalFormat()); + textures[Tex_Scene]->setFilter(osg::Texture2D::MIN_FILTER, mMainTemplate->getFilter(osg::Texture2D::MIN_FILTER)); + textures[Tex_Scene]->setFilter(osg::Texture2D::MAG_FILTER, mMainTemplate->getFilter(osg::Texture2D::MAG_FILTER)); + textures[Tex_Scene]->setWrap(osg::Texture::WRAP_S, mMainTemplate->getWrap(osg::Texture2D::WRAP_S)); + textures[Tex_Scene]->setWrap(osg::Texture::WRAP_T, mMainTemplate->getWrap(osg::Texture2D::WRAP_T)); + } + + 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; + + for (const auto& technique : mQueuedTemplates) + if (Misc::StringUtils::ciEqual(technique->getName(), name)) + return technique; + + auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), name, renderWidth(), renderHeight(), mUBO, mNormalsSupported); + + technique->compile(); + + if (technique->getStatus() != fx::Technique::Status::File_Not_exists) + technique->setLastModificationTime(std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()])); + + if (loadNextFrame) + { + mQueuedTemplates.push_back(technique); + return technique; + } + + reloadMainPass(*technique); + + mTemplates.push_back(std::move(technique)); + + return mTemplates.back(); + } + + void PostProcessor::reloadTechniques() + { + if (!isEnabled()) + return; + + mTechniques.clear(); + + std::vector techniqueStrings; + Misc::StringUtils::split(Settings::Manager::getString("chain", "Post Processing"), techniqueStrings, ","); + + const std::string mainIdentifier = "main"; + + auto main = loadTechnique(mainIdentifier); + + if (main) + main->setLocked(true); + + mTechniques.push_back(std::move(main)); + + for (auto& techniqueName : techniqueStrings) + { + Misc::StringUtils::trim(techniqueName); + + if (techniqueName.empty() || Misc::StringUtils::ciEqual(techniqueName, mainIdentifier)) + continue; + + mTechniques.push_back(loadTechnique(techniqueName)); + } + + dirtyTechniques(); + } + + void PostProcessor::reloadMainPass(fx::Technique& technique) + { + if (!technique.getMainTemplate()) + return; + + mMainTemplate = technique.getMainTemplate(); + + resize(); + } + + void PostProcessor::toggleMode() + { + for (auto& technique : mTemplates) + technique->compile(); + + dirtyTechniques(); + } + + void PostProcessor::disableDynamicShaders() + { + for (auto& technique : mTechniques) + if (technique->getDynamic()) + disableTechnique(technique); + } + + int PostProcessor::renderWidth() const + { + if (Stereo::getStereo()) + return Stereo::Manager::instance().eyeResolution().x(); + return mWidth; + } + + int PostProcessor::renderHeight() const + { + if (Stereo::getStereo()) + return Stereo::Manager::instance().eyeResolution().y(); + return mHeight; + } +} + diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp new file mode 100644 index 0000000000..2fa3e5622a --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -0,0 +1,262 @@ +#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H +#define OPENMW_MWRENDER_POSTPROCESSOR_H + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "pingpongcanvas.hpp" +#include "transparentpass.hpp" + +#include + +namespace osgViewer +{ + class Viewer; +} + +namespace Stereo +{ + class MultiviewFramebuffer; +} + +namespace VFS +{ + class Manager; +} + +namespace Shader +{ + class ShaderManager; +} + +namespace MWRender +{ + class RenderingManager; + class PingPongCull; + class PingPongCanvas; + class TransparentDepthBinCallback; + + class PostProcessor : public osg::Group + { + public: + using FBOArray = std::array, 5>; + using TextureArray = std::array, 5>; + using TechniqueList = std::vector>; + + enum TextureIndex + { + Tex_Scene, + Tex_Scene_LDR, + Tex_Depth, + Tex_OpaqueDepth, + Tex_Normal + }; + + enum FBOIndex + { + FBO_Primary, + FBO_Multisample, + FBO_FirstPerson, + FBO_OpaqueDepth, + FBO_Intercept + }; + + enum TextureUnits + { + Unit_LastShader = 0, + Unit_LastPass, + Unit_Depth, + Unit_EyeAdaptation, + Unit_Normals, + Unit_NextFree + }; + + PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs); + + ~PostProcessor(); + + void traverse(osg::NodeVisitor& nv) override; + + osg::ref_ptr getFbo(FBOIndex index, unsigned int frameId) { return mFbos[frameId][index]; } + + osg::ref_ptr getTexture(TextureIndex index, unsigned int frameId) { return mTextures[frameId][index]; } + + osg::ref_ptr getPrimaryFbo(unsigned int frameId) { return mFbos[frameId][FBO_Multisample] ? mFbos[frameId][FBO_Multisample] : mFbos[frameId][FBO_Primary]; } + + 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(); + + enum Status + { + Status_Error, + Status_Toggled, + Status_Unchanged + }; + + Status enableTechnique(std::shared_ptr technique, std::optional location = std::nullopt); + + Status disableTechnique(std::shared_ptr technique, bool dirty = true); + + bool getSupportsNormalsRT() const { return mNormalsSupported; } + + template + void setUniform(std::shared_ptr technique, const std::string& name, const T& value) + { + if (!isEnabled()) + return; + + auto it = technique->findUniform(name); + + if (it == technique->getUniformMap().end()) + return; + + if ((*it)->mStatic) + { + Log(Debug::Warning) << "Attempting to set a configration variable [" << name << "] as a uniform"; + return; + } + + (*it)->setValue(value); + } + + std::optional getUniformSize(std::shared_ptr technique, const std::string& name) + { + auto it = technique->findUniform(name); + + if (it == technique->getUniformMap().end()) + return std::nullopt; + + return (*it)->getNumElements(); + } + + bool isTechniqueEnabled(const std::shared_ptr& technique) const; + + void setExteriorFlag(bool exterior) { mExteriorFlag = exterior; } + + void setUnderwaterFlag(bool underwater) { mUnderwater = underwater; } + + void toggleMode(); + + 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; } + + void disable(); + + void enable(bool usePostProcessing = true); + + void setRenderTargetSize(int width, int height) { mWidth = width; mHeight = height; } + + void disableDynamicShaders(); + + int renderWidth() const; + int renderHeight() const; + + private: + + void populateTechniqueFiles(); + + size_t frame() const { return mViewer->getFrameStamp()->getFrameNumber(); } + + void createObjectsForFrame(size_t frameId); + + void createTexturesAndCamera(size_t frameId); + + void reloadTechniques(); + + void reloadMainPass(fx::Technique& technique); + + void dirtyTechniques(); + + void update(size_t frameId); + + void reloadIfRequired(); + + void updateLiveReload(); + + void cull(size_t frameId, osgUtil::CullVisitor* cv); + + osg::ref_ptr mRootNode; + osg::ref_ptr mHUDCamera; + + std::array mTextures; + std::array mFbos; + + TechniqueList mTechniques; + TechniqueList mTemplates; + TechniqueList mQueuedTemplates; + + std::unordered_map mTechniqueFileMap; + + int mSamples; + + bool mDirty; + size_t mDirtyFrameId; + + RenderingManager& mRendering; + osgViewer::Viewer* mViewer; + const VFS::Manager* mVFS; + + bool mReload; + bool mEnabled; + bool mUsePostProcessing; + bool mSoftParticles; + bool mDisableDepthPasses; + + size_t mLastFrameNumber; + float mLastSimulationTime; + + bool mExteriorFlag; + bool mUnderwater; + bool mHDR; + bool mNormals; + bool mPrevNormals; + bool mNormalsSupported; + bool mPassLights; + bool mPrevPassLights; + bool mUBO; + int mGLSLVersion; + + osg::ref_ptr mMainTemplate; + + osg::ref_ptr mStateUpdater; + osg::ref_ptr mPingPongCull; + osg::ref_ptr mPingPongCanvas; + osg::ref_ptr mTransparentDepthPostPass; + + int mWidth; + int mHeight; + + fx::DispatchArray mTemplateData; + }; +} + +#endif diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp index d07e7d37bb..5f202720b2 100644 --- a/apps/openmw/mwrender/recastmesh.cpp +++ b/apps/openmw/mwrender/recastmesh.cpp @@ -1,12 +1,17 @@ #include "recastmesh.hpp" #include -#include +#include +#include +#include +#include #include #include "vismask.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" namespace MWRender { RecastMesh::RecastMesh(const osg::ref_ptr& root, bool enabled) @@ -49,14 +54,14 @@ namespace MWRender if (it->second.mGeneration != tile->second->getGeneration() || it->second.mRevision != tile->second->getRevision()) { - const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings); + const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings.mRecast); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); group->setNodeMask(Mask_Debug); mRootNode->removeChild(it->second.mValue); mRootNode->addChild(group); it->second.mValue = group; it->second.mGeneration = tile->second->getGeneration(); it->second.mRevision = tile->second->getRevision(); - continue; } ++it; @@ -66,7 +71,8 @@ namespace MWRender { if (mGroups.count(tile.first)) continue; - const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings); + const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings.mRecast); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); group->setNodeMask(Mask_Debug); mGroups.emplace(tile.first, Group {tile.second->getGeneration(), tile.second->getRevision(), group}); mRootNode->addChild(group); diff --git a/apps/openmw/mwrender/recastmesh.hpp b/apps/openmw/mwrender/recastmesh.hpp index 729438dbe5..194ec04a62 100644 --- a/apps/openmw/mwrender/recastmesh.hpp +++ b/apps/openmw/mwrender/recastmesh.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWRENDER_RECASTMESH_H #define OPENMW_MWRENDER_RECASTMESH_H -#include +#include #include @@ -13,6 +13,11 @@ namespace osg class Geometry; } +namespace DetourNavigator +{ + struct Settings; +} + namespace MWRender { class RecastMesh diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 63d3743a97..7245b62e6b 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -2,8 +2,6 @@ #include #include -#include -#include #include #include @@ -13,51 +11,59 @@ #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 -#include -#include +#include #include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/groundcoverstore.hpp" + #include "../mwgui/loadingscreen.hpp" -#include "../mwbase/environment.hpp" +#include "../mwgui/postprocessorhud.hpp" + +#include "../mwmechanics/actorutil.hpp" + #include "../mwbase/windowmanager.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -65,19 +71,188 @@ #include "vismask.hpp" #include "pathgrid.hpp" #include "camera.hpp" -#include "viewovershoulder.hpp" #include "water.hpp" #include "terrainstorage.hpp" -#include "util.hpp" #include "navmesh.hpp" #include "actorspaths.hpp" #include "recastmesh.hpp" #include "fogmanager.hpp" #include "objectpaging.hpp" - +#include "screenshotmanager.hpp" +#include "groundcover.hpp" +#include "postprocessor.hpp" namespace MWRender { + class PerViewUniformStateUpdater final : public SceneUtil::StateSetUpdater + { + public: + PerViewUniformStateUpdater(Resource::SceneManager* sceneManager) + : mSceneManager(sceneManager) + { + mOpaqueTextureUnit = mSceneManager->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::OpaqueDepthTexture); + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("projectionMatrix", osg::Matrixf{})); + if (mSkyRTT) + stateset->addUniform(new osg::Uniform("sky", mSkyTextureUnit)); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(mProjectionMatrix); + if (mSkyRTT && nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + osg::Texture* skyTexture = mSkyRTT->getColorTexture(static_cast(nv)); + stateset->setTextureAttribute(mSkyTextureUnit, skyTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + + stateset->setTextureAttribute(mOpaqueTextureUnit, mSceneManager->getOpaqueDepthTex(nv->getTraversalNumber()), osg::StateAttribute::ON); + } + + void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(Stereo::Manager::instance().computeEyeViewOffset(0) * Stereo::Manager::instance().computeEyeProjection(0, SceneUtil::AutoDepth::isReversed())); + } + + void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(Stereo::Manager::instance().computeEyeViewOffset(1) * Stereo::Manager::instance().computeEyeProjection(1, SceneUtil::AutoDepth::isReversed())); + } + + void setProjectionMatrix(const osg::Matrixf& projectionMatrix) + { + mProjectionMatrix = projectionMatrix; + } + + const osg::Matrixf& getProjectionMatrix() const + { + return mProjectionMatrix; + } + + void enableSkyRTT(int skyTextureUnit, SceneUtil::RTTNode* skyRTT) + { + mSkyTextureUnit = skyTextureUnit; + mSkyRTT = skyRTT; + } + + private: + osg::Matrixf mProjectionMatrix; + int mSkyTextureUnit = -1; + SceneUtil::RTTNode* mSkyRTT = nullptr; + + Resource::SceneManager* mSceneManager; + int mOpaqueTextureUnit = -1; + }; + + class SharedUniformStateUpdater : public SceneUtil::StateSetUpdater + { + public: + SharedUniformStateUpdater(bool usePlayerUniforms) + : mLinearFac(0.f) + , mNear(0.f) + , mFar(0.f) + , mUsePlayerUniforms(usePlayerUniforms) + , mWindSpeed(0.f) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("linearFac", 0.f)); + stateset->addUniform(new osg::Uniform("near", 0.f)); + stateset->addUniform(new osg::Uniform("far", 0.f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 0.f)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{})); + if (mUsePlayerUniforms) + { + stateset->addUniform(new osg::Uniform("windSpeed", 0.0f)); + stateset->addUniform(new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f))); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + auto* uLinearFac = stateset->getUniform("linearFac"); + if (uLinearFac) + uLinearFac->set(mLinearFac); + + auto* uNear = stateset->getUniform("near"); + if (uNear) + uNear->set(mNear); + + auto* uFar = stateset->getUniform("far"); + if (uFar) + uFar->set(mFar); + + static const float mSkyBlendingStartCoef = Settings::Manager::getFloat("sky blending start", "Fog"); + auto* uSkyBlendingStart = stateset->getUniform("skyBlendingStart"); + if (uSkyBlendingStart) + uSkyBlendingStart->set(mFar * mSkyBlendingStartCoef); + + auto* uScreenRes = stateset->getUniform("screenRes"); + if (uScreenRes) + uScreenRes->set(mScreenRes); + + if (mUsePlayerUniforms) + { + auto* windSpeed = stateset->getUniform("windSpeed"); + if (windSpeed) + windSpeed->set(mWindSpeed); + + auto* playerPos = stateset->getUniform("playerPos"); + if (playerPos) + playerPos->set(mPlayerPos); + } + } + + void setLinearFac(float linearFac) + { + mLinearFac = linearFac; + } + + void setNear(float near) + { + mNear = near; + } + + void setFar(float far) + { + mFar = far; + } + + void setScreenRes(float width, float height) + { + mScreenRes = osg::Vec2f(width, height); + } + + void setWindSpeed(float windSpeed) + { + mWindSpeed = windSpeed; + } + + void setPlayerPos(osg::Vec3f playerPos) + { + mPlayerPos = playerPos; + } + + private: + float mLinearFac; + float mNear; + float mFar; + bool mUsePlayerUniforms; + float mWindSpeed; + osg::Vec3f mPlayerPos; + osg::Vec2f mScreenRes; + }; class StateUpdater : public SceneUtil::StateSetUpdater { @@ -171,7 +346,7 @@ namespace MWRender try { for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) - mResourceSystem->getSceneManager()->cacheInstance(*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) @@ -193,22 +368,40 @@ namespace MWRender RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator) - : mViewer(viewer) + const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore) + : mSkyBlending(Settings::Manager::getBool("sky blending", "Fog")) + , mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) - , mUnrefQueue(new SceneUtil::UnrefQueue) , 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 + , mNearClip(std::max(0.005f, Settings::Manager::getFloat("near clip", "Camera"))) + , mViewDistance(Settings::Manager::getFloat("viewing distance", "Camera")) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) + , mFieldOfView(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f)) + , mFirstPersonFieldOfView(std::clamp(Settings::Manager::getFloat("first person field of view", "Camera"), 1.f, 179.f)) { + bool reverseZ = SceneUtil::AutoDepth::isReversed(); + auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); + resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); - resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); - // Shadows and radial fog have problems with fixed-function mode - bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows"); + // Shadows and radial fog have problems with fixed-function mode. + bool forceShaders = Settings::Manager::getBool("radial fog", "Fog") + || Settings::Manager::getBool("exponential fog", "Fog") + || 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); + // 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")); @@ -217,8 +410,14 @@ namespace MWRender resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders")); resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders")); 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); + + // 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); + resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); - osg::ref_ptr sceneRoot = new SceneUtil::LightManager; sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); @@ -230,16 +429,17 @@ namespace MWRender shadowCastingTraversalMask |= Mask_Actor; if (Settings::Manager::getBool("player shadows", "Shadows")) shadowCastingTraversalMask |= Mask_Player; - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - shadowCastingTraversalMask |= Mask_Terrain; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; if (Settings::Manager::getBool("object shadows", "Shadows")) shadowCastingTraversalMask |= (Mask_Object|Mask_Static); + if (Settings::Manager::getBool("terrain shadows", "Shadows")) + shadowCastingTraversalMask |= Mask_Terrain; - mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); + mShadowManager = std::make_unique(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, Mask_Terrain|Mask_Object|Mask_Static, mResourceSystem->getSceneManager()->getShaderManager()); Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) @@ -248,17 +448,38 @@ namespace MWRender 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["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; + bool exponentialFog = Settings::Manager::getBool("exponential fog", "Fog"); + globalDefines["radialFog"] = (exponentialFog || Settings::Manager::getBool("radial fog", "Fog")) ? "1" : "0"; + globalDefines["exponentialFog"] = exponentialFog ? "1" : "0"; + globalDefines["skyBlending"] = mSkyBlending ? "1" : "0"; + globalDefines["refraction_enabled"] = "0"; + globalDefines["useGPUShader4"] = "0"; + globalDefines["useOVR_multiview"] = "0"; + globalDefines["numViews"] = "1"; + globalDefines["disableNormals"] = "1"; + + for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++) + globalDefines[itr->first] = itr->second; + + // Refactor this at some point - most shaders don't care about these defines + float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); + globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f); + globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance); + globalDefines["groundcoverStompMode"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp mode", "Groundcover"), 0, 2)); + globalDefines["groundcoverStompIntensity"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp intensity", "Groundcover"), 0, 2)); + + globalDefines["reverseZ"] = reverseZ ? "1" : "0"; // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); - mNavMesh.reset(new NavMesh(mRootNode, Settings::Manager::getBool("enable nav mesh render", "Navigator"))); - mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator"))); - mRecastMesh.reset(new RecastMesh(mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator"))); - mPathgrid.reset(new Pathgrid(mRootNode)); + 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")); + mPathgrid = std::make_unique(mRootNode); - mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get())); + mObjects = std::make_unique(mResourceSystem, sceneRoot); if (getenv("OPENMW_DONT_PRECOMPILE") == nullptr) { @@ -268,7 +489,7 @@ namespace MWRender mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation()); - mEffectManager.reset(new EffectManager(sceneRoot, mResourceSystem)); + 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"); @@ -276,40 +497,66 @@ namespace MWRender const bool useTerrainNormalMaps = Settings::Manager::getBool("auto use terrain normal maps", "Shaders"); const bool useTerrainSpecularMaps = Settings::Manager::getBool("auto use terrain specular maps", "Shaders"); - mTerrainStorage = new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps); + mTerrainStorage = std::make_unique(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps); + const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); - if (Settings::Manager::getBool("distant terrain", "Terrain")) + bool groundcover = Settings::Manager::getBool("enabled", "Groundcover"); + bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); + 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 float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); 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); - mTerrain.reset(new Terrain::QuadTreeWorld( - sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug, - compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize)); + bool debugChunks = Settings::Manager::getBool("debug chunks", "Terrain"); + mTerrain = std::make_unique( + sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, + compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks); if (Settings::Manager::getBool("object paging", "Terrain")) { - mObjectPaging.reset(new ObjectPaging(mResourceSystem->getSceneManager())); + mObjectPaging = std::make_unique(mResourceSystem->getSceneManager()); static_cast(mTerrain.get())->addChunkManager(mObjectPaging.get()); mResourceSystem->addResourceManager(mObjectPaging.get()); } } else - mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug)); + mTerrain = std::make_unique(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug); mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); - mTerrain->setWorkQueue(mWorkQueue.get()); + + if (groundcover) + { + float density = Settings::Manager::getFloat("density", "Groundcover"); + density = std::clamp(density, 0.f, 1.f); + + mGroundcover = std::make_unique(mResourceSystem->getSceneManager(), density, groundcoverDistance, groundcoverStore); + static_cast(mTerrain.get())->addChunkManager(mGroundcover.get()); + mResourceSystem->addResourceManager(mGroundcover.get()); + } + + mStateUpdater = new StateUpdater; + sceneRoot->addUpdateCallback(mStateUpdater); + + mSharedUniformStateUpdater = new SharedUniformStateUpdater(groundcover); + rootNode->addUpdateCallback(mSharedUniformStateUpdater); + + mPerViewUniformStateUpdater = new PerViewUniformStateUpdater(mResourceSystem->getSceneManager()); + rootNode->addCullCallback(mPerViewUniformStateUpdater); + + mPostProcessor = new PostProcessor(*this, viewer, mRootNode, resourceSystem->getVFS()); + resourceSystem->getSceneManager()->setOpaqueDepthTex(mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 0), mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 1)); + resourceSystem->getSceneManager()->setSoftParticles(mPostProcessor->softParticlesEnabled()); + resourceSystem->getSceneManager()->setSupportsNormalsRT(mPostProcessor->getSupportsNormalsRT()); // water goes after terrain for correct waterculling order - mWater.reset(new Water(mRootNode, sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); + mWater = std::make_unique(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath); - mCamera.reset(new Camera(mViewer->getCamera())); - if (Settings::Manager::getBool("view over shoulder", "Camera")) - mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get())); + mCamera = std::make_unique(mViewer->getCamera()); + + mScreenshotManager = std::make_unique(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get()); mViewer->setLightingMode(osgViewer::View::NO_LIGHT); @@ -321,6 +568,7 @@ namespace MWRender mSunLight->setAmbient(osg::Vec4f(0,0,0,1)); mSunLight->setSpecular(osg::Vec4f(0,0,0,0)); mSunLight->setConstantAttenuation(1.f); + sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); sceneRoot->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); @@ -332,18 +580,22 @@ namespace MWRender defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); - mFog.reset(new FogManager()); + mFog = std::make_unique(); - mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager())); + mSky = std::make_unique(sceneRoot, resourceSystem->getSceneManager(), mSkyBlending); mSky->setCamera(mViewer->getCamera()); - mSky->setRainIntensityUniform(mWater->getRainIntensityUniform()); + if (mSkyBlending) + { + int skyTextureUnit = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); + Log(Debug::Info) << "Reserving texture unit for sky RTT: " << skyTextureUnit; + mPerViewUniformStateUpdater->enableSkyRTT(skyTextureUnit, mSky->getSkyRTT()); + } source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); - mStateUpdater = new StateUpdater; - sceneRoot->addUpdateCallback(mStateUpdater); - osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING; if (!Settings::Manager::getBool("small feature culling", "Camera")) @@ -354,31 +606,37 @@ namespace MWRender cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; } - mViewer->getCamera()->setCullingMode( cullingMode ); - mViewer->getCamera()->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); mViewer->getCamera()->setCullingMode(cullingMode); + mViewer->getCamera()->setName(Constants::SceneCamera); - mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater)); + auto mask = ~(Mask_UpdateVisitor | Mask_SimpleWater); + MWBase::Environment::get().getWindowManager()->setCullMask(mask); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - mNearClip = Settings::Manager::getFloat("near clip", "Camera"); - mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - float fov = Settings::Manager::getFloat("field of view", "Camera"); - mFieldOfView = std::min(std::max(1.f, fov), 179.f); - float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); - mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("near", mNearClip)); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); + // Hopefully, anything genuinely requiring the default alpha func of GL_ALWAYS explicitly sets it + mRootNode->getOrCreateStateSet()->setAttribute(Shader::RemovedAlphaFunc::getInstance(GL_ALWAYS)); + // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and breaks things. + mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF); + + if (reverseZ) + { + osg::ref_ptr clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::ZERO_TO_ONE); + mRootNode->getOrCreateStateSet()->setAttributeAndModes(new SceneUtil::AutoDepth, osg::StateAttribute::ON); + mRootNode->getOrCreateStateSet()->setAttributeAndModes(clipcontrol, osg::StateAttribute::ON); + } + + SceneUtil::setCameraClearDepth(mViewer->getCamera()); + - mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); - mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); updateProjectionMatrix(); + + mViewer->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + mViewer->getUpdateVisitor()->setTraversalMode(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN); } RenderingManager::~RenderingManager() @@ -407,11 +665,6 @@ namespace MWRender return mWorkQueue.get(); } - SceneUtil::UnrefQueue* RenderingManager::getUnrefQueue() - { - return mUnrefQueue.get(); - } - Terrain::World* RenderingManager::getTerrain() { return mTerrain.get(); @@ -423,16 +676,19 @@ namespace MWRender mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); - const char* basemodels[] = {"xbase_anim", "xbase_anim.1st", "xbase_anim_female", "xbase_animkna"}; - for (size_t i=0; imModels.push_back(std::string("meshes/") + basemodels[i] + ".nif"); - workItem->mKeyframes.push_back(std::string("meshes/") + basemodels[i] + ".kf"); - } + 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->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->mTextures.emplace_back("textures/_land_default.dds"); - mWorkQueue->addWorkItem(workItem); + mWorkQueue->addWorkItem(std::move(workItem)); } double RenderingManager::getReferenceTime() const @@ -440,7 +696,7 @@ namespace MWRender return mViewer->getFrameStamp()->getReferenceTime(); } - osg::Group* RenderingManager::getLightRoot() + SceneUtil::LightManager* RenderingManager::getLightRoot() { return mSceneRoot.get(); } @@ -482,7 +738,32 @@ namespace MWRender void RenderingManager::configureAmbient(const ESM::Cell *cell) { - setAmbientColour(SceneUtil::colourFromRGB(cell->mAmbi.mAmbient)); + bool isInterior = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); + bool needsAdjusting = false; + if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) + needsAdjusting = isInterior; + + auto ambient = SceneUtil::colourFromRGB(cell->mAmbi.mAmbient); + + if (needsAdjusting) + { + constexpr float pR = 0.2126; + constexpr float pG = 0.7152; + constexpr float pB = 0.0722; + + // 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) + { + // 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()); + else + ambient *= mMinimumAmbientLuminance / relativeLuminance; + } + } + + setAmbientColour(ambient); osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); mSunLight->setDiffuse(diffuse); @@ -490,11 +771,14 @@ namespace MWRender mSunLight->setPosition(osg::Vec4f(-0.15f, 0.15f, 1.f, 0.f)); } - void RenderingManager::setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular) + void RenderingManager::setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis) { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); mSunLight->setSpecular(specular); + + mPostProcessor->getStateUpdater()->setSunColor(diffuse); + mPostProcessor->getStateUpdater()->setSunVis(sunVis); } void RenderingManager::setSunDirection(const osg::Vec3f &direction) @@ -504,6 +788,8 @@ namespace MWRender mSunLight->setPosition(osg::Vec4(position.x(), position.y(), position.z(), 0)); mSky->setSunDirection(position); + + mPostProcessor->getStateUpdater()->setSunPos(mSunLight->getPosition(), mNight); } void RenderingManager::addCell(const MWWorld::CellStore *store) @@ -513,7 +799,9 @@ namespace MWRender mWater->changeCell(store); if (store->getCell()->isExterior()) + { mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); + } } void RenderingManager::removeCell(const MWWorld::CellStore *store) { @@ -522,7 +810,9 @@ namespace MWRender mObjects->removeCell(store); if (store->getCell()->isExterior()) + { mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); + } mWater->removeCell(store); } @@ -541,6 +831,7 @@ namespace MWRender mShadowManager->enableOutdoorMode(); else mShadowManager->enableIndoorMode(); + mPostProcessor->getStateUpdater()->setIsInterior(!enabled); } bool RenderingManager::toggleBorders() @@ -566,14 +857,15 @@ namespace MWRender } else if (mode == Render_Scene) { - int mask = mViewer->getCamera()->getCullMask(); - bool enabled = mask&Mask_Scene; - enabled = !enabled; + const auto wm = MWBase::Environment::get().getWindowManager(); + unsigned int mask = wm->getCullMask(); + bool enabled = !(mask&sToggleWorldMask); if (enabled) - mask |= Mask_Scene; + mask |= sToggleWorldMask; else - mask &= ~Mask_Scene; - mViewer->getCamera()->setCullMask(mask); + mask &= ~sToggleWorldMask; + mWater->showWorld(enabled); + wm->setCullMask(mask); return enabled; } else if (mode == Render_NavMesh) @@ -610,30 +902,56 @@ namespace MWRender { reportStats(); - mUnrefQueue->flush(mWorkQueue.get()); + float rainIntensity = mSky->getPrecipitationAlpha(); + mWater->setRainIntensity(rainIntensity); if (!paused) { mEffectManager->update(dt); mSky->update(dt); mWater->update(dt); + + const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); + osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + + float windSpeed = mSky->getBaseWindSpeed(); + mSharedUniformStateUpdater->setWindSpeed(windSpeed); + mSharedUniformStateUpdater->setPlayerPos(playerPos); } updateNavMesh(); updateRecastMesh(); - if (mViewOverShoulderController) - mViewOverShoulderController->update(); + if (mUpdateProjectionMatrix) + { + mUpdateProjectionMatrix = false; + updateProjectionMatrix(); + } mCamera->update(dt, paused); - osg::Vec3d focal, cameraPos; - mCamera->getPosition(focal, cameraPos); - mCurrentCameraPos = cameraPos; + bool isUnderwater = mWater->isUnderwater(mCamera->getPosition()); + + float fogStart = mFog->getFogStart(isUnderwater); + float fogEnd = mFog->getFogEnd(isUnderwater); + osg::Vec4f fogColor = mFog->getFogColor(isUnderwater); + + mStateUpdater->setFogStart(fogStart); + mStateUpdater->setFogEnd(fogEnd); + setFogColor(fogColor); - bool isUnderwater = mWater->isUnderwater(cameraPos); - mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater)); - mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater)); - setFogColor(mFog->getFogColor(isUnderwater)); + auto world = MWBase::Environment::get().getWorld(); + const auto& stateUpdater = mPostProcessor->getStateUpdater(); + + stateUpdater->setFogRange(fogStart, fogEnd); + stateUpdater->setNearFar(mNearClip, mViewDistance); + stateUpdater->setIsUnderwater(isUnderwater); + stateUpdater->setFogColor(fogColor); + stateUpdater->setGameHour(world->getTimeStamp().getHour()); + stateUpdater->setWeatherId(world->getCurrentWeather()); + stateUpdater->setNextWeatherId(world->getNextWeather()); + stateUpdater->setWeatherTransition(world->getWeatherTransition()); + stateUpdater->setWindSpeed(world->getWindSpeed()); + mPostProcessor->setUnderwaterFlag(isUnderwater); } void RenderingManager::updatePlayerPtr(const MWWorld::Ptr &ptr) @@ -693,300 +1011,28 @@ namespace MWRender mWater->setCullCallback(mTerrain->getHeightCullCallback(height, Mask_Water)); mWater->setHeight(height); mSky->setWaterHeight(height); + + mPostProcessor->getStateUpdater()->setWaterHeight(height); } - class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback + void RenderingManager::screenshot(osg::Image* image, int w, int h) { - public: - NotifyDrawCompletedCallback(unsigned int frame) - : mDone(false), mFrame(frame) - { - } - - void operator () (osg::RenderInfo& renderInfo) const override - { - std::lock_guard lock(mMutex); - if (renderInfo.getState()->getFrameStamp()->getFrameNumber() >= mFrame) - { - mDone = true; - mCondition.notify_one(); - } - } - - void waitTillDone() - { - std::unique_lock lock(mMutex); - if (mDone) - return; - mCondition.wait(lock); - } - - mutable std::condition_variable mCondition; - mutable std::mutex mMutex; - mutable bool mDone; - unsigned int mFrame; - }; + mScreenshotManager->screenshot(image, w, h); + } - bool RenderingManager::screenshot360(osg::Image* image, std::string settingStr) + bool RenderingManager::screenshot360(osg::Image* image) { - int screenshotW = mViewer->getCamera()->getViewport()->width(); - int screenshotH = mViewer->getCamera()->getViewport()->height(); - int screenshotMapping = 0; - - std::vector settingArgs; - Misc::StringUtils::split(settingStr, settingArgs); - - if (settingArgs.size() > 0) - { - std::string typeStrings[4] = {"spherical","cylindrical","planet","cubemap"}; - bool found = false; - - for (int i = 0; i < 4; ++i) - if (settingArgs[0].compare(typeStrings[i]) == 0) - { - screenshotMapping = i; - found = true; - break; - } - - if (!found) - { - Log(Debug::Warning) << "Wrong screenshot type: " << settingArgs[0] << "."; - return false; - } - } - - // planet mapping needs higher resolution - int cubeSize = screenshotMapping == 2 ? screenshotW : screenshotW / 2; - - if (settingArgs.size() > 1) - screenshotW = std::min(10000,std::atoi(settingArgs[1].c_str())); - - if (settingArgs.size() > 2) - screenshotH = std::min(10000,std::atoi(settingArgs[2].c_str())); - - if (settingArgs.size() > 3) - cubeSize = std::min(5000,std::atoi(settingArgs[3].c_str())); - if (mCamera->isVanityOrPreviewModeEnabled()) { Log(Debug::Warning) << "Spherical screenshots are not allowed in preview mode."; return false; } - bool rawCubemap = screenshotMapping == 3; - - if (rawCubemap) - screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row - else if (screenshotMapping == 2) - screenshotH = screenshotW; // use square resolution for planet mapping - - std::vector> images; - - 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}; - - double fovBackup = mFieldOfView; - mFieldOfView = 90.0; // each cubemap side sees 90 degrees - - int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); - - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(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(); - screenshot(sideImage,cubeSize,cubeSize,transform); - - if (!rawCubemap) - sideImage->flipHorizontal(); - } - - mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); - mFieldOfView = fovBackup; - - 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))); - - std::map defineMap; - - Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr fragmentShader (shaderMgr.getShader("s360_fragment.glsl",defineMap,osg::Shader::FRAGMENT)); - osg::ref_ptr vertexShader (shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr stateset = new osg::StateSet; - - osg::ref_ptr program (new osg::Program); - program->addShader(fragmentShader); - program->addShader(vertexShader); - stateset->setAttributeAndModes(program, osg::StateAttribute::ON); - - stateset->addUniform(new osg::Uniform("cubeMap",0)); - stateset->addUniform(new osg::Uniform("mapping",screenshotMapping)); - stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); - - quad->setStateSet(stateset); - quad->setUpdateCallback(nullptr); - - screenshotCamera->addChild(quad); - - renderCameraToImage(screenshotCamera,image,screenshotW,screenshotH); + mScreenshotManager->screenshot360(image); return true; } - void RenderingManager::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); - - 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); - camera->attach(osg::Camera::COLOR_BUFFER,texture); - - image->setDataType(GL_UNSIGNED_BYTE); - image->setPixelFormat(texture->getInternalFormat()); - - mRootNode->addChild(camera); - - // The draw needs to complete before we can copy back our image. - osg::ref_ptr callback (new NotifyDrawCompletedCallback(0)); - camera->setFinalDrawCallback(callback); - - MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); - - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); - callback->waitTillDone(); - - 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); - } - - class ReadImageFromFramebufferCallback : public osg::Drawable::DrawCallback - { - public: - ReadImageFromFramebufferCallback(osg::Image* image, int width, int height) - : mWidth(width), mHeight(height), mImage(image) - { - } - void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* /*drawable*/) const override - { - int screenW = renderInfo.getCurrentCamera()->getViewport()->width(); - int screenH = renderInfo.getCurrentCamera()->getViewport()->height(); - double imageaspect = (double)mWidth/(double)mHeight; - int leftPadding = std::max(0, static_cast(screenW - screenH * imageaspect) / 2); - int topPadding = std::max(0, static_cast(screenH - screenW / imageaspect) / 2); - int width = screenW - leftPadding*2; - int height = screenH - topPadding*2; - mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); - mImage->scaleImage(mWidth, mHeight, 1); - } - private: - int mWidth; - int mHeight; - osg::ref_ptr mImage; - }; - - void RenderingManager::screenshotFramebuffer(osg::Image* image, int w, int h) - { - osg::Camera* camera = mViewer->getCamera(); - 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); - osg::ref_ptr callback (new NotifyDrawCompletedCallback(mViewer->getFrameStamp()->getFrameNumber())); - camera->setFinalDrawCallback(callback); - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); - callback->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); - camera->setFinalDrawCallback(nullptr); - } - - void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) - { - osg::ref_ptr rttCamera (new osg::Camera); - rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); - - rttCamera->setUpdateCallback(new NoTraverseCallback); - rttCamera->addChild(mSceneRoot); - - rttCamera->addChild(mWater->getReflectionCamera()); - rttCamera->addChild(mWater->getRefractionCamera()); - - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); - - rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - renderCameraToImage(rttCamera.get(),image,w,h); - } - osg::Vec4f RenderingManager::getScreenBounds(const osg::BoundingBox &worldbb) { if (!worldbb.valid()) return osg::Vec4f(); @@ -1020,7 +1066,7 @@ namespace MWRender { RenderingManager::RayResult result; result.mHit = false; - result.mHitRefnum.mContentFile = -1; + result.mHitRefnum.unset(); result.mRatio = 0; if (intersector->containsIntersections()) { @@ -1076,8 +1122,8 @@ namespace MWRender mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); mIntersectionVisitor->setIntersector(intersector); - int mask = ~0; - mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater); + unsigned int mask = ~0u; + mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); if (ignorePlayer) mask &= ~(Mask_Player); if (ignoreActors) @@ -1159,6 +1205,11 @@ namespace MWRender return mObjects->getAnimation(ptr); } + PostProcessor* RenderingManager::getPostProcessor() + { + return mPostProcessor; + } + void RenderingManager::setupPlayer(const MWWorld::Ptr &player) { if (!mPlayerNode) @@ -1222,20 +1273,52 @@ namespace MWRender void RenderingManager::updateProjectionMatrix() { - double aspect = mViewer->getCamera()->getViewport()->aspectRatio(); + double width = Settings::Manager::getInt("resolution x", "Video"); + double height = Settings::Manager::getInt("resolution y", "Video"); + + double aspect = (height == 0.0) ? 1.0 : width / height; float fov = mFieldOfView; if (mFieldOfViewOverridden) fov = mFieldOfViewOverride; + mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance); - mUniformNear->set(mNearClip); - mUniformFar->set(mViewDistance); + if (SceneUtil::AutoDepth::isReversed()) + { + mSharedUniformStateUpdater->setLinearFac(-mNearClip / (mViewDistance - mNearClip) - 1.f); + mPerViewUniformStateUpdater->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance)); + } + else + mPerViewUniformStateUpdater->setProjectionMatrix(mViewer->getCamera()->getProjectionMatrix()); + + mSharedUniformStateUpdater->setNear(mNearClip); + mSharedUniformStateUpdater->setFar(mViewDistance); + if (Stereo::getStereo()) + { + auto res = Stereo::Manager::instance().eyeResolution(); + mSharedUniformStateUpdater->setScreenRes(res.x(), res.y()); + Stereo::Manager::instance().setMasterProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); + } + else if (!mPostProcessor->isEnabled()) + { + mSharedUniformStateUpdater->setScreenRes(width, height); + } // Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may disappear. // Limit FOV here just for sure, otherwise viewing distance can be too high. - fov = std::min(mFieldOfView, 140.f); - float distanceMult = std::cos(osg::DegreesToRadians(fov)/2.f); + float distanceMult = std::cos(osg::DegreesToRadians(std::min(fov, 140.f))/2.f); mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f/distanceMult : 1.f)); + + if (mPostProcessor) + { + mPostProcessor->getStateUpdater()->setProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); + mPostProcessor->getStateUpdater()->setFov(fov); + } + } + + void RenderingManager::setScreenRes(int width, int height) + { + mSharedUniformStateUpdater->setScreenRes(width, height); } void RenderingManager::updateTextureFiltering() @@ -1250,6 +1333,7 @@ namespace MWRender ); mTerrain->updateTextureFiltering(); + mWater->processChangedSettings({}); mViewer->startThreading(); } @@ -1277,40 +1361,100 @@ namespace MWRender unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (stats->collectStats("resource")) { - stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getNumItems()); - mTerrain->reportStats(frameNumber, stats); } } void RenderingManager::processChangedSettings(const Settings::CategorySettingVector &changed) { + // Only perform a projection matrix update once if a relevant setting is changed. + bool updateProjection = false; + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Camera" && it->second == "field of view") { mFieldOfView = Settings::Manager::getFloat("field of view", "Camera"); - updateProjectionMatrix(); + updateProjection = true; + } + else if (it->first == "Video" && (it->second == "resolution x" || it->second == "resolution y")) + { + updateProjection = true; } else if (it->first == "Camera" && it->second == "viewing distance") { - mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - if(!Settings::Manager::getBool("use distant fog", "Fog")) - mStateUpdater->setFogEnd(mViewDistance); - updateProjectionMatrix(); + setViewDistance(Settings::Manager::getFloat("viewing distance", "Camera")); } else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) + { updateTextureFiltering(); + } else if (it->first == "Water") + { mWater->processChangedSettings(changed); + } + 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 == "light bounds multiplier" || + it->second == "maximum light distance" || + it->second == "light fade start" || + it->second == "max lights")) + { + auto* lightManager = getLightRoot(); + lightManager->processChangedSettings(changed); + + if (it->second == "max lights" && !lightManager->usingFFP()) + { + mViewer->stopThreading(); + + lightManager->updateMaxLights(); + + auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + for (const auto& [name, key] : lightManager->getLightDefines()) + defines[name] = key; + mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); + + mStateUpdater->reset(); + + mViewer->startThreading(); + } + } + else if (it->first == "Post Processing" && it->second == "enabled") + { + if (Settings::Manager::getBool("enabled", "Post Processing")) + mPostProcessor->enable(); + else + { + mPostProcessor->disable(); + if (auto* hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) + hud->setVisible(false); + } + } + } + + if (updateProjection) + { + updateProjectionMatrix(); } } - float RenderingManager::getNearClipDistance() const + void RenderingManager::setViewDistance(float distance, bool delay) { - return mNearClip; + mViewDistance = distance; + + if (delay) + { + mUpdateProjectionMatrix = true; + return; + } + + updateProjectionMatrix(); } float RenderingManager::getTerrainHeightAt(const osg::Vec3f &pos) @@ -1328,6 +1472,17 @@ namespace MWRender } } + void RenderingManager::setFieldOfView(float val) + { + mFieldOfView = val; + mUpdateProjectionMatrix = true; + } + + float RenderingManager::getFieldOfView() const + { + return mFieldOfViewOverridden ? mFieldOfViewOverridden : mFieldOfView; + } + osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const { osg::Vec3f halfExtents(0, 0, 0); @@ -1375,9 +1530,9 @@ namespace MWRender } void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const { - mActorsPaths->update(actor, path, halfExtents, start, end, mNavigator.getSettings()); + mActorsPaths->update(actor, path, agentBounds, start, end, mNavigator.getSettings()); } void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const @@ -1408,9 +1563,7 @@ namespace MWRender { try { - const auto locked = it->second->lockConst(); - mNavMesh->update(locked->getImpl(), mNavMeshNumber, locked->getGeneration(), - locked->getNavMeshRevision(), mNavigator.getSettings()); + mNavMesh->update(it->second, mNavMeshNumber, mNavigator.getSettings()); } catch (const std::exception& e) { @@ -1465,4 +1618,9 @@ namespace MWRender if (mObjectPaging) mObjectPaging->getPagedRefnums(activeGrid, out); } + + void RenderingManager::setNavMeshMode(NavMeshMode value) + { + mNavMesh->setMode(value); + } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index ef28cf544e..0424d20b23 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -10,7 +10,7 @@ #include #include "objects.hpp" - +#include "navmeshmode.hpp" #include "renderinginterface.hpp" #include "rendermode.hpp" @@ -59,27 +59,34 @@ namespace SceneUtil { class ShadowManager; class WorkQueue; - class UnrefQueue; + class LightManager; } namespace DetourNavigator { struct Navigator; struct Settings; + struct AgentBounds; } -namespace MWRender +namespace MWWorld { + class GroundcoverStore; +} +namespace MWRender +{ class StateUpdater; + class SharedUniformStateUpdater; + class PerViewUniformStateUpdater; class EffectManager; + class ScreenshotManager; class FogManager; class SkyManager; class NpcAnimation; class Pathgrid; class Camera; - class ViewOverShoulderController; class Water; class TerrainStorage; class LandManager; @@ -87,13 +94,15 @@ namespace MWRender class ActorsPaths; class RecastMesh; class ObjectPaging; + class Groundcover; + class PostProcessor; class RenderingManager : public MWRender::RenderingInterface { public: - RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator); + const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore); ~RenderingManager(); osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation(); @@ -103,17 +112,13 @@ namespace MWRender Resource::ResourceSystem* getResourceSystem(); SceneUtil::WorkQueue* getWorkQueue(); - SceneUtil::UnrefQueue* getUnrefQueue(); Terrain::World* getTerrain(); - osg::Uniform* mUniformNear; - osg::Uniform* mUniformFar; - void preloadCommonAssets(); double getReferenceTime() const; - osg::Group* getLightRoot(); + SceneUtil::LightManager* getLightRoot(); void setNightEyeFactor(float factor); @@ -125,7 +130,8 @@ namespace MWRender void skySetMoonColour(bool red); void setSunDirection(const osg::Vec3f& direction); - void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular); + void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis); + void setNight(bool isNight) { mNight = isNight; } void configureAmbient(const ESM::Cell* cell); void configureFog(const ESM::Cell* cell); @@ -148,9 +154,8 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); // make a new render at given size - void screenshotFramebuffer(osg::Image* image, int w, int h); // copy directly from framebuffer and scale to given size - bool screenshot360(osg::Image* image, std::string settingStr); + void screenshot(osg::Image* image, int w, int h); + bool screenshot360(osg::Image* image); struct RayResult { @@ -190,6 +195,8 @@ namespace MWRender Animation* getAnimation(const MWWorld::Ptr& ptr); const Animation* getAnimation(const MWWorld::ConstPtr& ptr) const; + PostProcessor* getPostProcessor(); + void addWaterRippleEmitter(const MWWorld::Ptr& ptr); void removeWaterRippleEmitter(const MWWorld::Ptr& ptr); void emitWaterRipple(const osg::Vec3f& pos); @@ -204,16 +211,20 @@ namespace MWRender void processChangedSettings(const Settings::CategorySettingVector& settings); - float getNearClipDistance() const; + float getNearClipDistance() const { return mNearClip; } + float getViewDistance() const { return mViewDistance; } + + void setViewDistance(float distance, bool delay = false); float getTerrainHeightAt(const osg::Vec3f& pos); // camera stuff Camera* getCamera() { return mCamera.get(); } - const osg::Vec3f& getCameraPosition() const { return mCurrentCameraPos; } /// temporarily override the field of view with given value. void overrideFieldOfView(float val); + void setFieldOfView(float val); + float getFieldOfView() const; /// reset a previous overrideFieldOfView() call, i.e. revert to field of view specified in the settings file. void resetFieldOfView(); @@ -226,7 +237,7 @@ namespace MWRender bool toggleBorders(); void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const; + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const; void removeActorPath(const MWWorld::ConstPtr& actor) const; @@ -239,8 +250,13 @@ namespace MWRender bool pagingUnlockCache(); void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); - private: void updateProjectionMatrix(); + + void setScreenRes(int width, int height); + + void setNavMeshMode(NavMeshMode value); + + private: void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); @@ -248,23 +264,22 @@ namespace MWRender void reportStats() const; - void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); - void updateNavMesh(); void updateRecastMesh(); + const bool mSkyBlending; + osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); osg::ref_ptr mIntersectionVisitor; osg::ref_ptr mViewer; osg::ref_ptr mRootNode; - osg::ref_ptr mSceneRoot; + osg::ref_ptr mSceneRoot; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; osg::ref_ptr mSunLight; @@ -277,21 +292,25 @@ namespace MWRender std::unique_ptr mObjects; std::unique_ptr mWater; std::unique_ptr mTerrain; - TerrainStorage* mTerrainStorage; + std::unique_ptr mTerrainStorage; std::unique_ptr mObjectPaging; + std::unique_ptr mGroundcover; std::unique_ptr mSky; std::unique_ptr mFog; + std::unique_ptr mScreenshotManager; std::unique_ptr mEffectManager; std::unique_ptr mShadowManager; + osg::ref_ptr mPostProcessor; osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; - std::unique_ptr mViewOverShoulderController; - osg::Vec3f mCurrentCameraPos; osg::ref_ptr mStateUpdater; + osg::ref_ptr mSharedUniformStateUpdater; + osg::ref_ptr mPerViewUniformStateUpdater; osg::Vec4f mAmbientColor; + float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; @@ -300,6 +319,8 @@ namespace MWRender float mFieldOfViewOverride; float mFieldOfView; float mFirstPersonFieldOfView; + bool mUpdateProjectionMatrix = false; + bool mNight = false; void operator = (const RenderingManager&); RenderingManager(const RenderingManager&); diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index 6788f53f44..037ed4455f 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -1,6 +1,7 @@ #include "ripplesimulation.hpp" #include +#include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include "vismask.hpp" @@ -47,7 +49,7 @@ namespace } osg::ref_ptr controller (new NifOsg::FlipController(0, 0.3f/rippleFrameCount, textures)); - controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); + controller->setSource(std::make_shared()); node->addUpdateCallback(controller); osg::ref_ptr stateset (new osg::StateSet); @@ -55,13 +57,13 @@ namespace stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); - osg::ref_ptr depth (new osg::Depth); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); osg::ref_ptr polygonOffset (new osg::PolygonOffset); - polygonOffset->setUnits(-1); - polygonOffset->setFactor(-1); + polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1 : -1); + polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 1 : -1); stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); diff --git a/apps/openmw/mwrender/rotatecontroller.cpp b/apps/openmw/mwrender/rotatecontroller.cpp index 534cc74906..d3df4364d0 100644 --- a/apps/openmw/mwrender/rotatecontroller.cpp +++ b/apps/openmw/mwrender/rotatecontroller.cpp @@ -22,21 +22,27 @@ void RotateController::setRotate(const osg::Quat &rotate) mRotate = rotate; } -void RotateController::operator()(osg::Node *node, osg::NodeVisitor *nv) +void RotateController::setOffset(const osg::Vec3f& offset) +{ + mOffset = offset; +} + +void RotateController::operator()(osg::MatrixTransform *node, osg::NodeVisitor *nv) { if (!mEnabled) { traverse(node, nv); return; } - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); + osg::Matrix matrix = node->getMatrix(); osg::Quat worldOrient = getWorldOrientation(node); + osg::Quat worldOrientInverse = worldOrient.inverse(); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); + osg::Quat orient = worldOrient * mRotate * worldOrientInverse * matrix.getRotate(); matrix.setRotate(orient); + matrix.setTrans(matrix.getTrans() + worldOrientInverse * mOffset); - transform->setMatrix(matrix); + node->setMatrix(matrix); traverse(node,nv); } diff --git a/apps/openmw/mwrender/rotatecontroller.hpp b/apps/openmw/mwrender/rotatecontroller.hpp index 9d4080ac6e..1f3ee0f845 100644 --- a/apps/openmw/mwrender/rotatecontroller.hpp +++ b/apps/openmw/mwrender/rotatecontroller.hpp @@ -1,31 +1,36 @@ #ifndef OPENMW_MWRENDER_ROTATECONTROLLER_H #define OPENMW_MWRENDER_ROTATECONTROLLER_H -#include +#include #include +namespace osg +{ + class MatrixTransform; +} + namespace MWRender { /// Applies a rotation in \a relativeTo's space. /// @note Assumes that the node being rotated has its "original" orientation set every frame by a different controller. /// The rotation is then applied on top of that orientation. -/// @note Must be set on a MatrixTransform. -class RotateController : public osg::NodeCallback +class RotateController : public SceneUtil::NodeCallback { public: RotateController(osg::Node* relativeTo); void setEnabled(bool enabled); - + void setOffset(const osg::Vec3f& offset); void setRotate(const osg::Quat& rotate); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); protected: osg::Quat getWorldOrientation(osg::Node* node); bool mEnabled; + osg::Vec3f mOffset; osg::Quat mRotate; osg::Node* mRelativeTo; }; diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp new file mode 100644 index 0000000000..40665cc250 --- /dev/null +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -0,0 +1,366 @@ +#include "screenshotmanager.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../mwgui/loadingscreen.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "util.hpp" +#include "vismask.hpp" +#include "water.hpp" +#include "postprocessor.hpp" + +namespace MWRender +{ + enum Screenshot360Type + { + Spherical, + Cylindrical, + Planet, + RawCubemap + }; + + class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback + { + public: + NotifyDrawCompletedCallback() + : mDone(false), mFrame(0) + { + } + + void operator () (osg::RenderInfo& renderInfo) const override + { + std::lock_guard lock(mMutex); + if (renderInfo.getState()->getFrameStamp()->getFrameNumber() >= mFrame && !mDone) + { + mDone = true; + mCondition.notify_one(); + } + } + + void waitTillDone() + { + std::unique_lock lock(mMutex); + if (mDone) + return; + mCondition.wait(lock); + } + + void reset(unsigned int frame) + { + std::lock_guard lock(mMutex); + mDone = false; + mFrame = frame; + } + + mutable std::condition_variable mCondition; + mutable std::mutex mMutex; + mutable bool mDone; + unsigned int mFrame; + }; + + class ReadImageFromFramebufferCallback : public osg::Drawable::DrawCallback + { + public: + ReadImageFromFramebufferCallback(osg::Image* image, int width, int height) + : mWidth(width), mHeight(height), mImage(image) + { + } + void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* /*drawable*/) const override + { + int screenW = renderInfo.getCurrentCamera()->getViewport()->width(); + int screenH = renderInfo.getCurrentCamera()->getViewport()->height(); + if (Stereo::getStereo()) + { + auto eyeRes = Stereo::Manager::instance().eyeResolution(); + screenW = eyeRes.x(); + screenH = eyeRes.y(); + } + double imageaspect = (double)mWidth/(double)mHeight; + int leftPadding = std::max(0, static_cast(screenW - screenH * imageaspect) / 2); + int topPadding = std::max(0, static_cast(screenH - screenW / imageaspect) / 2); + 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); + } + private: + int mWidth; + int mHeight; + osg::ref_ptr mImage; + }; + + ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water) + : mViewer(viewer) + , mRootNode(rootNode) + , mSceneRoot(sceneRoot) + , mDrawCompleteCallback(new NotifyDrawCompletedCallback) + , mResourceSystem(resourceSystem) + , mWater(water) + { + } + + ScreenshotManager::~ScreenshotManager() + { + } + + void ScreenshotManager::screenshot(osg::Image* image, int w, int h) + { + osg::Camera* camera = mViewer->getCamera(); + 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()); + // 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 typeStrings[4] = {"spherical", "cylindrical", "planet", "cubemap"}; + bool found = false; + + for (int i = 0; i < 4; ++i) + { + if (settingArgs[0].compare(typeStrings[i]) == 0) + { + 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, std::atoi(settingArgs[1].c_str())); + + if (settingArgs.size() > 2) + screenshotH = std::min(10000, std::atoi(settingArgs[2].c_str())); + + if (settingArgs.size() > 3) + cubeSize = std::min(5000, std::atoi(settingArgs[3].c_str())); + + 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))); + + std::map defineMap; + + Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); + osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr stateset = quad->getOrCreateStateSet(); + + osg::ref_ptr program(new osg::Program); + program->addShader(fragmentShader); + program->addShader(vertexShader); + stateset->setAttributeAndModes(program, 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); + 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); + float nearClip = Settings::Manager::getFloat("near clip", "Camera"); + float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); + // 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 new file mode 100644 index 0000000000..094a4a20f4 --- /dev/null +++ b/apps/openmw/mwrender/screenshotmanager.hpp @@ -0,0 +1,44 @@ +#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(); + + 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()); + }; +} + +#endif diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 2920e07dde..c850c1ad8a 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,1319 +1,187 @@ #include "sky.hpp" -#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 #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include + +#include + +#include "../mwworld/weather.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "vismask.hpp" #include "renderbin.hpp" +#include "util.hpp" +#include "skyutil.hpp" namespace { - osg::ref_ptr createAlphaTrackingUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); - return mat; - } - - osg::ref_ptr createUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; - } - - osg::ref_ptr createTexturedQuad(int numUvSets=1) - { - osg::ref_ptr geom = new osg::Geometry; - - osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5, -0.5, 0)); - verts->push_back(osg::Vec3f(-0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, -0.5, 0)); - - geom->setVertexArray(verts); - - osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(0, 0)); - texcoords->push_back(osg::Vec2f(0, 1)); - texcoords->push_back(osg::Vec2f(1, 1)); - texcoords->push_back(osg::Vec2f(1, 0)); - - osg::ref_ptr colors = new osg::Vec4Array; - colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); - geom->setColorArray(colors, osg::Array::BIND_OVERALL); - - for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); - - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); - - return geom; - } - -} - -namespace MWRender -{ - -class AtmosphereUpdater : public SceneUtil::StateSetUpdater -{ -public: - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - } - -private: - osg::Vec4f mEmissionColor; -}; - -class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater -{ -public: - AtmosphereNightUpdater(Resource::ImageManager* imageManager) - { - // we just need a texture, its contents don't really matter - mTexture = new osg::Texture2D(imageManager->getWarningImage()); - } - - void setFade(const float fade) - { - mColor.a() = fade; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr texEnv (new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - - stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mColor); - } - - osg::ref_ptr mTexture; - - osg::Vec4f mColor; -}; - -class CloudUpdater : public SceneUtil::StateSetUpdater -{ -public: - CloudUpdater() - : mAnimationTimer(0.f) - , mOpacity(0.f) - { - } - - void setAnimationTimer(float timer) - { - mAnimationTimer = timer; - } - - void setTexture(osg::ref_ptr texture) - { - mTexture = texture; - } - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - void setOpacity(float opacity) - { - mOpacity = opacity; - } - -protected: - void setDefaults(osg::StateSet *stateset) override - { - osg::ref_ptr texmat (new osg::TexMat); - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); - stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already - osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); - texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); - texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); - - stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override - { - osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); - - stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - - osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); - } - -private: - float mAnimationTimer; - osg::ref_ptr mTexture; - osg::Vec4f mEmissionColor; - float mOpacity; -}; - -/// Transform that removes the eyepoint of the modelview matrix, -/// i.e. its children are positioned relative to the camera. -class CameraRelativeTransform : public osg::Transform -{ -public: - CameraRelativeTransform() - { - // Culling works in node-local space, not in camera space, so we can't cull this node correctly - // That's not a problem though, children of this node can be culled just fine - // Just make sure you do not place a CameraRelativeTransform deep in the scene graph - setCullingActive(false); - - addCullCallback(new CullCallback); - } - - CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) - : osg::Transform(copy, copyop) - { - } - - META_Node(MWRender, CameraRelativeTransform) - - const osg::Vec3f& getLastViewPoint() const - { - return mViewPoint; - } - - bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override - { - if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - mViewPoint = static_cast(nv)->getViewPoint(); - } - - if (_referenceFrame==RELATIVE_RF) - { - matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); - return false; - } - else // absolute - { - matrix.makeIdentity(); - return true; - } - } - - osg::BoundingSphere computeBound() const override - { - return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); - } - - class CullCallback : public osg::NodeCallback - { - public: - void operator() (osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - - // XXX have to remove unwanted culling plane of the water reflection camera - - // Remove all planes that aren't from the standard frustum - unsigned int numPlanes = 4; - if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) - ++numPlanes; - if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) - ++numPlanes; - - int mask = 0x1; - int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); - for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) - { - if (i >= numPlanes) - { - // turn off this culling plane - resultMask &= (~mask); - } - - mask <<= 1; - } - - cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); - cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); - - cv->getProjectionCullingStack().back().pushCurrentMask(); - cv->getCurrentCullingSet().pushCurrentMask(); - - traverse(node, nv); - - cv->getProjectionCullingStack().back().popCurrentMask(); - cv->getCurrentCullingSet().popCurrentMask(); - } - }; -private: - // viewPoint for the current frame - mutable osg::Vec3f mViewPoint; -}; - -class ModVertexAlphaVisitor : public osg::NodeVisitor -{ -public: - ModVertexAlphaVisitor(int meshType) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mMeshType(meshType) - { - } - - void apply(osg::Drawable& drw) override - { - osg::Geometry* geom = drw.asGeometry(); - if (!geom) - return; - - osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); - for (unsigned int i=0; isize(); ++i) - { - float alpha = 1.f; - if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (mMeshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 0.25098; // second row - else alpha = 1.f; - } - else if (mMeshType == 2) - { - if (geom->getColorArray()) - { - osg::Vec4Array* origColors = static_cast(geom->getColorArray()); - alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; - } - else - alpha = 1.f; - } - - (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); - } - - geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - } - -private: - int mMeshType; -}; - -/// @brief Hides the node subgraph if the eye point is below water. -/// @note Must be added as cull callback. -/// @note Meant to be used on a node that is child of a CameraRelativeTransform. -/// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public osg::NodeCallback -{ -public: - UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) - : mCameraRelativeTransform(cameraRelativeTransform) - , mEnabled(true) - , mWaterLevel(0.f) - { - } - - bool isUnderwater() - { - osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); - return mEnabled && viewPoint.z() < mWaterLevel; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - if (isUnderwater()) - return; - - traverse(node, nv); - } - - void setEnabled(bool enabled) - { - mEnabled = enabled; - } - void setWaterLevel(float waterLevel) - { - mWaterLevel = waterLevel; - } - -private: - osg::ref_ptr mCameraRelativeTransform; - bool mEnabled; - float mWaterLevel; -}; - -/// A base class for the sun and moons. -class CelestialBody -{ -public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0) - : mVisibleMask(visibleMask) - { - mGeom = createTexturedQuad(numUvSets); - mTransform = new osg::PositionAttitudeTransform; - mTransform->setNodeMask(mVisibleMask); - mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); - mTransform->addChild(mGeom); - - parentNode->addChild(mTransform); - } - - virtual ~CelestialBody() {} - - virtual void adjustTransparency(const float ratio) = 0; - - void setVisible(bool visible) - { - mTransform->setNodeMask(visible ? mVisibleMask : 0); - } - -protected: - unsigned int mVisibleMask; - static const float mDistance; - osg::ref_ptr mTransform; - osg::ref_ptr mGeom; -}; - -const float CelestialBody::mDistance = 1000.0f; - -class Sun : public CelestialBody -{ -public: - Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) - : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) - , mUpdater(new Updater) - { - mTransform->addUpdateCallback(mUpdater); - - osg::ref_ptr sunTex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"))); - sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - - osg::ref_ptr queryNode (new osg::Group); - // Need to render after the world geometry so we can correctly test for occlusions - osg::StateSet* stateset = queryNode->getOrCreateStateSet(); - stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); - stateset->setNestRenderBins(false); - // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun - osg::ref_ptr alphaFunc (new osg::AlphaFunc); - alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - // 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, osg::StateAttribute::ON); - osg::ref_ptr po (new osg::PolygonOffset( -1., -1. )); - stateset->setAttributeAndModes(po, osg::StateAttribute::ON); - - mTransform->addChild(queryNode); - - mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); - mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); - - createSunFlash(imageManager); - createSunGlare(); - } - - ~Sun() - { - mTransform->removeUpdateCallback(mUpdater); - destroySunFlash(); - destroySunGlare(); - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mColor.r() = color.r(); - mUpdater->mColor.g() = color.g(); - mUpdater->mColor.b() = color.b(); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mColor.a() = ratio; - if (mSunGlareCallback) - mSunGlareCallback->setGlareView(ratio); - if (mSunFlashCallback) - mSunFlashCallback->setGlareView(ratio); - } - - void setDirection(const osg::Vec3f& direction) - { - osg::Vec3f normalizedDirection = direction / direction.length(); - mTransform->setPosition(normalizedDirection * mDistance); - - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); - mTransform->setAttitude(quat); - } - - void setGlareTimeOfDayFade(float val) - { - if (mSunGlareCallback) - mSunGlareCallback->setTimeOfDayFade(val); - } - -private: - class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback - { - public: - osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } - }; - - /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. - osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) - { - osg::ref_ptr oqn = new osg::OcclusionQueryNode; - oqn->setQueriesEnabled(true); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced - osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); -#else - osg::ref_ptr queryGeom = oqn->getQueryGeometry(); -#endif - - // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, - // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry - // is only called once. - // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. - queryGeom->setDataVariance(osg::Object::STATIC); - - // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, - // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to - // circumvent this. - queryGeom->setVertexArray(mGeom->getVertexArray()); - queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); - queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); - queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); - - // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. - oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); - // Still need a proper bounding sphere. - oqn->setInitialBound(queryGeom->getBound()); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - oqn->setQueryGeometry(queryGeom.release()); -#endif - - osg::StateSet* queryStateSet = new osg::StateSet; - if (queryVisible) - { - osg::ref_ptr depth (new osg::Depth); - depth->setFunction(osg::Depth::LESS); - // 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. - depth->setZNear(1.0); - depth->setZFar(1.0); - depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - else - { - queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - oqn->setQueryStateSet(queryStateSet); - - parent->addChild(oqn); - - return oqn; - } - - void createSunFlash(Resource::ImageManager& imageManager) - { - osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - osg::ref_ptr transform (new osg::PositionAttitudeTransform); - const float scale = 2.6f; - transform->setScale(osg::Vec3f(scale,scale,scale)); - - mTransform->addChild(transform); - - osg::ref_ptr geom = createTexturedQuad(); - transform->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - - mSunFlashNode = transform; - - mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); - mSunFlashNode->addCullCallback(mSunFlashCallback); - } - void destroySunFlash() - { - if (mSunFlashNode) - { - mSunFlashNode->removeCullCallback(mSunFlashCallback); - mSunFlashCallback = nullptr; - } - } - - void createSunGlare() - { - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? - camera->setViewMatrix(osg::Matrix::identity()); - camera->setClearMask(0); - camera->setRenderOrder(osg::Camera::NESTED_RENDER); - camera->setAllowEventFocus(false); - - osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); - - camera->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - - // set up additive blending - osg::ref_ptr blendFunc (new osg::BlendFunc); - blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); - blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); - mSunGlareNode = camera; - - mSunGlareNode->addCullCallback(mSunGlareCallback); - - mTransform->addChild(camera); - } - void destroySunGlare() - { - if (mSunGlareNode) - { - mSunGlareNode->removeCullCallback(mSunGlareCallback); - mSunGlareCallback = nullptr; - } - } - - class Updater : public SceneUtil::StateSetUpdater + class WrapAroundOperator : public osgParticle::Operator { public: - osg::Vec4f mColor; - - Updater() - : mColor(1.f, 1.f, 1.f, 1.f) - { - } - - void setDefaults(osg::StateSet* stateset) override + WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange) + : osgParticle::Operator() + , mCamera(camera) + , mWrapRange(wrapRange) + , mHalfWrapRange(mWrapRange / 2.0) { - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); + mPreviousCameraPosition = getCameraPosition(); } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + osg::Object *cloneType() const override { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + return nullptr; } - }; - class OcclusionCallback : public osg::NodeCallback - { - public: - OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : mOcclusionQueryVisiblePixels(oqnVisible) - , mOcclusionQueryTotalPixels(oqnTotal) + osg::Object *clone(const osg::CopyOp &op) const override { + return nullptr; } - protected: - float getVisibleRatio (osg::Camera* camera) + void operate(osgParticle::Particle *P, double dt) override { - int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); - int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); - - float visibleRatio = 0.f; - if (total > 0) - visibleRatio = static_cast(visible) / static_cast(total); - - float dt = MWBase::Environment::get().getFrameDuration(); - - float lastRatio = mLastRatio[osg::observer_ptr(camera)]; - - float change = dt*10; - - if (visibleRatio > lastRatio) - visibleRatio = std::min(visibleRatio, lastRatio + change); - else - visibleRatio = std::max(visibleRatio, lastRatio - change); - - mLastRatio[osg::observer_ptr(camera)] = visibleRatio; - - return visibleRatio; } - private: - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; - - std::map, float> mLastRatio; - }; - - /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback - { - public: - SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : OcclusionCallback(oqnVisible, oqnTotal) - , mGlareView(1.f) + void operateParticles(osgParticle::ParticleSystem *ps, double dt) override { - } + osg::Vec3 position = getCameraPosition(); + osg::Vec3 positionDifference = position - mPreviousCameraPosition; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); + osg::Matrix toWorld, toLocal; - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + std::vector worldMatrices = ps->getWorldMatrices(); - osg::ref_ptr stateset; - if (visibleRatio > 0.f) + if (!worldMatrices.empty()) { - const float fadeThreshold = 0.1; - if (visibleRatio < fadeThreshold) - { - float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (createUnlitMaterial()); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); - stateset = new osg::StateSet; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; + toWorld = worldMatrices[0]; + toLocal.invert(toWorld); } - float scale = visibleRatio; - - if (scale == 0.f) + for (int i = 0; i < ps->numParticles(); ++i) { - // no traverse - return; - } - else - { - osg::Matrix modelView = *cv->getModelViewMatrix(); - - modelView.preMultScale(osg::Vec3f(visibleRatio, visibleRatio, visibleRatio)); - - if (stateset) - cv->pushStateSet(stateset); - - cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - - traverse(node, nv); - - cv->popModelViewMatrix(); - - if (stateset) - cv->popStateSet(); - } - } - - void setGlareView(float value) - { - mGlareView = value; - } - - private: - float mGlareView; - }; - - - /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. - /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback - { - public: - SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, - osg::ref_ptr sunTransform) - : OcclusionCallback(oqnVisible, oqnTotal) - , mSunTransform(sunTransform) - , mTimeOfDayFade(1.f) - , mGlareView(1.f) - { - mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); - mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); - mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); - - // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, - // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, - // so the resulting color looks more orange than red. - mColor *= 2; - for (int i=0; i<3; ++i) - mColor[i] = std::min(1.f, mColor[i]); - } - - void operator ()(osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + osgParticle::Particle *p = ps->getParticle(i); + p->setPosition(toWorld.preMult(p->getPosition())); + p->setPosition(p->getPosition() - positionDifference); - const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); - - float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); - float fade = value * mSunGlareFaderMax; - - fade *= mTimeOfDayFade * mGlareView * visibleRatio; - - if (fade == 0.f) - { - // no traverse - return; - } - else - { - osg::ref_ptr stateset (new osg::StateSet); - - osg::ref_ptr mat (createUnlitMaterial()); + for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions + { + osg::Vec3 pos = p->getPosition(); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + if (pos[j] < -mHalfWrapRange[j]) + pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); + else if (pos[j] > mHalfWrapRange[j]) + pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + p->setPosition(pos); + } - cv->pushStateSet(stateset); - traverse(node, nv); - cv->popStateSet(); + p->setPosition(toLocal.preMult(p->getPosition())); } - } - void setTimeOfDayFade(float val) - { - mTimeOfDayFade = val; + mPreviousCameraPosition = position; } - void setGlareView(float glareView) - { - mGlareView = glareView; - } + protected: + osg::Camera *mCamera; + osg::Vec3 mPreviousCameraPosition; + osg::Vec3 mWrapRange; + osg::Vec3 mHalfWrapRange; - private: - float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + osg::Vec3 getCameraPosition() { - osg::Vec3d eye, center, up; - viewMatrix.getLookAt(eye, center, up); - - osg::Vec3d forward = center - eye; - osg::Vec3d sun = mSunTransform->getPosition(); - - forward.normalize(); - sun.normalize(); - float angleRadians = std::acos(forward * sun); - return angleRadians; + return mCamera->getInverseViewMatrix().getTrans(); } - - osg::ref_ptr mSunTransform; - float mTimeOfDayFade; - float mGlareView; - osg::Vec4f mColor; - float mSunGlareFaderMax; - float mSunGlareFaderAngleMax; }; - osg::ref_ptr mUpdater; - osg::ref_ptr mSunFlashCallback; - osg::ref_ptr mSunFlashNode; - osg::ref_ptr mSunGlareCallback; - osg::ref_ptr mSunGlareNode; - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; -}; - -class Moon : public CelestialBody -{ -public: - enum Type - { - Type_Masser = 0, - Type_Secunda - }; - - Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) - : CelestialBody(parentNode, scaleFactor, 2) - , mType(type) - , mPhase(MoonState::Phase::Unspecified) - , mUpdater(new Updater(imageManager)) - { - setPhase(MoonState::Phase::Full); - setVisible(true); - - mGeom->addUpdateCallback(mUpdater); - } - - ~Moon() - { - mGeom->removeUpdateCallback(mUpdater); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mTransparency *= ratio; - } - - void setState(const MoonState& state) - { - float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; - float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; - - osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); - - osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); - mTransform->setPosition(direction * mDistance); - - // The moon quad is initially oriented facing down, so we need to offset its X-axis - // rotation to rotate it to face the camera when sitting at the horizon. - osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - mTransform->setAttitude(attX * rotZ); - - setPhase(state.mPhase); - mUpdater->mTransparency = state.mMoonAlpha; - mUpdater->mShadowBlend = state.mShadowBlend; - } - - void setAtmosphereColor(const osg::Vec4f& color) + class WeatherAlphaOperator : public osgParticle::Operator { - mUpdater->mAtmosphereColor = color; - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mMoonColor = color; - } - - unsigned int getPhaseInt() const - { - if (mPhase == MoonState::Phase::New) return 0; - else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; - else if (mPhase == MoonState::Phase::WaningCrescent) return 1; - else if (mPhase == MoonState::Phase::FirstQuarter) return 2; - else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; - else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; - else if (mPhase == MoonState::Phase::WaningGibbous) return 3; - else if (mPhase == MoonState::Phase::Full) return 4; - return 0; - } - -private: - struct Updater : public SceneUtil::StateSetUpdater - { - Resource::ImageManager& mImageManager; - osg::ref_ptr mPhaseTex; - osg::ref_ptr mCircleTex; - float mTransparency; - float mShadowBlend; - osg::Vec4f mAtmosphereColor; - osg::Vec4f mMoonColor; - - Updater(Resource::ImageManager& imageManager) - : mImageManager(imageManager) - , mPhaseTex() - , mCircleTex() - , mTransparency(1.0f) - , mShadowBlend(1.0f) - , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) - , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) - { - } + public: + WeatherAlphaOperator(float& alpha, bool rain) + : mAlpha(alpha) + , mIsRain(rain) + { } - void setDefaults(osg::StateSet* stateset) override + osg::Object *cloneType() const override { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); - - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv2 = new osg::TexEnvCombine; - texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); - texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); - texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); - - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + return nullptr; } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + osg::Object *clone(const osg::CopyOp &op) const override { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mMoonColor * mShadowBlend); - - osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + return nullptr; } - void setTextures(const std::string& phaseTex, const std::string& circleTex) + void operate(osgParticle::Particle *particle, double dt) override { - mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); - mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); - mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - reset(); + constexpr float rainThreshold = 0.6f; // Rain_Threshold? + float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; + particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } - }; - - Type mType; - MoonState::Phase mPhase; - osg::ref_ptr mUpdater; - - void setPhase(const MoonState::Phase& phase) - { - if(mPhase == phase) - return; - - mPhase = phase; - - std::string textureName = "textures/tx_"; - - if (mType == Moon::Type_Secunda) - textureName += "secunda_"; - else - textureName += "masser_"; - - if (phase == MoonState::Phase::New) textureName += "new"; - else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; - else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; - else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; - else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; - else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; - else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; - else if(phase == MoonState::Phase::Full) textureName += "full"; - textureName += ".dds"; - - if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); - else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); - } -}; - -SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) - : mSceneManager(sceneManager) - , mCamera(nullptr) - , mRainIntensityUniform(nullptr) - , mAtmosphereNightRoll(0.f) - , mCreated(false) - , mIsStorm(false) - , mDay(0) - , mMonth(0) - , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) - , mStormDirection(0,1,0) - , mClouds() - , mNextClouds() - , mCloudBlendFactor(0.0f) - , mCloudSpeed(0.0f) - , mStarsOpacity(0.0f) - , mRemainingTransitionTime(0.0f) - , mRainEnabled(false) - , mRainSpeed(0) - , mRainDiameter(0) - , mRainMinHeight(0) - , mRainMaxHeight(0) - , mRainEntranceSpeed(1) - , mRainMaxRaindrops(0) - , mWindSpeed(0.f) - , mEnabled(true) - , mSunEnabled(true) - , mWeatherAlpha(0.f) -{ - osg::ref_ptr skyroot (new CameraRelativeTransform); - skyroot->setName("Sky Root"); - // Assign empty program to specify we don't want shaders - // The shaders generated by the SceneManager can't handle everything we need - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - - skyroot->setNodeMask(Mask_Sky); - parentNode->addChild(skyroot); - - mRootNode = skyroot; - - mEarlyRenderBinRoot = new osg::Group; - // render before the world is rendered - mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); - // Prevent unwanted clipping by water reflection camera's clipping plane - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); - mRootNode->addChild(mEarlyRenderBinRoot); - - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); -} - -void SkyManager::setRainIntensityUniform(osg::Uniform *uniform) -{ - mRainIntensityUniform = uniform; -} - -void SkyManager::create() -{ - assert(!mCreated); - - mAtmosphereDay = mSceneManager->getInstance("meshes/sky_atmosphere.nif", mEarlyRenderBinRoot); - ModVertexAlphaVisitor modAtmosphere(0); - mAtmosphereDay->accept(modAtmosphere); - - mAtmosphereUpdater = new AtmosphereUpdater; - mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - - mAtmosphereNightNode = new osg::PositionAttitudeTransform; - mAtmosphereNightNode->setNodeMask(0); - mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - - osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists("meshes/sky_night_02.nif")) - atmosphereNight = mSceneManager->getInstance("meshes/sky_night_02.nif", mAtmosphereNightNode); - else - atmosphereNight = mSceneManager->getInstance("meshes/sky_night_01.nif", mAtmosphereNightNode); - atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - ModVertexAlphaVisitor modStars(2); - atmosphereNight->accept(modStars); - mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); - atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - - mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); - - mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); - mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); - - mCloudNode = new osg::PositionAttitudeTransform; - mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance("meshes/sky_clouds_01.nif", mCloudNode); - ModVertexAlphaVisitor modClouds(1); - mCloudMesh->accept(modClouds); - mCloudUpdater = new CloudUpdater; - mCloudUpdater->setOpacity(1.f); - mCloudMesh->addUpdateCallback(mCloudUpdater); - - mCloudMesh2 = mSceneManager->getInstance("meshes/sky_clouds_01.nif", mCloudNode); - mCloudMesh2->accept(modClouds); - mCloudUpdater2 = new CloudUpdater; - mCloudUpdater2->setOpacity(0.f); - mCloudMesh2->addUpdateCallback(mCloudUpdater2); - mCloudMesh2->setNodeMask(0); - - osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - - mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - - mCreated = true; -} - -class RainCounter : public osgParticle::ConstantRateCounter -{ -public: - int numParticlesToCreate(double dt) const override - { - // limit dt to avoid large particle emissions if there are jumps in the simulation time - // 0.2 seconds is the same cap as used in Engine's frame loop - dt = std::min(dt, 0.2); - return ConstantRateCounter::numParticlesToCreate(dt); - } -}; - -class RainShooter : public osgParticle::Shooter -{ -public: - RainShooter() - : mAngle(0.f) - { - } - - void shoot(osgParticle::Particle* particle) const override - { - particle->setVelocity(mVelocity); - particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); - } - - void setVelocity(const osg::Vec3f& velocity) - { - mVelocity = velocity; - } - - void setAngle(float angle) - { - mAngle = angle; - } - - osg::Object* cloneType() const override - { - return new RainShooter; - } - osg::Object* clone(const osg::CopyOp &) const override - { - return new RainShooter(*this); - } - -private: - osg::Vec3f mVelocity; - float mAngle; -}; - -// Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. -class AlphaFader : public SceneUtil::StateSetUpdater -{ -public: - /// @param alphaUpdate variable which to update with alpha value - AlphaFader(float *alphaUpdate) - : mAlpha(1.f) - { - mAlphaUpdate = alphaUpdate; - } + private: + float &mAlpha; + bool mIsRain; + }; - void setAlpha(float alpha) + // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. + class AlphaFader : public SceneUtil::StateSetUpdater { - mAlpha = alpha; - } + public: + /// @param alpha the variable alpha value is recovered from + AlphaFader(const float& alpha) + : mAlpha(alpha) + { } - void setDefaults(osg::StateSet* stateset) override - { - // need to create a deep copy of StateAttributes we will modify - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); - } + void setDefaults(osg::StateSet* stateset) override + { + // need to create a deep copy of StateAttributes we will modify + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); + } - void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, mAlpha)); + } - if (mAlphaUpdate) - *mAlphaUpdate = mAlpha; - } + protected: + const float &mAlpha; + }; // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: - SetupVisitor(float *alphaUpdate) + SetupVisitor(const float &alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - mAlphaUpdate = alphaUpdate; - } + , mAlpha(alpha) + { } void apply(osg::Node &node) override { @@ -1326,632 +194,729 @@ public: while (callback) { - if ((composite = dynamic_cast(callback))) + composite = dynamic_cast(callback); + if (composite) break; callback = callback->getNestedCallback(); } - osg::ref_ptr alphaFader (new AlphaFader(mAlphaUpdate)); + osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); else node.addUpdateCallback(alphaFader); - - mAlphaFaders.push_back(alphaFader); } } traverse(node); } - std::vector > getAlphaFaders() + private: + const float &mAlpha; + }; + + class SkyRTT : public SceneUtil::RTTNode + { + public: + SkyRTT(osg::Vec2f size, osg::Group* earlyRenderBinRoot) : + RTTNode(static_cast(size.x()), static_cast(size.y()), 0, false, 1, StereoAwareness::Aware), + mEarlyRenderBinRoot(earlyRenderBinRoot) + { + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + } + + void setDefaults(osg::Camera* camera) override { - return mAlphaFaders; + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setName("SkyCamera"); + camera->setNodeMask(MWRender::Mask_RenderToTexture); + camera->setCullMask(MWRender::Mask_Sky); + camera->addChild(mEarlyRenderBinRoot); + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); } private: - std::vector > mAlphaFaders; - float *mAlphaUpdate; + osg::ref_ptr mEarlyRenderBinRoot; }; -protected: - float mAlpha; - float *mAlphaUpdate; -}; +} -class RainFader : public AlphaFader +namespace MWRender { -public: - RainFader(float *alphaUpdate): AlphaFader(alphaUpdate) - { - } + SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager, bool enableSkyRTT) + : mSceneManager(sceneManager) + , mCamera(nullptr) + , mAtmosphereNightRoll(0.f) + , mCreated(false) + , mIsStorm(false) + , mDay(0) + , mMonth(0) + , mCloudAnimationTimer(0.f) + , mRainTimer(0.f) + , mStormParticleDirection(MWWorld::Weather::defaultDirection()) + , mStormDirection(MWWorld::Weather::defaultDirection()) + , mClouds() + , mNextClouds() + , 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) + , mWindSpeed(0.f) + , mBaseWindSpeed(0.f) + , mEnabled(true) + , mSunEnabled(true) + , mSunglareEnabled(true) + , mPrecipitationAlpha(0.f) + , mDirtyParticlesEffect(false) + { + osg::ref_ptr skyroot = new CameraRelativeTransform; + skyroot->setName("Sky Root"); + // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline + if (!mSceneManager->getForceShaders()) + skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); + SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); + parentNode->addChild(skyroot); + + mEarlyRenderBinRoot = new osg::Group; + // render before the world is rendered + mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + // Prevent unwanted clipping by water reflection camera's clipping plane + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); + + if (enableSkyRTT) + { + mSkyRTT = new SkyRTT(Settings::Manager::getVector2("sky rtt resolution", "Fog"), mEarlyRenderBinRoot); + skyroot->addChild(mSkyRTT); + mRootNode = new osg::Group; + skyroot->addChild(mRootNode); + } + else + mRootNode = skyroot; - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr mat (new osg::Material); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,1)); - mat->setColorMode(osg::Material::OFF); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + mRootNode->setNodeMask(Mask_Sky); + mRootNode->addChild(mEarlyRenderBinRoot); + mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); } - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override + void SkyManager::create() { - AlphaFader::apply(stateset,nv); - *mAlphaUpdate = mAlpha * 2.0; // mAlpha is limited to 0.6 so multiply by 2 to reach full intensity - } -}; + assert(!mCreated); -void SkyManager::setCamera(osg::Camera *camera) -{ - mCamera = camera; -} + bool forceShaders = mSceneManager->getForceShaders(); -class WrapAroundOperator : public osgParticle::Operator -{ -public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator() - { - mCamera = camera; - mWrapRange = wrapRange; - mHalfWrapRange = mWrapRange / 2.0; - mPreviousCameraPosition = getCameraPosition(); - } + mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); + ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); + mAtmosphereDay->accept(modAtmosphere); - osg::Object *cloneType() const override - { - return nullptr; - } + mAtmosphereUpdater = new AtmosphereUpdater; + mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; + mAtmosphereNightNode = new osg::PositionAttitudeTransform; + mAtmosphereNightNode->setNodeMask(0); + 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); + else + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); + atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); + atmosphereNight->accept(modStars); + mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); + atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); + + mSun = std::make_unique(mEarlyRenderBinRoot, *mSceneManager); + mSun->setSunglare(mSunglareEnabled); + mMasser = std::make_unique(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser); + mSecunda = std::make_unique(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda); + + mCloudNode = new osg::Group; + mEarlyRenderBinRoot->addChild(mCloudNode); + + mCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudMesh); + mCloudUpdater = new CloudUpdater(forceShaders); + mCloudUpdater->setOpacity(1.f); + cloudMeshChild->addUpdateCallback(mCloudUpdater); + mCloudMesh->addChild(cloudMeshChild); + + mNextCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr nextCloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mNextCloudMesh); + mNextCloudUpdater = new CloudUpdater(forceShaders); + mNextCloudUpdater->setOpacity(0.f); + nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); + mNextCloudMesh->setNodeMask(0); + mNextCloudMesh->addChild(nextCloudMeshChild); + + mCloudNode->addChild(mCloudMesh); + mCloudNode->addChild(mNextCloudMesh); + + ModVertexAlphaVisitor modClouds(ModVertexAlphaVisitor::Clouds); + mCloudMesh->accept(modClouds); + mNextCloudMesh->accept(modClouds); + + if (mSceneManager->getForceShaders()) + { + Shader::ShaderManager::DefineMap defines = {}; + Stereo::Manager::instance().shaderStereoDefines(defines); + auto vertex = mSceneManager->getShaderManager().getShader("sky_vertex.glsl", defines, osg::Shader::VERTEX); + auto fragment = mSceneManager->getShaderManager().getShader("sky_fragment.glsl", defines, osg::Shader::FRAGMENT); + auto program = mSceneManager->getShaderManager().getProgram(vertex, fragment); + mEarlyRenderBinRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", -1)); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); + + mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); + + mCreated = true; } - void operate(osgParticle::Particle *P, double dt) override + void SkyManager::setCamera(osg::Camera *camera) { + mCamera = camera; } - void operateParticles(osgParticle::ParticleSystem *ps, double dt) override + void SkyManager::createRain() { - osg::Vec3 position = getCameraPosition(); - osg::Vec3 positionDifference = position - mPreviousCameraPosition; + if (mRainNode) + return; - osg::Matrix toWorld, toLocal; + mRainNode = new osg::Group; - std::vector worldMatrices = ps->getWorldMatrices(); - - if (!worldMatrices.empty()) - { - toWorld = worldMatrices[0]; - toLocal.invert(toWorld); - } + mRainParticleSystem = new NifOsg::ParticleSystem; + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - for (int i = 0; i < ps->numParticles(); ++i) - { - osgParticle::Particle *p = ps->getParticle(i); - p->setPosition(toWorld.preMult(p->getPosition())); - p->setPosition(p->getPosition() - positionDifference); + mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); + mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions - { - osg::Vec3 pos = p->getPosition(); + osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); - if (pos[j] < -mHalfWrapRange[j]) - pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); - else if (pos[j] > mHalfWrapRange[j]) - pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; + osg::ref_ptr raindropTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds")); + raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - p->setPosition(pos); - } + stateset->setTextureAttributeAndModes(0, raindropTex); + stateset->setNestRenderBins(false); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - p->setPosition(toLocal.preMult(p->getPosition())); - } + osg::ref_ptr mat = new osg::Material; + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(mat); - mPreviousCameraPosition = position; - } + osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); + particleTemplate.setLifeTime(1); -protected: - osg::Camera *mCamera; - osg::Vec3 mPreviousCameraPosition; - osg::Vec3 mWrapRange; - osg::Vec3 mHalfWrapRange; + osg::ref_ptr emitter = new osgParticle::ModularEmitter; + emitter->setParticleSystem(mRainParticleSystem); - osg::Vec3 getCameraPosition() - { - return mCamera->getInverseViewMatrix().getTrans(); - } -}; + osg::ref_ptr placer = new osgParticle::BoxPlacer; + placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + emitter->setPlacer(placer); + mPlacer = placer; -void SkyManager::createRain() -{ - if (mRainNode) - return; - - mRainNode = new osg::Group; - - mRainParticleSystem = new osgParticle::ParticleSystem; - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - - mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); - mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - - osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); - - osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); - raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); - stateset->setNestRenderBins(false); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); - particleTemplate.setLifeTime(1); - - osg::ref_ptr emitter (new osgParticle::ModularEmitter); - emitter->setParticleSystem(mRainParticleSystem); - - osg::ref_ptr placer (new osgParticle::BoxPlacer); - placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - emitter->setPlacer(placer); - mPlacer = placer; - - // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. - // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). - // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. - osg::ref_ptr counter (new RainCounter); - counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); - emitter->setCounter(counter); - mCounter = counter; - - osg::ref_ptr shooter (new RainShooter); - mRainShooter = shooter; - emitter->setShooter(shooter); - - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mRainParticleSystem); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,rainRange)); - program->setParticleSystem(mRainParticleSystem); - mRainNode->addChild(program); - - mRainNode->addChild(emitter); - mRainNode->addChild(mRainParticleSystem); - mRainNode->addChild(updater); - - mRainFader = new RainFader(&mWeatherAlpha); - mRainNode->addUpdateCallback(mRainFader); - mRainNode->addCullCallback(mUnderwaterSwitch); - mRainNode->setNodeMask(Mask_WeatherParticles); - - mRootNode->addChild(mRainNode); -} + // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. + // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). + // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. + osg::ref_ptr counter = new RainCounter; + counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + emitter->setCounter(counter); + mCounter = counter; -void SkyManager::destroyRain() -{ - if (!mRainNode) - return; - - mRootNode->removeChild(mRainNode); - mRainNode = nullptr; - mPlacer = nullptr; - mCounter = nullptr; - mRainParticleSystem = nullptr; - mRainShooter = nullptr; - mRainFader = nullptr; -} + osg::ref_ptr shooter = new RainShooter; + mRainShooter = shooter; + emitter->setShooter(shooter); -SkyManager::~SkyManager() -{ - if (mRootNode) - { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; + osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; + updater->addParticleSystem(mRainParticleSystem); + + osg::ref_ptr program = new osgParticle::ModularProgram; + program->addOperator(new WrapAroundOperator(mCamera,rainRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); + program->setParticleSystem(mRainParticleSystem); + mRainNode->addChild(program); + + mRainNode->addChild(emitter); + mRainNode->addChild(mRainParticleSystem); + mRainNode->addChild(updater); + + // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. + mRainNode->addCullCallback(mUnderwaterSwitch); + mRainNode->setNodeMask(Mask_WeatherParticles); + + mRainParticleSystem->setUserValue("simpleLighting", true); + mSceneManager->recreateShaders(mRainNode); + + mRootNode->addChild(mRainNode); } -} -int SkyManager::getMasserPhase() const -{ - if (!mCreated) return 0; - return mMasser->getPhaseInt(); -} + void SkyManager::destroyRain() + { + if (!mRainNode) + return; -int SkyManager::getSecundaPhase() const -{ - if (!mCreated) return 0; - return mSecunda->getPhaseInt(); -} + mRootNode->removeChild(mRainNode); + mRainNode = nullptr; + mPlacer = nullptr; + mCounter = nullptr; + mRainParticleSystem = nullptr; + mRainShooter = nullptr; + } -bool SkyManager::isEnabled() -{ - return mEnabled; -} + SkyManager::~SkyManager() + { + if (mRootNode) + { + mRootNode->getParent(0)->removeChild(mRootNode); + mRootNode = nullptr; + } + } -bool SkyManager::hasRain() -{ - return mRainNode != nullptr; -} + int SkyManager::getMasserPhase() const + { + if (!mCreated) return 0; + return mMasser->getPhaseInt(); + } -void SkyManager::update(float duration) -{ - if (!mEnabled) + int SkyManager::getSecundaPhase() const { - if (mRainIntensityUniform) - mRainIntensityUniform->set((float) 0.0); + if (!mCreated) return 0; + return mSecunda->getPhaseInt(); + } - return; + bool SkyManager::isEnabled() + { + return mEnabled; } - if (mRainIntensityUniform) + bool SkyManager::hasRain() const { - if (mIsStorm || (!hasRain() && !mParticleNode)) - mRainIntensityUniform->set((float) 0.0); - else - mRainIntensityUniform->set((float) mWeatherAlpha); + return mRainNode != nullptr; } - switchUnderwaterRain(); + float SkyManager::getPrecipitationAlpha() const + { + if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) + return mPrecipitationAlpha; + + return 0.f; + } - if (mIsStorm) + void SkyManager::update(float duration) { - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); + if (!mEnabled) + return; - mCloudNode->setAttitude(quat); - if (mParticleNode) + switchUnderwaterRain(); + + if (mIsStorm && mParticleNode) { + osg::Quat quat; + quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == "meshes\\blizzard.nif") - quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); + if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) + quat.makeRotate(osg::Vec3f(-1,0,0), mStormParticleDirection); mParticleNode->setAttitude(quat); } - } - else - mCloudNode->setAttitude(osg::Quat()); - - // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; - mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); - mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); - - // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); - if (mAtmosphereNightNode->getNodeMask() != 0) - mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); -} -void SkyManager::setEnabled(bool enabled) -{ - if (enabled && !mCreated) - create(); + // UV Scroll the clouds + mCloudAnimationTimer += duration * mCloudSpeed * 0.003; + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); + mCloudUpdater->setTextureCoord(mCloudAnimationTimer); - mRootNode->setNodeMask(enabled ? Mask_Sky : 0); + // morrowind rotates each cloud mesh independently + osg::Quat rotation; + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); + mCloudMesh->setAttitude(rotation); - mEnabled = enabled; -} + if (mNextCloudMesh->getNodeMask()) + { + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); + mNextCloudMesh->setAttitude(rotation); + } -void SkyManager::setMoonColour (bool red) -{ - if (!mCreated) return; - mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); -} + // rotate the stars by 360 degrees every 4 days + mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); + if (mAtmosphereNightNode->getNodeMask() != 0) + mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); + } -void SkyManager::updateRainParameters() -{ - if (mRainShooter) + void SkyManager::setEnabled(bool enabled) { - float angle = -std::atan(mWindSpeed/50.f); - mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); - mRainShooter->setAngle(angle); + if (enabled && !mCreated) + create(); - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); - mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + if (!enabled && mParticleNode && mParticleEffect) + { + mCurrentParticleEffect.clear(); + mDirtyParticlesEffect = true; + } - mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + mEnabled = enabled; } -} -void SkyManager::switchUnderwaterRain() -{ - if (!mRainParticleSystem) - return; + void SkyManager::setMoonColour (bool red) + { + if (!mCreated) return; + mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); + } - bool freeze = mUnderwaterSwitch->isUnderwater(); - mRainParticleSystem->setFrozen(freeze); -} + void SkyManager::updateRainParameters() + { + if (mRainShooter) + { + float angle = -std::atan(mWindSpeed/50.f); + mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); + mRainShooter->setAngle(angle); -void SkyManager::setWeather(const WeatherResult& weather) -{ - if (!mCreated) return; + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - mRainEntranceSpeed = weather.mRainEntranceSpeed; - mRainMaxRaindrops = weather.mRainMaxRaindrops; - mRainDiameter = weather.mRainDiameter; - mRainMinHeight = weather.mRainMinHeight; - mRainMaxHeight = weather.mRainMaxHeight; - mRainSpeed = weather.mRainSpeed; - mWindSpeed = weather.mWindSpeed; + mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - if (mRainEffect != weather.mRainEffect) - { - mRainEffect = weather.mRainEffect; - if (!mRainEffect.empty()) - { - createRain(); - } - else - { - destroyRain(); + mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); } } - updateRainParameters(); + void SkyManager::switchUnderwaterRain() + { + if (!mRainParticleSystem) + return; - mIsStorm = weather.mIsStorm; + bool freeze = mUnderwaterSwitch->isUnderwater(); + mRainParticleSystem->setFrozen(freeze); + } - if (mCurrentParticleEffect != weather.mParticleEffect) + void SkyManager::setWeather(const WeatherResult& weather) { - mCurrentParticleEffect = weather.mParticleEffect; + if (!mCreated) return; - // cleanup old particles - if (mParticleEffect) - { - mParticleNode->removeChild(mParticleEffect); - mParticleEffect = nullptr; - mParticleFaders.clear(); - } + mRainEntranceSpeed = weather.mRainEntranceSpeed; + mRainMaxRaindrops = weather.mRainMaxRaindrops; + mRainDiameter = weather.mRainDiameter; + mRainMinHeight = weather.mRainMinHeight; + mRainMaxHeight = weather.mRainMaxHeight; + mRainSpeed = weather.mRainSpeed; + mWindSpeed = weather.mWindSpeed; + mBaseWindSpeed = weather.mBaseWindSpeed; - if (mCurrentParticleEffect.empty()) + if (mRainEffect != weather.mRainEffect) { - if (mParticleNode) + mRainEffect = weather.mRainEffect; + if (!mRainEffect.empty()) { - mRootNode->removeChild(mParticleNode); - mParticleNode = nullptr; + createRain(); } - } - else - { - if (!mParticleNode) + else { - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->addCullCallback(mUnderwaterSwitch); - mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); + destroyRain(); } + } - mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + updateRainParameters(); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); - mParticleEffect->accept(assignVisitor); + mIsStorm = weather.mIsStorm; - AlphaFader::SetupVisitor alphaFaderSetupVisitor(&mWeatherAlpha); + if (mIsStorm) + mStormDirection = weather.mStormDirection; - mParticleEffect->accept(alphaFaderSetupVisitor); - mParticleFaders = alphaFaderSetupVisitor.getAlphaFaders(); + if (mDirtyParticlesEffect || (mCurrentParticleEffect != weather.mParticleEffect)) + { + mDirtyParticlesEffect = false; + mCurrentParticleEffect = weather.mParticleEffect; - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - mParticleEffect->accept(disableFreezeOnCullVisitor); + // cleanup old particles + if (mParticleEffect) + { + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } - if (!weather.mIsStorm) + if (mCurrentParticleEffect.empty()) + { + if (mParticleNode) + { + mRootNode->removeChild(mParticleNode); + mParticleNode = nullptr; + } + } + else { + if (!mParticleNode) + { + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->addCullCallback(mUnderwaterSwitch); + mParticleNode->setNodeMask(Mask_WeatherParticles); + mRootNode->addChild(mParticleNode); + } + + mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::make_shared()); + mParticleEffect->accept(assignVisitor); + + SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mParticleEffect->accept(alphaFaderSetupVisitor); + SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); mParticleEffect->accept(findPSVisitor); for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) { osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); + + osg::ref_ptr program = new osgParticle::ModularProgram; + if (!mIsStorm) + program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); program->setParticleSystem(ps); mParticleNode->addChild(program); + + for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + { + ps->getParticle(particleIndex)->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + ps->getParticle(particleIndex)->update(0, true); + } + + ps->setUserValue("simpleLighting", true); } + + mSceneManager->recreateShaders(mParticleNode); } } - } - if (mClouds != weather.mCloudTexture) - { - mClouds = weather.mCloudTexture; + if (mClouds != weather.mCloudTexture) + { + mClouds = weather.mCloudTexture; - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + std::string 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); + 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(cloudTex); + } - if (mNextClouds != weather.mNextCloudTexture) - { - mNextClouds = weather.mNextCloudTexture; + if (mStormDirection != weather.mStormDirection) + mStormDirection = weather.mStormDirection; - if (!mNextClouds.empty()) + if (mNextStormDirection != weather.mNextStormDirection) + mNextStormDirection = weather.mNextStormDirection; + + if (mNextClouds != weather.mNextCloudTexture) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + mNextClouds = weather.mNextCloudTexture; - 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); + if (!mNextClouds.empty()) + { + std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); - mCloudUpdater2->setTexture(cloudTex); + 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); + mNextStormDirection = weather.mStormDirection; + } } - } - if (mCloudBlendFactor != weather.mCloudBlendFactor) - { - mCloudBlendFactor = weather.mCloudBlendFactor; + if (mCloudBlendFactor != weather.mCloudBlendFactor) + { + mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); - mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); - mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0 : 0); - } + mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); + mNextCloudUpdater->setOpacity(mCloudBlendFactor); + mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + } - if (mCloudColour != weather.mFogColor) - { - osg::Vec4f clr (weather.mFogColor); - clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + if (mCloudColour != weather.mFogColor) + { + osg::Vec4f clr (weather.mFogColor); + clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); - mCloudUpdater->setEmissionColor(clr); - mCloudUpdater2->setEmissionColor(clr); + mCloudUpdater->setEmissionColor(clr); + mNextCloudUpdater->setEmissionColor(clr); - mCloudColour = weather.mFogColor; - } + mCloudColour = weather.mFogColor; + } - if (mSkyColour != weather.mSkyColor) - { - mSkyColour = weather.mSkyColor; + if (mSkyColour != weather.mSkyColor) + { + mSkyColour = weather.mSkyColor; - mAtmosphereUpdater->setEmissionColor(mSkyColour); - mMasser->setAtmosphereColor(mSkyColour); - mSecunda->setAtmosphereColor(mSkyColour); - } + mAtmosphereUpdater->setEmissionColor(mSkyColour); + mMasser->setAtmosphereColor(mSkyColour); + mSecunda->setAtmosphereColor(mSkyColour); + } - if (mFogColour != weather.mFogColor) - { - mFogColour = weather.mFogColor; + if (mFogColour != weather.mFogColor) + { + mFogColour = weather.mFogColor; + } + + mCloudSpeed = weather.mCloudSpeed; + + mMasser->adjustTransparency(weather.mGlareView); + mSecunda->adjustTransparency(weather.mGlareView); + + mSun->setColor(weather.mSunDiscColor); + mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + + float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + + if (weather.mNight && mStarsOpacity != nextStarsOpacity) + { + mStarsOpacity = nextStarsOpacity; + + mAtmosphereNightUpdater->setFade(mStarsOpacity); + } + + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + mPrecipitationAlpha = weather.mPrecipitationAlpha; } - mCloudSpeed = weather.mCloudSpeed; + float SkyManager::getBaseWindSpeed() const + { + if (!mCreated) return 0.f; - mMasser->adjustTransparency(weather.mGlareView); - mSecunda->adjustTransparency(weather.mGlareView); + return mBaseWindSpeed; + } - mSun->setColor(weather.mSunDiscColor); - mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + void SkyManager::setSunglare(bool enabled) + { + mSunglareEnabled = enabled; - float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + if (mSun) + mSun->setSunglare(mSunglareEnabled); + } - if (weather.mNight && mStarsOpacity != nextStarsOpacity) + void SkyManager::sunEnable() { - mStarsOpacity = nextStarsOpacity; + if (!mCreated) return; - mAtmosphereNightUpdater->setFade(mStarsOpacity); + mSun->setVisible(true); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0 : 0); + void SkyManager::sunDisable() + { + if (!mCreated) return; - if (mRainFader) - mRainFader->setAlpha(weather.mEffectFade * 0.6); // * Rain_Threshold? + mSun->setVisible(false); + } - for (AlphaFader* fader : mParticleFaders) - fader->setAlpha(weather.mEffectFade); -} + void SkyManager::setStormParticleDirection(const osg::Vec3f &direction) + { + mStormParticleDirection = direction; + } -void SkyManager::sunEnable() -{ - if (!mCreated) return; + void SkyManager::setSunDirection(const osg::Vec3f& direction) + { + if (!mCreated) return; - mSun->setVisible(true); -} + mSun->setDirection(direction); + } -void SkyManager::sunDisable() -{ - if (!mCreated) return; + void SkyManager::setMasserState(const MoonState& state) + { + if(!mCreated) return; - mSun->setVisible(false); -} + mMasser->setState(state); + } -void SkyManager::setStormDirection(const osg::Vec3f &direction) -{ - mStormDirection = direction; -} + void SkyManager::setSecundaState(const MoonState& state) + { + if(!mCreated) return; -void SkyManager::setSunDirection(const osg::Vec3f& direction) -{ - if (!mCreated) return; + mSecunda->setState(state); + } - mSun->setDirection(direction); -} + void SkyManager::setDate(int day, int month) + { + mDay = day; + mMonth = month; + } -void SkyManager::setMasserState(const MoonState& state) -{ - if(!mCreated) return; + void SkyManager::setGlareTimeOfDayFade(float val) + { + mSun->setGlareTimeOfDayFade(val); + } - mMasser->setState(state); -} + void SkyManager::setWaterHeight(float height) + { + mUnderwaterSwitch->setWaterLevel(height); + } -void SkyManager::setSecundaState(const MoonState& state) -{ - if(!mCreated) return; + 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")); - mSecunda->setState(state); -} + 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")); -void SkyManager::setDate(int day, int month) -{ - mDay = day; - mMonth = month; -} + textures.emplace_back("textures/tx_mooncircle_full_s.dds"); + textures.emplace_back("textures/tx_mooncircle_full_m.dds"); -void SkyManager::setGlareTimeOfDayFade(float val) -{ - mSun->setGlareTimeOfDayFade(val); -} + textures.emplace_back("textures/tx_masser_new.dds"); + textures.emplace_back("textures/tx_masser_one_wax.dds"); + textures.emplace_back("textures/tx_masser_half_wax.dds"); + textures.emplace_back("textures/tx_masser_three_wax.dds"); + textures.emplace_back("textures/tx_masser_one_wan.dds"); + textures.emplace_back("textures/tx_masser_half_wan.dds"); + textures.emplace_back("textures/tx_masser_three_wan.dds"); + textures.emplace_back("textures/tx_masser_full.dds"); -void SkyManager::setWaterHeight(float height) -{ - mUnderwaterSwitch->setWaterLevel(height); -} + textures.emplace_back("textures/tx_secunda_new.dds"); + textures.emplace_back("textures/tx_secunda_one_wax.dds"); + textures.emplace_back("textures/tx_secunda_half_wax.dds"); + textures.emplace_back("textures/tx_secunda_three_wax.dds"); + textures.emplace_back("textures/tx_secunda_one_wan.dds"); + textures.emplace_back("textures/tx_secunda_half_wan.dds"); + textures.emplace_back("textures/tx_secunda_three_wan.dds"); + textures.emplace_back("textures/tx_secunda_full.dds"); -void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) -{ - models.emplace_back("meshes/sky_atmosphere.nif"); - if (mSceneManager->getVFS()->exists("meshes/sky_night_02.nif")) - models.emplace_back("meshes/sky_night_02.nif"); - models.emplace_back("meshes/sky_night_01.nif"); - models.emplace_back("meshes/sky_clouds_01.nif"); - - models.emplace_back("meshes\\ashcloud.nif"); - models.emplace_back("meshes\\blightcloud.nif"); - models.emplace_back("meshes\\snow.nif"); - models.emplace_back("meshes\\blizzard.nif"); - - textures.emplace_back("textures/tx_mooncircle_full_s.dds"); - textures.emplace_back("textures/tx_mooncircle_full_m.dds"); - - textures.emplace_back("textures/tx_masser_new.dds"); - textures.emplace_back("textures/tx_masser_one_wax.dds"); - textures.emplace_back("textures/tx_masser_half_wax.dds"); - textures.emplace_back("textures/tx_masser_three_wax.dds"); - textures.emplace_back("textures/tx_masser_one_wan.dds"); - textures.emplace_back("textures/tx_masser_half_wan.dds"); - textures.emplace_back("textures/tx_masser_three_wan.dds"); - textures.emplace_back("textures/tx_masser_full.dds"); - - textures.emplace_back("textures/tx_secunda_new.dds"); - textures.emplace_back("textures/tx_secunda_one_wax.dds"); - textures.emplace_back("textures/tx_secunda_half_wax.dds"); - textures.emplace_back("textures/tx_secunda_three_wax.dds"); - textures.emplace_back("textures/tx_secunda_one_wan.dds"); - textures.emplace_back("textures/tx_secunda_half_wan.dds"); - textures.emplace_back("textures/tx_secunda_three_wan.dds"); - textures.emplace_back("textures/tx_secunda_full.dds"); - - textures.emplace_back("textures/tx_sun_05.dds"); - textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); - - textures.emplace_back("textures/tx_raindrop_01.dds"); -} + textures.emplace_back("textures/tx_sun_05.dds"); + textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); -void SkyManager::setWaterEnabled(bool enabled) -{ - mUnderwaterSwitch->setEnabled(enabled); -} + textures.emplace_back("textures/tx_raindrop_01.dds"); + } + void SkyManager::setWaterEnabled(bool enabled) + { + mUnderwaterSwitch->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index cf697bd44f..8682a6f17f 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -7,12 +7,8 @@ #include #include -#include -namespace osg -{ - class Camera; -} +#include "skyutil.hpp" namespace osg { @@ -20,6 +16,7 @@ namespace osg class Node; class Material; class PositionAttitudeTransform; + class Camera; } namespace osgParticle @@ -33,98 +30,19 @@ namespace Resource class SceneManager; } -namespace MWRender +namespace SceneUtil { - class AtmosphereUpdater; - class AtmosphereNightUpdater; - class CloudUpdater; - class Sun; - class Moon; - class RainCounter; - class RainShooter; - class RainFader; - class AlphaFader; - class UnderwaterSwitchCallback; - - struct WeatherResult - { - std::string mCloudTexture; - std::string mNextCloudTexture; - float mCloudBlendFactor; - - osg::Vec4f mFogColor; - - osg::Vec4f mAmbientColor; - - osg::Vec4f mSkyColor; - - // sun light color - osg::Vec4f mSunColor; - - // alpha is the sun transparency - osg::Vec4f mSunDiscColor; - - float mFogDepth; - - float mDLFogFactor; - float mDLFogOffset; - - float mWindSpeed; - float mCurrentWindSpeed; - float mNextWindSpeed; - - float mCloudSpeed; - - float mGlareView; - - bool mNight; // use night skybox - float mNightFade; // fading factor for night skybox - - bool mIsStorm; - - std::string mAmbientLoopSoundID; - float mAmbientSoundVolume; - - std::string mParticleEffect; - std::string mRainEffect; - float mEffectFade; - - float mRainDiameter; - float mRainMinHeight; - float mRainMaxHeight; - float mRainSpeed; - float mRainEntranceSpeed; - int mRainMaxRaindrops; - }; - - struct MoonState - { - enum class Phase - { - Full = 0, - WaningGibbous, - ThirdQuarter, - WaningCrescent, - New, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - Unspecified - }; - - float mRotationFromHorizon; - float mRotationFromNorth; - Phase mPhase; - float mShadowBlend; - float mMoonAlpha; - }; + class RTTNode; +} +namespace MWRender +{ ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager { public: - SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager); + SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager, bool enableSkyRTT); ~SkyManager(); void update(float duration); @@ -156,11 +74,13 @@ namespace MWRender bool isEnabled(); - bool hasRain(); + bool hasRain() const; + + float getPrecipitationAlpha() const; void setRainSpeed(float speed); - void setStormDirection(const osg::Vec3f& direction); + void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); @@ -179,7 +99,11 @@ namespace MWRender void setCamera(osg::Camera *camera); - void setRainIntensityUniform(osg::Uniform *uniform); + float getBaseWindSpeed() const; + + void setSunglare(bool enabled); + + SceneUtil::RTTNode* getSkyRTT() { return mSkyRTT.get(); } private: void create(); @@ -193,22 +117,20 @@ namespace MWRender Resource::SceneManager* mSceneManager; osg::Camera *mCamera; - osg::Uniform *mRainIntensityUniform; osg::ref_ptr mRootNode; osg::ref_ptr mEarlyRenderBinRoot; osg::ref_ptr mParticleNode; osg::ref_ptr mParticleEffect; - std::vector > mParticleFaders; osg::ref_ptr mUnderwaterSwitch; - osg::ref_ptr mCloudNode; + osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; - osg::ref_ptr mCloudUpdater2; - osg::ref_ptr mCloudMesh; - osg::ref_ptr mCloudMesh2; + osg::ref_ptr mNextCloudUpdater; + osg::ref_ptr mCloudMesh; + osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; @@ -227,7 +149,6 @@ namespace MWRender osg::ref_ptr mPlacer; osg::ref_ptr mCounter; osg::ref_ptr mRainShooter; - osg::ref_ptr mRainFader; bool mCreated; @@ -240,7 +161,10 @@ namespace MWRender float mRainTimer; + // particle system rotation is independent of cloud rotation internally + osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; @@ -265,14 +189,19 @@ namespace MWRender float mRainEntranceSpeed; int mRainMaxRaindrops; float mWindSpeed; + float mBaseWindSpeed; bool mEnabled; bool mSunEnabled; + bool mSunglareEnabled; - float mWeatherAlpha; + float mPrecipitationAlpha; + bool mDirtyParticlesEffect; osg::Vec4f mMoonScriptColor; + + osg::ref_ptr mSkyRTT; }; } -#endif // GAME_RENDER_SKY_H +#endif diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp new file mode 100644 index 0000000000..29a82f7eb0 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.cpp @@ -0,0 +1,1212 @@ +#include "skyutil.hpp" + +#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 "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/weather.hpp" + +#include "vismask.hpp" +#include "renderbin.hpp" + +namespace +{ + enum class Pass + { + Atmosphere, + Atmosphere_Night, + Clouds, + Moon, + Sun, + Sunflash_Query, + Sunglare, + }; + + osg::ref_ptr createTexturedQuad(int numUvSets = 1, float scale = 1.f) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-0.5 * scale, -0.5 * scale, 0)); + verts->push_back(osg::Vec3f(-0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, -0.5 * scale, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(0, 1)); + texcoords->push_back(osg::Vec2f(0, 0)); + texcoords->push_back(osg::Vec2f(1, 0)); + texcoords->push_back(osg::Vec2f(1, 1)); + + osg::ref_ptr colors = new osg::Vec4Array; + colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); + geom->setColorArray(colors, osg::Array::BIND_OVERALL); + + for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + + return geom; + } + + struct DummyComputeBoundCallback : osg::Node::ComputeBoundingSphereCallback + { + osg::BoundingSphere computeBound(const osg::Node& node) const override + { + return osg::BoundingSphere(); + } + }; +} + +namespace MWRender +{ + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode) + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(colorMode); + return mat; + } + + osg::ref_ptr createAlphaTrackingUnlitMaterial() + { + return createUnlitMaterial(osg::Material::DIFFUSE); + } + + class SunUpdater : public SceneUtil::StateSetUpdater + { + public: + osg::Vec4f mColor; + + SunUpdater() + : mColor(1.f, 1.f, 1.f, 1.f) + { } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->setAttributeAndModes(createUnlitMaterial()); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + } + }; + + OcclusionCallback::OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : mOcclusionQueryVisiblePixels(oqnVisible) + , mOcclusionQueryTotalPixels(oqnTotal) + { } + + float OcclusionCallback::getVisibleRatio (osg::Camera* camera) + { + int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); + int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); + + float visibleRatio = 0.f; + if (total > 0) + visibleRatio = static_cast(visible) / static_cast(total); + + float dt = MWBase::Environment::get().getFrameDuration(); + + float lastRatio = mLastRatio[osg::observer_ptr(camera)]; + + float change = dt*10; + + if (visibleRatio > lastRatio) + visibleRatio = std::min(visibleRatio, lastRatio + change); + else + visibleRatio = std::max(visibleRatio, lastRatio - change); + + mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + + return visibleRatio; + } + + /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. + class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : OcclusionCallback(oqnVisible, oqnTotal) + , mGlareView(1.f) + { } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + osg::ref_ptr stateset; + + if (visibleRatio > 0.f) + { + const float fadeThreshold = 0.1; + if (visibleRatio < fadeThreshold) + { + float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; + osg::ref_ptr mat (createUnlitMaterial()); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); + stateset = new osg::StateSet; + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } + } + + float scale = visibleRatio; + + if (scale == 0.f) + { + // no traverse + return; + } + else if (scale == 1.f) + traverse(node, cv); + else + { + osg::Matrix modelView = *cv->getModelViewMatrix(); + + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); + + if (stateset) + cv->pushStateSet(stateset); + + cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + + traverse(node, cv); + + cv->popModelViewMatrix(); + + if (stateset) + cv->popStateSet(); + } + } + + void setGlareView(float value) + { + mGlareView = value; + } + + private: + float mGlareView; + }; + + /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. + /// Must be attached as a cull callback to the node above the glare node. + class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, + osg::ref_ptr sunTransform) + : OcclusionCallback(oqnVisible, oqnTotal) + , mSunTransform(sunTransform) + , mTimeOfDayFade(1.f) + , mGlareView(1.f) + { + mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); + mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); + mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); + + // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, + // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, + // so the resulting color looks more orange than red. + mColor *= 2; + for (int i=0; i<3; ++i) + mColor[i] = std::min(1.f, mColor[i]); + } + + void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); + + float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); + float fade = value * mSunGlareFaderMax; + + fade *= mTimeOfDayFade * mGlareView * visibleRatio; + + if (fade == 0.f) + { + // no traverse + return; + } + else + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr mat = createUnlitMaterial(); + + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + + stateset->setAttributeAndModes(mat); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } + } + + void setTimeOfDayFade(float val) + { + mTimeOfDayFade = val; + } + + void setGlareView(float glareView) + { + mGlareView = glareView; + } + + private: + float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + { + osg::Vec3d eye, center, up; + viewMatrix.getLookAt(eye, center, up); + + osg::Vec3d forward = center - eye; + osg::Vec3d sun = mSunTransform->getPosition(); + + forward.normalize(); + sun.normalize(); + float angleRadians = std::acos(forward * sun); + return angleRadians; + } + + osg::ref_ptr mSunTransform; + float mTimeOfDayFade; + float mGlareView; + osg::Vec4f mColor; + float mSunGlareFaderMax; + float mSunGlareFaderAngleMax; + }; + + struct MoonUpdater : SceneUtil::StateSetUpdater + { + Resource::ImageManager& mImageManager; + osg::ref_ptr mPhaseTex; + osg::ref_ptr mCircleTex; + float mTransparency; + float mShadowBlend; + osg::Vec4f mAtmosphereColor; + osg::Vec4f mMoonColor; + bool mForceShaders; + + MoonUpdater(Resource::ImageManager& imageManager, bool forceShaders) + : mImageManager(imageManager) + , mPhaseTex() + , mCircleTex() + , mTransparency(1.0f) + , mShadowBlend(1.0f) + , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) + , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + , mForceShaders(forceShaders) + { } + + void setDefaults(osg::StateSet* stateset) override + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon))); + stateset->setTextureAttributeAndModes(0, mPhaseTex); + stateset->setTextureAttributeAndModes(1, mCircleTex); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("diffuseMap", 0)); + stateset->addUniform(new osg::Uniform("maskMap", 1)); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(0, mPhaseTex); + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor + stateset->setTextureAttributeAndModes(0, texEnv); + + stateset->setTextureAttributeAndModes(1, mCircleTex); + osg::ref_ptr texEnv2 = new osg::TexEnvCombine; + texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); + texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); + texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency + stateset->setTextureAttributeAndModes(1, texEnv2); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + if (mForceShaders) + { + stateset->setTextureAttribute(0, mPhaseTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(1, mCircleTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + if (auto* uMoonBlend = stateset->getUniform("moonBlend")) + uMoonBlend->set(mMoonColor * mShadowBlend); + if (auto* uAtmosphereFade = stateset->getUniform("atmosphereFade")) + uAtmosphereFade->set(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mMoonColor * mShadowBlend); + + osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + } + + void setTextures(const std::string& phaseTex, const std::string& circleTex) + { + mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); + mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); + mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reset(); + } + }; + + class CameraRelativeTransformCullCallback : public SceneUtil::NodeCallback + { + public: + void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + { + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, cv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; + + void AtmosphereUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere))); + } + + void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + } + + AtmosphereNightUpdater::AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders) + : mColor(osg::Vec4f(0,0,0,0)) + , mTexture(new osg::Texture2D(imageManager->getWarningImage())) + , mForceShaders(forceShaders) + { } + + void AtmosphereNightUpdater::setFade(float fade) + { + mColor.a() = fade; + } + + void AtmosphereNightUpdater::setDefaults(osg::StateSet* stateset) + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("opacity", 0.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night))); + } + else + { + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + + stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void AtmosphereNightUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mColor.a()); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mColor); + } + } + + CloudUpdater::CloudUpdater(bool forceShaders) + : mOpacity(0.f) + , mForceShaders(forceShaders) + { } + + void CloudUpdater::setTexture(osg::ref_ptr texture) + { + mTexture = texture; + } + + void CloudUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void CloudUpdater::setOpacity(float opacity) + { + mOpacity = opacity; + } + + void CloudUpdater::setTextureCoord(float timer) + { + mTexMat = osg::Matrixf::translate(osg::Vec3f(0.f, -timer, 0.f)); + } + + void CloudUpdater::setDefaults(osg::StateSet *stateset) + { + stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr texmat = new osg::TexMat; + stateset->setTextureAttributeAndModes(0, texmat); + + if (mForceShaders) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + stateset->addUniform(new osg::Uniform("opacity", 1.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds))); + } + else + { + stateset->setTextureAttributeAndModes(1, texmat); + // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already + osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; + texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); + texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); + + stateset->setTextureAttributeAndModes(1, texEnvCombine); + + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void CloudUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + + osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); + texMat->setMatrix(mTexMat); + + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mOpacity); + } + else + { + stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); + } + } + + + class SkyStereoStatesetUpdater : public SceneUtil::StateSetUpdater + { + public: + SkyStereoStatesetUpdater() + { + } + + protected: + void setDefaults(osg::StateSet* stateset) override + { + if (!Stereo::getMultiview()) + stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "projectionMatrix"), osg::StateAttribute::OVERRIDE); + + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override + { + if (Stereo::getMultiview()) + { + std::array projectionMatrices; + auto& sm = Stereo::Manager::instance(); + + for (int view : {0, 1}) + { + auto projectionMatrix = sm.computeEyeProjection(view, true); + auto viewOffsetMatrix = sm.computeEyeViewOffset(view); + for (int col : {0, 1, 2}) + viewOffsetMatrix(3, col) = 0; + + projectionMatrices[view] = viewOffsetMatrix * projectionMatrix; + } + + Stereo::setMultiviewMatrices(stateset, projectionMatrices); + } + } + void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* /*cv*/) override + { + auto& sm = Stereo::Manager::instance(); + auto* projectionMatrixUniform = stateset->getUniform("projectionMatrix"); + auto projectionMatrix = sm.computeEyeProjection(0, true); + auto viewOffsetMatrix = sm.computeEyeViewOffset(0); + for (int col : {0, 1, 2}) + viewOffsetMatrix(3, col) = 0; + + projectionMatrixUniform->set(viewOffsetMatrix * projectionMatrix); + } + void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* /*cv*/) override + { + auto& sm = Stereo::Manager::instance(); + auto* projectionMatrixUniform = stateset->getUniform("projectionMatrix"); + auto projectionMatrix = sm.computeEyeProjection(1, true); + auto viewOffsetMatrix = sm.computeEyeViewOffset(1); + for (int col : {0, 1, 2}) + viewOffsetMatrix(3, col) = 0; + + projectionMatrixUniform->set(viewOffsetMatrix * projectionMatrix); + } + + private: + }; + + CameraRelativeTransform::CameraRelativeTransform() + { + // Culling works in node-local space, not in camera space, so we can't cull this node correctly + // That's not a problem though, children of this node can be culled just fine + // Just make sure you do not place a CameraRelativeTransform deep in the scene graph + setCullingActive(false); + + addCullCallback(new CameraRelativeTransformCullCallback); + if (Stereo::getStereo()) + addCullCallback(new SkyStereoStatesetUpdater); + } + + CameraRelativeTransform::CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) + : osg::Transform(copy, copyop) + { } + + const osg::Vec3f& CameraRelativeTransform::getLastViewPoint() const + { + return mViewPoint; + } + + bool CameraRelativeTransform::computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const + { + if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + mViewPoint = static_cast(nv)->getViewPoint(); + } + + if (_referenceFrame==RELATIVE_RF) + { + matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); + return false; + } + else // absolute + { + matrix.makeIdentity(); + return true; + } + } + + osg::BoundingSphere CameraRelativeTransform::computeBound() const + { + return osg::BoundingSphere(); + } + + UnderwaterSwitchCallback::UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) + : mCameraRelativeTransform(cameraRelativeTransform) + , mEnabled(true) + , mWaterLevel(0.f) + { } + + bool UnderwaterSwitchCallback::isUnderwater() + { + osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); + return mEnabled && viewPoint.z() < mWaterLevel; + } + + void UnderwaterSwitchCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (isUnderwater()) + return; + + traverse(node, nv); + } + + void UnderwaterSwitchCallback::setEnabled(bool enabled) + { + mEnabled = enabled; + } + void UnderwaterSwitchCallback::setWaterLevel(float waterLevel) + { + mWaterLevel = waterLevel; + } + + const float CelestialBody::mDistance = 1000.0f; + + CelestialBody::CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask) + : mVisibleMask(visibleMask) + { + mGeom = createTexturedQuad(numUvSets); + mGeom->getOrCreateStateSet(); + mTransform = new osg::PositionAttitudeTransform; + mTransform->setNodeMask(mVisibleMask); + mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); + mTransform->addChild(mGeom); + + parentNode->addChild(mTransform); + } + + void CelestialBody::setVisible(bool visible) + { + mTransform->setNodeMask(visible ? mVisibleMask : 0); + } + + Sun::Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager) + : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) + , mUpdater(new SunUpdater) + { + mTransform->addUpdateCallback(mUpdater); + + Resource::ImageManager& imageManager = *sceneManager.getImageManager(); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds")); + 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()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + osg::ref_ptr queryNode = new osg::Group; + // Need to render after the world geometry so we can correctly test for occlusions + osg::StateSet* stateset = queryNode->getOrCreateStateSet(); + stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); + stateset->setNestRenderBins(false); + // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun + if (!sceneManager.getForceShaders()) + { + osg::ref_ptr alphaFunc = new osg::AlphaFunc; + alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); + stateset->setAttributeAndModes(alphaFunc); + } + stateset->setTextureAttributeAndModes(0, sunTex); + 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); + + mTransform->addChild(queryNode); + + mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); + mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); + + createSunFlash(imageManager); + createSunGlare(); + } + + Sun::~Sun() + { + mTransform->removeUpdateCallback(mUpdater); + destroySunFlash(); + destroySunGlare(); + } + + void Sun::setColor(const osg::Vec4f& color) + { + mUpdater->mColor.r() = color.r(); + mUpdater->mColor.g() = color.g(); + mUpdater->mColor.b() = color.b(); + } + + void Sun::adjustTransparency(const float ratio) + { + mUpdater->mColor.a() = ratio; + if (mSunGlareCallback) + mSunGlareCallback->setGlareView(ratio); + if (mSunFlashCallback) + mSunFlashCallback->setGlareView(ratio); + } + + void Sun::setDirection(const osg::Vec3f& direction) + { + osg::Vec3f normalizedDirection = direction / direction.length(); + mTransform->setPosition(normalizedDirection * mDistance); + + osg::Quat quat; + quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); + mTransform->setAttitude(quat); + } + + void Sun::setGlareTimeOfDayFade(float val) + { + if (mSunGlareCallback) + mSunGlareCallback->setTimeOfDayFade(val); + } + + void Sun::setSunglare(bool enabled) + { + mSunGlareNode->setNodeMask(enabled ? ~0u : 0); + mSunFlashNode->setNodeMask(enabled ? ~0u : 0); + } + + osg::ref_ptr Sun::createOcclusionQueryNode(osg::Group* parent, bool queryVisible) + { + osg::ref_ptr oqn = new osg::OcclusionQueryNode; + oqn->setQueriesEnabled(true); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced + osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); +#else + osg::ref_ptr queryGeom = oqn->getQueryGeometry(); +#endif + + // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, + // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry + // is only called once. + // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. + queryGeom->setDataVariance(osg::Object::STATIC); + + // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, + // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to + // circumvent this. + queryGeom->setVertexArray(mGeom->getVertexArray()); + queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); + queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); + queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); + + // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. + oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); + // Still need a proper bounding sphere. + oqn->setInitialBound(queryGeom->getBound()); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + oqn->setQueryGeometry(queryGeom.release()); +#endif + + osg::StateSet* queryStateSet = new osg::StateSet; + if (queryVisible) + { + osg::ref_ptr depth = new SceneUtil::AutoDepth(osg::Depth::LEQUAL); + // 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); + queryStateSet->setAttributeAndModes(depth); + } + else + { + queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + oqn->setQueryStateSet(queryStateSet); + + parent->addChild(oqn); + + return oqn; + } + + void Sun::createSunFlash(Resource::ImageManager& imageManager) + { + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds")); + 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); + + mTransform->addChild(group); + + const float scale = 2.6f; + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, tex); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + mSunFlashNode = group; + + mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); + mSunFlashNode->addCullCallback(mSunFlashCallback); + } + + void Sun::destroySunFlash() + { + if (mSunFlashNode) + { + mSunFlashNode->removeCullCallback(mSunFlashCallback); + mSunFlashCallback = nullptr; + } + } + + void Sun::createSunGlare() + { + osg::ref_ptr camera = new osg::Camera; + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + camera->getOrCreateStateSet()->addUniform(new osg::Uniform("projectionMatrix", static_cast(camera->getProjectionMatrix()))); + SceneUtil::setCameraClearDepth(camera); + + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); + camera->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare))); + + // set up additive blending + osg::ref_ptr blendFunc = new osg::BlendFunc; + blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); + blendFunc->setDestination(osg::BlendFunc::ONE); + stateset->setAttributeAndModes(blendFunc); + + mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); + mSunGlareNode = camera; + + mSunGlareNode->addCullCallback(mSunGlareCallback); + + mTransform->addChild(camera); + } + + void Sun::destroySunGlare() + { + if (mSunGlareNode) + { + mSunGlareNode->removeCullCallback(mSunGlareCallback); + mSunGlareCallback = nullptr; + } + } + + Moon::Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type) + : CelestialBody(parentNode, scaleFactor, 2) + , mType(type) + , mPhase(MoonState::Phase::Unspecified) + , mUpdater(new MoonUpdater(*sceneManager.getImageManager(), sceneManager.getForceShaders())) + { + setPhase(MoonState::Phase::Full); + setVisible(true); + + mGeom->addUpdateCallback(mUpdater); + } + + Moon::~Moon() + { + mGeom->removeUpdateCallback(mUpdater); + } + + void Moon::adjustTransparency(const float ratio) + { + mUpdater->mTransparency *= ratio; + } + + void Moon::setState(const MoonState state) + { + float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; + float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; + + osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); + + osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); + mTransform->setPosition(direction * mDistance); + + // The moon quad is initially oriented facing down, so we need to offset its X-axis + // rotation to rotate it to face the camera when sitting at the horizon. + osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + mTransform->setAttitude(attX * rotZ); + + setPhase(state.mPhase); + mUpdater->mTransparency = state.mMoonAlpha; + mUpdater->mShadowBlend = state.mShadowBlend; + } + + void Moon::setAtmosphereColor(const osg::Vec4f& color) + { + mUpdater->mAtmosphereColor = color; + } + + void Moon::setColor(const osg::Vec4f& color) + { + mUpdater->mMoonColor = color; + } + + unsigned int Moon::getPhaseInt() const + { + switch (mPhase) + { + case MoonState::Phase::New: + return 0; + case MoonState::Phase::WaxingCrescent: + return 1; + case MoonState::Phase::WaningCrescent: + return 1; + case MoonState::Phase::FirstQuarter: + return 2; + case MoonState::Phase::ThirdQuarter: + return 2; + case MoonState::Phase::WaxingGibbous: + return 3; + case MoonState::Phase::WaningGibbous: + return 3; + case MoonState::Phase::Full: + return 4; + default: + return 0; + } + } + + void Moon::setPhase(const MoonState::Phase& phase) + { + if(mPhase == phase) + return; + + mPhase = phase; + + std::string textureName = "textures/tx_"; + + if (mType == Moon::Type_Secunda) + textureName += "secunda_"; + else + textureName += "masser_"; + + switch (mPhase) + { + case MoonState::Phase::New: + textureName += "new"; + break; + case MoonState::Phase::WaxingCrescent: + textureName += "one_wax"; + break; + case MoonState::Phase::FirstQuarter: + textureName += "half_wax"; + break; + case MoonState::Phase::WaxingGibbous: + textureName += "three_wax"; + break; + case MoonState::Phase::WaningCrescent: + textureName += "one_wan"; + break; + case MoonState::Phase::ThirdQuarter: + textureName += "half_wan"; + break; + case MoonState::Phase::WaningGibbous: + textureName += "three_wan"; + break; + case MoonState::Phase::Full: + textureName += "full"; + break; + default: + break; + } + + textureName += ".dds"; + + if (mType == Moon::Type_Secunda) + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); + else + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); + } + + int RainCounter::numParticlesToCreate(double dt) const + { + // limit dt to avoid large particle emissions if there are jumps in the simulation time + // 0.2 seconds is the same cap as used in Engine's frame loop + dt = std::min(dt, 0.2); + return ConstantRateCounter::numParticlesToCreate(dt); + } + + RainShooter::RainShooter() + : mAngle(0.f) + { } + + void RainShooter::shoot(osgParticle::Particle* particle) const + { + particle->setVelocity(mVelocity); + particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); + } + + void RainShooter::setVelocity(const osg::Vec3f& velocity) + { + mVelocity = velocity; + } + + void RainShooter::setAngle(float angle) + { + mAngle = angle; + } + + osg::Object* RainShooter::cloneType() const + { + return new RainShooter; + } + + osg::Object* RainShooter::clone(const osg::CopyOp &) const + { + return new RainShooter(*this); + } + + ModVertexAlphaVisitor::ModVertexAlphaVisitor(ModVertexAlphaVisitor::MeshType type) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mType(type) + { } + + void ModVertexAlphaVisitor::apply(osg::Geometry& geometry) + { + osg::ref_ptr colors = new osg::Vec4Array(geometry.getVertexArray()->getNumElements()); + for (unsigned int i=0; isize(); ++i) + { + float alpha = 1.f; + + switch (mType) + { + case ModVertexAlphaVisitor::Atmosphere: + { + // this is a cylinder, so every second vertex belongs to the bottom-most row + alpha = (i%2) ? 0.f : 1.f; + break; + } + case ModVertexAlphaVisitor::Clouds: + { + if (i>= 49 && i <= 64) + alpha = 0.f; // bottom-most row + else if (i>= 33 && i <= 48) + alpha = 0.25098; // second row + else + alpha = 1.f; + break; + } + case ModVertexAlphaVisitor::Stars: + { + if (geometry.getColorArray()) + { + osg::Vec4Array* origColors = static_cast(geometry.getColorArray()); + alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; + } + else + alpha = 1.f; + break; + } + } + + (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); + } + + geometry.setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } +} diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp new file mode 100644 index 0000000000..604e5909e8 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.hpp @@ -0,0 +1,344 @@ +#ifndef OPENMW_MWRENDER_SKYUTIL_H +#define OPENMW_MWRENDER_SKYUTIL_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Resource +{ + class ImageManager; + class SceneManager; +} + +namespace MWRender +{ + struct MoonUpdater; + class SunUpdater; + class SunFlashCallback; + class SunGlareCallback; + + struct WeatherResult + { + std::string mCloudTexture; + std::string mNextCloudTexture; + float mCloudBlendFactor; + + osg::Vec4f mFogColor; + + osg::Vec4f mAmbientColor; + + osg::Vec4f mSkyColor; + + // sun light color + osg::Vec4f mSunColor; + + // alpha is the sun transparency + osg::Vec4f mSunDiscColor; + + float mFogDepth; + + float mDLFogFactor; + float mDLFogOffset; + + float mWindSpeed; + float mBaseWindSpeed; + float mCurrentWindSpeed; + float mNextWindSpeed; + + float mCloudSpeed; + + float mGlareView; + + bool mNight; // use night skybox + float mNightFade; // fading factor for night skybox + + bool mIsStorm; + + std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; + + std::string mParticleEffect; + std::string mRainEffect; + float mPrecipitationAlpha; + + float mRainDiameter; + float mRainMinHeight; + float mRainMaxHeight; + float mRainSpeed; + float mRainEntranceSpeed; + int mRainMaxRaindrops; + + osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; + }; + + struct MoonState + { + enum class Phase + { + Full, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + New, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + Unspecified + }; + + float mRotationFromHorizon; + float mRotationFromNorth; + Phase mPhase; + float mShadowBlend; + float mMoonAlpha; + }; + + osg::ref_ptr createAlphaTrackingUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode = osg::Material::OFF); + + class OcclusionCallback + { + public: + OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal); + + protected: + float getVisibleRatio (osg::Camera* camera); + + private: + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + + std::map, float> mLastRatio; + }; + + class AtmosphereUpdater : public SceneUtil::StateSetUpdater + { + public: + void setEmissionColor(const osg::Vec4f& emissionColor); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mEmissionColor; + }; + + class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater + { + public: + AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders); + + void setFade(float fade); + + protected: + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mColor; + osg::ref_ptr mTexture; + bool mForceShaders; + }; + + class CloudUpdater : public SceneUtil::StateSetUpdater + { + public: + CloudUpdater(bool forceShaders); + + void setTexture(osg::ref_ptr texture); + + void setEmissionColor(const osg::Vec4f& emissionColor); + void setOpacity(float opacity); + void setTextureCoord(float timer); + + protected: + void setDefaults(osg::StateSet *stateset) override; + void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; + + private: + osg::ref_ptr mTexture; + osg::Vec4f mEmissionColor; + float mOpacity; + bool mForceShaders; + osg::Matrixf mTexMat; + }; + + /// Transform that removes the eyepoint of the modelview matrix, + /// i.e. its children are positioned relative to the camera. + class CameraRelativeTransform : public osg::Transform + { + public: + CameraRelativeTransform(); + + CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop); + + META_Node(MWRender, CameraRelativeTransform) + + const osg::Vec3f& getLastViewPoint() const; + + bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override; + + osg::BoundingSphere computeBound() const override; + + private: + // viewPoint for the current frame + mutable osg::Vec3f mViewPoint; + }; + + /// @brief Hides the node subgraph if the eye point is below water. + /// @note Must be added as cull callback. + /// @note Meant to be used on a node that is child of a CameraRelativeTransform. + /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. + class UnderwaterSwitchCallback : public SceneUtil::NodeCallback + { + public: + UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform); + bool isUnderwater(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv); + void setEnabled(bool enabled); + void setWaterLevel(float waterLevel); + + private: + osg::ref_ptr mCameraRelativeTransform; + bool mEnabled; + float mWaterLevel; + }; + + /// A base class for the sun and moons. + class CelestialBody + { + public: + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u); + + virtual ~CelestialBody() = default; + + virtual void adjustTransparency(const float ratio) = 0; + + void setVisible(bool visible); + + protected: + unsigned int mVisibleMask; + static const float mDistance; + osg::ref_ptr mTransform; + osg::ref_ptr mGeom; + }; + + class Sun : public CelestialBody + { + public: + Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager); + + ~Sun(); + + void setColor(const osg::Vec4f& color); + void adjustTransparency(const float ratio) override; + + void setDirection(const osg::Vec3f& direction); + void setGlareTimeOfDayFade(float val); + void setSunglare(bool enabled); + + private: + /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. + osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible); + + void createSunFlash(Resource::ImageManager& imageManager); + void destroySunFlash(); + + void createSunGlare(); + void destroySunGlare(); + + osg::ref_ptr mUpdater; + osg::ref_ptr mSunFlashNode; + osg::ref_ptr mSunGlareNode; + osg::ref_ptr mSunFlashCallback; + osg::ref_ptr mSunGlareCallback; + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + }; + + class Moon : public CelestialBody + { + public: + enum Type + { + Type_Masser = 0, + Type_Secunda + }; + + Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type); + + ~Moon(); + + void adjustTransparency(const float ratio) override; + void setState(const MoonState state); + void setAtmosphereColor(const osg::Vec4f& color); + void setColor(const osg::Vec4f& color); + + unsigned int getPhaseInt() const; + + private: + Type mType; + MoonState::Phase mPhase; + osg::ref_ptr mUpdater; + + void setPhase(const MoonState::Phase& phase); + }; + + class RainCounter : public osgParticle::ConstantRateCounter + { + public: + int numParticlesToCreate(double dt) const override; + }; + + class RainShooter : public osgParticle::Shooter + { + public: + RainShooter(); + + osg::Object* cloneType() const override; + + osg::Object* clone(const osg::CopyOp &) const override; + + void shoot(osgParticle::Particle* particle) const override; + + void setVelocity(const osg::Vec3f& velocity); + void setAngle(float angle); + + private: + osg::Vec3f mVelocity; + float mAngle; + }; + + class ModVertexAlphaVisitor : public osg::NodeVisitor + { + public: + enum MeshType + { + Atmosphere, + Stars, + Clouds + }; + + ModVertexAlphaVisitor(MeshType type); + + void apply(osg::Geometry& geometry) override; + + private: + MeshType mType; + }; +} + +#endif diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 528ce70ea3..879a2ef68b 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -33,7 +33,10 @@ namespace MWRender void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY) { - minX = 0, minY = 0, maxX = 0, maxY = 0; + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index 90bf42b841..edea556157 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include diff --git a/apps/openmw/mwrender/transparentpass.cpp b/apps/openmw/mwrender/transparentpass.cpp new file mode 100644 index 0000000000..239a11821b --- /dev/null +++ b/apps/openmw/mwrender/transparentpass.cpp @@ -0,0 +1,107 @@ +#include "transparentpass.hpp" + +#include +#include +#include + +#include + +#include +#include +#include + +namespace MWRender +{ + TransparentDepthBinCallback::TransparentDepthBinCallback(Shader::ShaderManager& shaderManager, bool postPass) + : mStateSet(new osg::StateSet) + , mPostPass(postPass) + { + osg::ref_ptr image = new osg::Image; + image->allocateImage(1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE); + image->setColor(osg::Vec4(1,1,1,1), 0, 0); + + osg::ref_ptr dummyTexture = new osg::Texture2D(image); + + constexpr osg::StateAttribute::OverrideValue modeOff = osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE; + constexpr osg::StateAttribute::OverrideValue modeOn = osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE; + + mStateSet->setTextureAttributeAndModes(0, dummyTexture); + + Shader::ShaderManager::DefineMap defines; + Stereo::Manager::instance().shaderStereoDefines(defines); + osg::ref_ptr vertex = shaderManager.getShader("blended_depth_postpass_vertex.glsl", defines, osg::Shader::VERTEX); + osg::ref_ptr fragment = shaderManager.getShader("blended_depth_postpass_fragment.glsl", defines, osg::Shader::FRAGMENT); + + mStateSet->setAttributeAndModes(new osg::BlendFunc, modeOff); + mStateSet->setAttributeAndModes(shaderManager.getProgram(vertex, fragment), modeOn); + + for (unsigned int unit = 1; unit < 8; ++unit) + mStateSet->setTextureMode(unit, GL_TEXTURE_2D, modeOff); + } + + void TransparentDepthBinCallback::drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) + { + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + + bool validFbo = false; + unsigned int frameId = state.getFrameStamp()->getFrameNumber() % 2; + + const auto& fbo = mFbo[frameId]; + const auto& msaaFbo = mMsaaFbo[frameId]; + const auto& opaqueFbo = mOpaqueFbo[frameId]; + + if (bin->getStage()->getMultisampleResolveFramebufferObject() && bin->getStage()->getMultisampleResolveFramebufferObject() == fbo) + validFbo = true; + else if (bin->getStage()->getFrameBufferObject() && (bin->getStage()->getFrameBufferObject() == fbo || bin->getStage()->getFrameBufferObject() == msaaFbo)) + validFbo = true; + + if (!validFbo) + { + bin->drawImplementation(renderInfo, previous); + return; + } + + const osg::Texture* tex = opaqueFbo->getAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER).getTexture(); + + if (Stereo::getMultiview()) + { + if (!mMultiviewResolve[frameId]) + { + mMultiviewResolve[frameId] = std::make_unique(msaaFbo ? msaaFbo : fbo, opaqueFbo, GL_DEPTH_BUFFER_BIT); + } + mMultiviewResolve[frameId]->resolveImplementation(state); + } + else + { + opaqueFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, tex->getTextureWidth(), tex->getTextureHeight(), 0, 0, tex->getTextureWidth(), tex->getTextureHeight(), GL_DEPTH_BUFFER_BIT, GL_NEAREST); + } + + msaaFbo ? msaaFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER) : fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + // draws scene into primary attachments + bin->drawImplementation(renderInfo, previous); + + if (!mPostPass) + return; + + opaqueFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + osg::ref_ptr restore = bin->getStateSet(); + bin->setStateSet(mStateSet); + // draws transparent post-pass to populate a postprocess friendly depth texture with alpha-clipped geometry + bin->drawImplementation(renderInfo, previous); + bin->setStateSet(restore); + + msaaFbo ? msaaFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER) : fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.checkGLErrors("after TransparentDepthBinCallback::drawImplementation"); + } + + void TransparentDepthBinCallback::dirtyFrame(int frameId) + { + if (mMultiviewResolve[frameId]) + mMultiviewResolve[frameId]->dirty(); + } + +} diff --git a/apps/openmw/mwrender/transparentpass.hpp b/apps/openmw/mwrender/transparentpass.hpp new file mode 100644 index 0000000000..652fe8b8b7 --- /dev/null +++ b/apps/openmw/mwrender/transparentpass.hpp @@ -0,0 +1,46 @@ +#ifndef OPENMW_MWRENDER_TRANSPARENTPASS_H +#define OPENMW_MWRENDER_TRANSPARENTPASS_H + +#include + +#include +#include + +#include + +#include "postprocessor.hpp" + +namespace Shader +{ + class ShaderManager; +} + +namespace Stereo +{ + class MultiviewFramebufferResolve; +} + +namespace MWRender +{ + class TransparentDepthBinCallback : public osgUtil::RenderBin::DrawCallback + { + public: + TransparentDepthBinCallback(Shader::ShaderManager& shaderManager, bool postPass); + + void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override; + void dirtyFrame(int frameId); + + std::array, 2> mFbo; + std::array, 2> mMsaaFbo; + std::array, 2> mOpaqueFbo; + + std::array, 2> mMultiviewResolve; + + private: + osg::ref_ptr mStateSet; + bool mPostPass; + }; + +} + +#endif diff --git a/apps/openmw/mwrender/viewovershoulder.cpp b/apps/openmw/mwrender/viewovershoulder.cpp deleted file mode 100644 index 799e34c992..0000000000 --- a/apps/openmw/mwrender/viewovershoulder.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "viewovershoulder.hpp" - -#include - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/refdata.hpp" - -#include "../mwmechanics/drawstate.hpp" - -namespace MWRender -{ - - ViewOverShoulderController::ViewOverShoulderController(Camera* camera) : - mCamera(camera), mMode(Mode::RightShoulder), - mAutoSwitchShoulder(Settings::Manager::getBool("auto switch shoulder", "Camera")), - mOverShoulderHorizontalOffset(30.f), mOverShoulderVerticalOffset(-10.f) - { - osg::Vec2f offset = Settings::Manager::getVector2("view over shoulder offset", "Camera"); - mOverShoulderHorizontalOffset = std::abs(offset.x()); - mOverShoulderVerticalOffset = offset.y(); - mDefaultShoulderIsRight = offset.x() >= 0; - - mCamera->enableDynamicCameraDistance(true); - mCamera->enableCrosshairInThirdPersonMode(true); - mCamera->setFocalPointTargetOffset(offset); - } - - void ViewOverShoulderController::update() - { - if (mCamera->isFirstPerson()) - return; - - Mode oldMode = mMode; - auto ptr = mCamera->getTrackingPtr(); - bool combat = ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Nothing; - if (combat && !mCamera->isVanityOrPreviewModeEnabled()) - mMode = Mode::Combat; - else if (MWBase::Environment::get().getWorld()->isSwimming(ptr)) - mMode = Mode::Swimming; - else if (oldMode == Mode::Combat || oldMode == Mode::Swimming) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - if (mAutoSwitchShoulder && (mMode == Mode::LeftShoulder || mMode == Mode::RightShoulder)) - trySwitchShoulder(); - - if (oldMode == mMode) - return; - - if (mCamera->getMode() == Camera::Mode::Vanity) - // Player doesn't touch controls for a long time. Transition should be very slow. - mCamera->setFocalPointTransitionSpeed(0.2f); - else if ((oldMode == Mode::Combat || mMode == Mode::Combat) && mCamera->getMode() == Camera::Mode::Normal) - // Transition to/from combat mode and we are not it preview mode. Should be fast. - mCamera->setFocalPointTransitionSpeed(5.f); - else - mCamera->setFocalPointTransitionSpeed(1.f); // Default transition speed. - - switch (mMode) - { - case Mode::RightShoulder: - mCamera->setFocalPointTargetOffset({mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::LeftShoulder: - mCamera->setFocalPointTargetOffset({-mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::Combat: - case Mode::Swimming: - default: - mCamera->setFocalPointTargetOffset({0, 15}); - } - } - - void ViewOverShoulderController::trySwitchShoulder() - { - if (mCamera->getMode() != Camera::Mode::Normal) - return; - - const float limitToSwitch = 120; // switch to other shoulder if wall is closer than this limit - const float limitToSwitchBack = 300; // switch back to default shoulder if there is no walls at this distance - - auto orient = osg::Quat(mCamera->getYaw(), osg::Vec3d(0,0,1)); - osg::Vec3d playerPos = mCamera->getFocalPoint() - mCamera->getFocalPointOffset(); - - MWBase::World* world = MWBase::Environment::get().getWorld(); - osg::Vec3d sideOffset = orient * osg::Vec3d(world->getHalfExtents(mCamera->getTrackingPtr()).x() - 1, 0, 0); - float rayRight = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 0, 0), limitToSwitchBack + 1); - float rayLeft = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 0, 0), limitToSwitchBack + 1); - float rayRightForward = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 3, 0), limitToSwitchBack + 1); - float rayLeftForward = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 3, 0), limitToSwitchBack + 1); - float distRight = std::min(rayRight, rayRightForward); - float distLeft = std::min(rayLeft, rayLeftForward); - - if (distLeft < limitToSwitch && distRight > limitToSwitchBack) - mMode = Mode::RightShoulder; - else if (distRight < limitToSwitch && distLeft > limitToSwitchBack) - mMode = Mode::LeftShoulder; - else if (distRight > limitToSwitchBack && distLeft > limitToSwitchBack) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - } - -} diff --git a/apps/openmw/mwrender/viewovershoulder.hpp b/apps/openmw/mwrender/viewovershoulder.hpp deleted file mode 100644 index 80ac308656..0000000000 --- a/apps/openmw/mwrender/viewovershoulder.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef VIEWOVERSHOULDER_H -#define VIEWOVERSHOULDER_H - -#include "camera.hpp" - -namespace MWRender -{ - - class ViewOverShoulderController - { - public: - ViewOverShoulderController(Camera* camera); - - void update(); - - private: - void trySwitchShoulder(); - enum class Mode { RightShoulder, LeftShoulder, Combat, Swimming }; - - Camera* mCamera; - Mode mMode; - bool mAutoSwitchShoulder; - float mOverShoulderHorizontalOffset; - float mOverShoulderVerticalOffset; - bool mDefaultShoulderIsRight; - }; - -} - -#endif // VIEWOVERSHOULDER_H diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index f9f9dc74ca..a7a28614cb 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -19,7 +19,7 @@ namespace MWRender /// another mask, or what type of node this mask is usually set on. /// @note The mask values are not serialized within models, nor used in any other way that would break backwards /// compatibility if the enumeration values were to be changed. Feel free to change them when it makes sense. - enum VisMask + enum VisMask : unsigned int { Mask_UpdateVisitor = 0x1, // reserved for separating UpdateVisitors from CullVisitors @@ -53,9 +53,14 @@ namespace MWRender Mask_PreCompile = (1<<18), // Set on a camera's cull mask to enable the LightManager - Mask_Lighting = (1<<19) + Mask_Lighting = (1<<19), + + Mask_Groundcover = (1<<20), }; + // 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; + } #endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index b569f1bfab..41fbc1c05b 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -10,11 +10,11 @@ #include #include #include +#include #include -#include -#include +#include #include #include @@ -25,21 +25,28 @@ #include #include +#include #include +#include #include +#include #include +#include +#include #include #include -#include +#include #include #include "../mwworld/cellstore.hpp" +#include "../mwbase/environment.hpp" + #include "vismask.hpp" #include "ripplesimulation.hpp" #include "renderbin.hpp" @@ -55,20 +62,17 @@ namespace MWRender /// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); class ClipCullNode : public osg::Group { - class PlaneCullCallback : public osg::NodeCallback + class PlaneCullCallback : public SceneUtil::NodeCallback { public: /// @param cullPlane The culling plane (in world space). PlaneCullCallback(const osg::Plane* cullPlane) - : osg::NodeCallback() - , mCullPlane(cullPlane) + : mCullPlane(cullPlane) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::Polytope::PlaneList origPlaneList = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); osg::Plane plane = *mCullPlane; @@ -80,7 +84,7 @@ class ClipCullNode : public osg::Group cv->getProjectionCullingStack().back().getFrustum().add(plane); - traverse(node, nv); + traverse(node, cv); // undo cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); @@ -90,7 +94,7 @@ class ClipCullNode : public osg::Group const osg::Plane* mCullPlane; }; - class FlipCallback : public osg::NodeCallback + class FlipCallback : public SceneUtil::NodeCallback { public: FlipCallback(const osg::Plane* cullPlane) @@ -98,9 +102,8 @@ class ClipCullNode : public osg::Group { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); osg::Vec3d eyePoint = cv->getEyePoint(); osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); @@ -120,7 +123,7 @@ class ClipCullNode : public osg::Group modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } @@ -135,7 +138,7 @@ public: mClipNodeTransform = new osg::Group; mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); - addChild(mClipNodeTransform); + osg::Group::addChild(mClipNodeTransform); mClipNode = new osg::ClipNode; @@ -163,31 +166,28 @@ private: /// This callback on the Camera has the effect of a RELATIVE_RF_INHERIT_VIEWPOINT transform mode (which does not exist in OSG). /// We want to keep the View Point of the parent camera so we will not have to recreate LODs. -class InheritViewPointCallback : public osg::NodeCallback +class InheritViewPointCallback : public SceneUtil::NodeCallback { public: InheritViewPointCallback() {} - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); osg::ref_ptr modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); cv->popModelViewMatrix(); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT); - traverse(node, nv); + traverse(node, cv); } }; /// Moves water mesh away from the camera slightly if the camera gets too close on the Z axis. /// The offset works around graphics artifacts that occurred with the GL_DEPTH_CLAMP when the camera gets extremely close to the mesh (seen on NVIDIA at least). /// Must be added as a Cull callback. -class FudgeCallback : public osg::NodeCallback +class FudgeCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - const float fudge = 0.2; if (std::abs(cv->getEyeLocal().z()) < fudge) { @@ -200,18 +200,48 @@ public: modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,diff)); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } else - traverse(node, nv); + traverse(node, cv); } }; +class RainIntensityUpdater : public SceneUtil::StateSetUpdater +{ +public: + RainIntensityUpdater() + : mRainIntensity(0.f) + { + } + + void setRainIntensity(float rainIntensity) + { + mRainIntensity = rainIntensity; + } + +protected: + void setDefaults(osg::StateSet* stateset) override + { + osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); + stateset->addUniform(rainIntensityUniform.get()); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override + { + osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); + if (rainIntensityUniform != nullptr) + rainIntensityUniform->set(mRainIntensity); + } + +private: + float mRainIntensity; +}; + osg::ref_ptr readPngImage (const std::string& file) { - // use boost in favor of osgDB::readImage, to handle utf-8 path issues on Windows - boost::filesystem::ifstream inStream; + std::ifstream inStream; inStream.open(file, std::ios_base::in | std::ios_base::binary); if (inStream.fail()) Log(Debug::Error) << "Error: Failed to open " << file; @@ -228,63 +258,44 @@ osg::ref_ptr readPngImage (const std::string& file) return result.getImage(); } - -class Refraction : public osg::Camera +class Refraction : public SceneUtil::RTTNode { public: - Refraction() + Refraction(uint32_t rttSize) + : RTTNode(rttSize, rttSize, 0, false, 1, StereoAwareness::Aware) + , mNodeMask(Refraction::sDefaultCullMask) { - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - setName("RefractionCamera"); - setCullCallback(new InheritViewPointCallback); - - setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); - setNodeMask(Mask_RenderToTexture); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the scene is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + mClipCullNode = new ClipCullNode; + } + + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setName("RefractionCamera"); + camera->addCullCallback(new InheritViewPointCallback); + camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); + osg::ref_ptr fog(new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); - getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); - - mClipCullNode = new ClipCullNode; - addChild(mClipCullNode); - - mRefractionTexture = new osg::Texture2D; - mRefractionTexture->setTextureSize(rttSize, rttSize); - mRefractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setInternalFormat(GL_RGB); - mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - attach(osg::Camera::COLOR_BUFFER, mRefractionTexture); + camera->getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); - mRefractionDepthTexture = new osg::Texture2D; - mRefractionDepthTexture->setTextureSize(rttSize, rttSize); - mRefractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); - mRefractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setSourceType(GL_UNSIGNED_INT); - mRefractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); - attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); + if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + } - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setScene(osg::Node* scene) @@ -297,92 +308,77 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f,std::max(0.0f, - Settings::Manager::getFloat("refraction scale", "Water"))); + const float refractionScale = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f); - setViewMatrix(osg::Matrix::scale(1,1,refractionScale) * - osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel)); - - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); - } + mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * + osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); - osg::Texture2D* getRefractionTexture() const - { - return mRefractionTexture.get(); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } - osg::Texture2D* getRefractionDepthTexture() const + void showWorld(bool show) { - return mRefractionDepthTexture.get(); + if (show) + mNodeMask = Refraction::sDefaultCullMask; + else + mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; } private: osg::ref_ptr mClipCullNode; - osg::ref_ptr mRefractionTexture; - osg::ref_ptr mRefractionDepthTexture; osg::ref_ptr mScene; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + + unsigned int mNodeMask; + + static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover; }; -class Reflection : public osg::Camera +class Reflection : public SceneUtil::RTTNode { public: - Reflection(bool isInterior) + Reflection(uint32_t rttSize, bool isInterior) + : RTTNode(rttSize, rttSize, 0, false, 0, StereoAwareness::Aware) { - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - setName("ReflectionCamera"); - setCullCallback(new InheritViewPointCallback); - setInterior(isInterior); - setNodeMask(Mask_RenderToTexture); - - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); - - mReflectionTexture = new osg::Texture2D; - mReflectionTexture->setTextureSize(rttSize, rttSize); - mReflectionTexture->setInternalFormat(GL_RGB); - mReflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + mClipCullNode = new ClipCullNode; + } - attach(osg::Camera::COLOR_BUFFER, mReflectionTexture); + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setName("ReflectionCamera"); + camera->addCullCallback(new InheritViewPointCallback); // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. - osg::ref_ptr frontFace (new osg::FrontFace); + osg::ref_ptr frontFace(new osg::FrontFace); frontFace->setMode(osg::FrontFace::CLOCKWISE); - getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); + camera->getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); - mClipCullNode = new ClipCullNode; - addChild(mClipCullNode); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); + + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + } - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setInterior(bool isInterior) { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(4, std::max(isInterior ? 2 : 0, reflectionDetail)); - unsigned int extraMask = 0; - if(reflectionDetail >= 1) extraMask |= Mask_Terrain; - if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect|Mask_ParticleSystem|Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player|Mask_Actor; - setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask); + mInterior = isInterior; + mNodeMask = calcNodeMask(); } void setWaterLevel(float waterLevel) { - setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); + mViewMatrix = osg::Matrix::scale(1, 1, -1) * osg::Matrix::translate(0, 0, 2 * waterLevel); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, 1), osg::Vec3d(0, 0, waterLevel))); } void setScene(osg::Node* scene) @@ -393,15 +389,34 @@ public: mClipCullNode->addChild(scene); } - osg::Texture2D* getReflectionTexture() const + void showWorld(bool show) { - return mReflectionTexture.get(); + if (show) + mNodeMask = calcNodeMask(); + else + mNodeMask = calcNodeMask() & ~sToggleWorldMask; } private: - osg::ref_ptr mReflectionTexture; + + unsigned int calcNodeMask() + { + int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); + reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); + unsigned int extraMask = 0; + if(reflectionDetail >= 1) extraMask |= Mask_Terrain; + if(reflectionDetail >= 2) extraMask |= Mask_Static; + if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; + if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; + return Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + } + osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; + osg::Node::NodeMask mNodeMask; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + bool mInterior; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -428,7 +443,8 @@ public: Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem *resourceSystem, osgUtil::IncrementalCompileOperation *ico, const std::string& resourcePath) - : mParent(parent) + : mRainIntensityUpdater(nullptr) + , mParent(parent) , mSceneRoot(sceneRoot) , mResourceSystem(resourceSystem) , mResourcePath(resourcePath) @@ -436,14 +452,17 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mToggled(true) , mTop(0) , mInterior(false) + , mShowWorld(true) , mCullCallback(nullptr) + , mShaderWaterStateSetUpdater(nullptr) { - mSimulation.reset(new RippleSimulation(mSceneRoot, resourceSystem)); + mSimulation = std::make_unique(mSceneRoot, resourceSystem); mWaterGeom = SceneUtil::createWaterGeometry(Constants::CellSizeInUnits*150, 40, 900); mWaterGeom->setDrawCallback(new DepthClampCallback); mWaterGeom->setNodeMask(Mask_Water); mWaterGeom->setDataVariance(osg::Object::STATIC); + mWaterGeom->setName("Water Geometry"); mWaterNode = new osg::PositionAttitudeTransform; mWaterNode->setName("Water Root"); @@ -454,14 +473,13 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem osg::ref_ptr geom2 (osg::clone(mWaterGeom.get(), osg::CopyOp::DEEP_COPY_NODES)); createSimpleWaterStateSet(geom2, Fallback::Map::getFloat("Water_Map_Alpha")); geom2->setNodeMask(Mask_SimpleWater); + geom2->setName("Simple Water Geometry"); mWaterNode->addChild(geom2); mSceneRoot->addChild(mWaterNode); setHeight(mTop); - mRainIntensityUniform = new osg::Uniform("rainIntensity",(float) 0.0); - updateWaterMaterial(); if (ico) @@ -491,29 +509,33 @@ void Water::setCullCallback(osg::Callback* callback) } } -osg::Uniform *Water::getRainIntensityUniform() -{ - return mRainIntensityUniform.get(); -} - void Water::updateWaterMaterial() { + if (mShaderWaterStateSetUpdater) + { + mWaterNode->removeCullCallback(mShaderWaterStateSetUpdater); + mShaderWaterStateSetUpdater = nullptr; + } if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } + mWaterNode->setStateSet(nullptr); + mWaterGeom->setStateSet(nullptr); + mWaterGeom->setUpdateCallback(nullptr); + if (Settings::Manager::getBool("shader", "Water")) { - mReflection = new Reflection(mInterior); + unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); + + mReflection = new Reflection(rttSize, mInterior); mReflection->setWaterLevel(mTop); mReflection->setScene(mSceneRoot); if (mCullCallback) @@ -522,7 +544,7 @@ void Water::updateWaterMaterial() if (Settings::Manager::getBool("refraction", "Water")) { - mRefraction = new Refraction; + mRefraction = new Refraction(rttSize); mRefraction->setWaterLevel(mTop); mRefraction->setScene(mSceneRoot); if (mCullCallback) @@ -530,7 +552,9 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } - createShaderWaterStateSet(mWaterGeom, mReflection, mRefraction); + showWorld(mShowWorld); + + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); @@ -538,25 +562,32 @@ void Water::updateWaterMaterial() updateVisible(); } -osg::Camera *Water::getReflectionCamera() +osg::Node *Water::getReflectionNode() { return mReflection; } -osg::Camera *Water::getRefractionCamera() +osg::Node* Water::getRefractionNode() { return mRefraction; } +osg::Vec3d Water::getPosition() const +{ + return mWaterNode->getPosition(); +} + void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); node->setStateSet(stateset); + node->setUpdateCallback(nullptr); + mRainIntensityUpdater = nullptr; // Add animated textures std::vector > textures; - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; i tex (new osg::Texture2D(mResourceSystem->getImageManager()->getImage(texname.str()))); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + mResourceSystem->getSceneManager()->applyFilterSettings(tex); textures.push_back(tex); } @@ -574,7 +606,7 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) float fps = Fallback::Map::getFloat("Water_SurfaceFPS"); osg::ref_ptr controller (new NifOsg::FlipController(0, 1.f/fps, textures)); - controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); + controller->setSource(std::make_shared()); node->setUpdateCallback(controller); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); @@ -588,17 +620,80 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) sceneManager->setForceShaders(oldValue); } +class ShaderWaterStateSetUpdater : public SceneUtil::StateSetUpdater +{ +public: + ShaderWaterStateSetUpdater(Water* water, Reflection* reflection, Refraction* refraction, osg::ref_ptr program, osg::ref_ptr normalMap) + : mWater(water) + , mReflection(reflection) + , mRefraction(refraction) + , mProgram(program) + , mNormalMap(normalMap) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("normalMap", 0)); + stateset->setTextureAttributeAndModes(0, mNormalMap, osg::StateAttribute::ON); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("reflectionMap", 1)); + if (mRefraction) + { + stateset->addUniform(new osg::Uniform("refractionMap", 2)); + stateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); + stateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); + } + else + { + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + stateset->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWater->getPosition()))); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + stateset->setTextureAttributeAndModes(1, mReflection->getColorTexture(cv), osg::StateAttribute::ON); + + if (mRefraction) + { + stateset->setTextureAttributeAndModes(2, mRefraction->getColorTexture(cv), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(3, mRefraction->getDepthTexture(cv), osg::StateAttribute::ON); + } + stateset->getUniform("nodePosition")->set(osg::Vec3f(mWater->getPosition())); + } + +private: + Water* mWater; + Reflection* mReflection; + Refraction* mRefraction; + osg::ref_ptr mProgram; + osg::ref_ptr mNormalMap; +}; + void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction) { // use a define map to conditionally compile the shader std::map defineMap; - defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); + 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); + + Stereo::Manager::instance().shaderStereoDefines(defineMap); Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr vertexShader (shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr fragmentShader (shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader(shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr fragmentShader(shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr program = shaderMgr.getProgram(vertexShader, fragmentShader); - osg::ref_ptr normalMap (new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); + osg::ref_ptr normalMap(new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); if (normalMap->getImage()) normalMap->getImage()->flipVertical(); @@ -607,44 +702,13 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R normalMap->setMaxAnisotropy(16); normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + - osg::ref_ptr shaderStateset = new osg::StateSet; - shaderStateset->addUniform(new osg::Uniform("normalMap", 0)); - shaderStateset->addUniform(new osg::Uniform("reflectionMap", 1)); - - shaderStateset->setTextureAttributeAndModes(0, normalMap, osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(1, reflection->getReflectionTexture(), osg::StateAttribute::ON); - - if (refraction) - { - shaderStateset->setTextureAttributeAndModes(2, refraction->getRefractionTexture(), osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(3, refraction->getRefractionDepthTexture(), osg::StateAttribute::ON); - shaderStateset->addUniform(new osg::Uniform("refractionMap", 2)); - shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); - } - else - { - shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); - - osg::ref_ptr depth (new osg::Depth); - depth->setWriteMask(false); - shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - - shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + mRainIntensityUpdater = new RainIntensityUpdater(); + node->setUpdateCallback(mRainIntensityUpdater); - shaderStateset->addUniform(mRainIntensityUniform.get()); - - osg::ref_ptr program (new osg::Program); - program->addShader(vertexShader); - program->addShader(fragmentShader); - shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); - - node->setStateSet(shaderStateset); - node->setUpdateCallback(nullptr); + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, program, normalMap); + node->addCullCallback(mShaderWaterStateSetUpdater); } void Water::processChangedSettings(const Settings::CategorySettingVector& settings) @@ -658,13 +722,11 @@ Water::~Water() if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } @@ -672,7 +734,7 @@ Water::~Water() void Water::listAssetsToPreload(std::vector &textures) { - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; isetInterior(mInterior); - - // create a new StateSet to prevent threading issues - osg::ref_ptr nodeStateSet (new osg::StateSet); - nodeStateSet->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWaterNode->getPosition()))); - mWaterNode->setStateSet(nodeStateSet); } void Water::setHeight(const float height) @@ -727,6 +784,12 @@ void Water::setHeight(const float height) mRefraction->setWaterLevel(mTop); } +void Water::setRainIntensity(float rainIntensity) +{ + if (mRainIntensityUpdater) + mRainIntensityUpdater->setRainIntensity(rainIntensity); +} + void Water::update(float dt) { mSimulation->update(dt); @@ -735,11 +798,11 @@ void Water::update(float dt) void Water::updateVisible() { bool visible = mEnabled && mToggled; - mWaterNode->setNodeMask(visible ? ~0 : 0); + mWaterNode->setNodeMask(visible ? ~0u : 0u); if (mRefraction) - mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0); + mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0u); if (mReflection) - mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0); + mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0u); } bool Water::toggle() @@ -790,4 +853,13 @@ void Water::clearRipples() mSimulation->clear(); } +void Water::showWorld(bool show) +{ + if (mReflection) + mReflection->showWorld(show); + if (mRefraction) + mRefraction->showWorld(show); + mShowWorld = show; +} + } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 3787ef4268..c7acbf708f 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -6,8 +6,7 @@ #include #include -#include -#include +#include #include @@ -17,6 +16,7 @@ namespace osg class PositionAttitudeTransform; class Geometry; class Node; + class Callback; } namespace osgUtil @@ -46,11 +46,12 @@ namespace MWRender class Refraction; class Reflection; class RippleSimulation; + class RainIntensityUpdater; /// Water rendering class Water { - osg::ref_ptr mRainIntensityUniform; + osg::ref_ptr mRainIntensityUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; @@ -70,8 +71,10 @@ namespace MWRender bool mToggled; float mTop; bool mInterior; + bool mShowWorld; osg::Callback* mCullCallback; + osg::ref_ptr mShaderWaterStateSetUpdater; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); @@ -112,15 +115,18 @@ namespace MWRender void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); + void setRainIntensity(const float rainIntensity); void update(float dt); - osg::Camera *getReflectionCamera(); - osg::Camera *getRefractionCamera(); + osg::Node* getReflectionNode(); + osg::Node* getRefractionNode(); + + osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); - osg::Uniform *getRainIntensityUniform(); + void showWorld(bool show); }; } diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 0c2a11466b..0572eae3be 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -60,13 +60,13 @@ WeaponAnimation::~WeaponAnimation() } -void WeaponAnimation::attachArrow(MWWorld::Ptr actor) +void WeaponAnimation::attachArrow(const MWWorld::Ptr& actor) { const MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weaponSlot == inv.end()) return; - if (weaponSlot->getTypeName() != typeid(ESM::Weapon).name()) + if (weaponSlot->getType() != ESM::Weapon::sRecordId) return; int type = weaponSlot->get()->mBase->mData.mType; @@ -109,7 +109,7 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end()) return; - if (weapon->getTypeName() != typeid(ESM::Weapon).name()) + if (weapon->getType() != ESM::Weapon::sRecordId) return; // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. @@ -172,14 +172,13 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) } } -void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) +void WeaponAnimation::addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { mSpineControllers[i] = nullptr; - std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); + Animation::NodeMap::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); if (found != nodes.end()) { osg::Node* node = found->second; diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index d02107333e..125587c1bd 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -34,7 +34,7 @@ namespace MWRender virtual ~WeaponAnimation(); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. - void attachArrow(MWWorld::Ptr actor); + void attachArrow(const MWWorld::Ptr &actor); void detachArrow(MWWorld::Ptr actor); @@ -42,8 +42,7 @@ namespace MWRender void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. - void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + void addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index 499c2f672d..47cddee083 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -14,6 +14,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/aiactivate.hpp" #include "../mwmechanics/aiescort.hpp" @@ -44,13 +45,17 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view objectID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; i(duration), x, y, z); + if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) + return; + + MWMechanics::AiEscort escortPackage(actorID, static_cast(duration), x, y, z, repeat); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; @@ -127,10 +140,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view actorID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view cellID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; @@ -145,15 +158,20 @@ namespace MWScript Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; igetStore().get().find(cellID); + if (!MWBase::Environment::get().getWorld()->getStore().get().search(std::string{cellID})) + return; - MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z); + MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z, repeat); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; @@ -169,9 +187,11 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getAiSequence().isPackageDone(); + bool done = false; + if (ptr.getClass().isActor()) + done = ptr.getClass().getCreatureStats(ptr).getAiSequence().isPackageDone(); - runtime.push (value); + runtime.push(done); } }; @@ -208,8 +228,7 @@ namespace MWScript { if(!repeat) repeat = true; - Interpreter::Type_Integer idleValue = runtime[0].mInteger; - idleValue = std::min(255, std::max(0, idleValue)); + Interpreter::Type_Integer idleValue = std::clamp(runtime[0].mInteger, 0, 255); idleList.push_back(idleValue); runtime.pop(); --arg0; @@ -222,9 +241,12 @@ namespace MWScript --arg0; } - // discard additional arguments (reset), because we have no idea what they mean. + // discard additional arguments, because we have no idea what they mean. for (unsigned int i=0; i class OpGetAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; + MWMechanics::AiSetting mIndex; public: - OpGetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + OpGetAiSetting(MWMechanics::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getModified()); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats (ptr).getAiSetting(mIndex).getModified(false); + runtime.push(value); } }; template class OpModAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; + MWMechanics::AiSetting mIndex; public: - OpModAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + OpModAiSetting(MWMechanics::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { @@ -257,6 +282,9 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value; ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified); @@ -266,20 +294,20 @@ namespace MWScript template class OpSetAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; + MWMechanics::AiSetting mIndex; public: - OpSetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + OpSetAiSetting(MWMechanics::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - - MWMechanics::Stat stat = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex); - stat.setModified(value, 0); - ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, stat); - ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, value); + if(ptr.getClass().isActor()) + { + ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, value); + ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, value); + } } }; @@ -292,7 +320,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view actorID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; @@ -307,10 +335,14 @@ namespace MWScript Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; i(ptr.getClass().getCreatureStats (ptr).getAiSequence().getLastRunTypeId()); + Interpreter::Type_Integer value = -1; + if(ptr.getClass().isActor()) + { + const auto& stats = ptr.getClass().getCreatureStats(ptr); + if(!stats.isDead() || !stats.isDeathAnimationFinished()) + { + value = static_cast(stats.getAiSequence().getLastRunTypeId()); + } + } runtime.push (value); } @@ -377,7 +421,7 @@ namespace MWScript { MWWorld::Ptr observer = R()(runtime, false); // required=false - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view actorID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); @@ -400,7 +444,7 @@ namespace MWScript MWWorld::Ptr source = R()(runtime); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view actorID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); @@ -421,26 +465,28 @@ namespace MWScript void execute (Interpreter::Runtime &runtime) override { MWWorld::Ptr actor = R()(runtime); - std::string testedTargetId = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view testedTargetId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - bool targetsAreEqual = false; - MWWorld::Ptr targetPtr; - if (creatureStats.getAiSequence().getCombatTarget (targetPtr)) - { - if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId) - targetsAreEqual = true; - } - else if (testedTargetId == "player") // Currently the player ID is hardcoded + if (actor.getClass().isActor()) { - MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); - bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; - bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); - targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); + const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); + MWWorld::Ptr targetPtr; + if (creatureStats.getAiSequence().getCombatTarget(targetPtr)) + { + if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId) + targetsAreEqual = true; + } + else if (testedTargetId == "player") // Currently the player ID is hardcoded + { + MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); + bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; + bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); + targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); + } } - runtime.push(int(targetsAreEqual)); + runtime.push(targetsAreEqual); } }; @@ -451,7 +497,7 @@ namespace MWScript void execute (Interpreter::Runtime &runtime) override { MWWorld::Ptr actor = R()(runtime); - std::string targetID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view targetID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); @@ -467,8 +513,9 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = R()(runtime); - MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - creatureStats.getAiSequence().stopCombat(); + if (!actor.getClass().isActor()) + return; + MWBase::Environment::get().getMechanicsManager()->stopCombat(actor); } }; @@ -498,74 +545,76 @@ namespace MWScript Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); + if (!actor.getClass().isActor() || actor == MWMechanics::getPlayer()) + return; + MWMechanics::AiFace facePackage(x, y); actor.getClass().getCreatureStats(actor).getAiSequence().stack(facePackage, actor); } }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment3 (Compiler::Ai::opcodeAIActivate, new OpAiActivate); - interpreter.installSegment3 (Compiler::Ai::opcodeAIActivateExplicit, new OpAiActivate); - interpreter.installSegment3 (Compiler::Ai::opcodeAiTravel, new OpAiTravel); - interpreter.installSegment3 (Compiler::Ai::opcodeAiTravelExplicit, new OpAiTravel); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscort, new OpAiEscort); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortExplicit, new OpAiEscort); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCell, new OpAiEscortCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCellExplicit, new OpAiEscortCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiWander, new OpAiWander); - interpreter.installSegment3 (Compiler::Ai::opcodeAiWanderExplicit, new OpAiWander); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollow, new OpAiFollow); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowExplicit, new OpAiFollow); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCell, new OpAiFollowCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCellExplicit, new OpAiFollowCell); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDone, new OpGetAiPackageDone); - - interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDoneExplicit, - new OpGetAiPackageDone); - interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackage, new OpGetCurrentAIPackage); - interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackageExplicit, new OpGetCurrentAIPackage); - interpreter.installSegment5 (Compiler::Ai::opcodeGetDetected, new OpGetDetected); - interpreter.installSegment5 (Compiler::Ai::opcodeGetDetectedExplicit, new OpGetDetected); - interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSight, new OpGetLineOfSight); - interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSightExplicit, new OpGetLineOfSight); - interpreter.installSegment5 (Compiler::Ai::opcodeGetTarget, new OpGetTarget); - interpreter.installSegment5 (Compiler::Ai::opcodeGetTargetExplicit, new OpGetTarget); - interpreter.installSegment5 (Compiler::Ai::opcodeStartCombat, new OpStartCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStartCombatExplicit, new OpStartCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStopCombat, new OpStopCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStopCombatExplicit, new OpStopCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeToggleAI, new OpToggleAI); - - interpreter.installSegment5 (Compiler::Ai::opcodeSetHello, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetHelloExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFight, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFightExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFlee, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFleeExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarm, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarmExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeModHello, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeModHelloExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFight, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFightExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFlee, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFleeExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeModAlarm, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeModAlarmExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeGetHello, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetHelloExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFight, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFightExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFlee, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace); - interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace); + interpreter.installSegment3>(Compiler::Ai::opcodeAIActivate); + interpreter.installSegment3>(Compiler::Ai::opcodeAIActivateExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiTravel); + interpreter.installSegment3>(Compiler::Ai::opcodeAiTravelExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscort); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortCell); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortCellExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiWander); + interpreter.installSegment3>(Compiler::Ai::opcodeAiWanderExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollow); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowCell); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowCellExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetAiPackageDone); + + interpreter.installSegment5>(Compiler::Ai::opcodeGetAiPackageDoneExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetCurrentAiPackage); + interpreter.installSegment5>(Compiler::Ai::opcodeGetCurrentAiPackageExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetDetected); + interpreter.installSegment5>(Compiler::Ai::opcodeGetDetectedExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetLineOfSight); + interpreter.installSegment5>(Compiler::Ai::opcodeGetLineOfSightExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetTarget); + interpreter.installSegment5>(Compiler::Ai::opcodeGetTargetExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeStartCombat); + interpreter.installSegment5>(Compiler::Ai::opcodeStartCombatExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeStopCombat); + interpreter.installSegment5>(Compiler::Ai::opcodeStopCombatExplicit); + interpreter.installSegment5(Compiler::Ai::opcodeToggleAI); + + interpreter.installSegment5>(Compiler::Ai::opcodeSetHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeSetHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeSetFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeSetFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeSetFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeSetFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeSetAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>(Compiler::Ai::opcodeSetAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>(Compiler::Ai::opcodeModHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeModHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeModFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeModFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeModFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeModFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeModAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>(Compiler::Ai::opcodeModAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>(Compiler::Ai::opcodeGetHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeGetHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>(Compiler::Ai::opcodeGetFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeGetFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>(Compiler::Ai::opcodeGetFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeGetFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>(Compiler::Ai::opcodeGetAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>(Compiler::Ai::opcodeGetAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>(Compiler::Ai::opcodeFace); + interpreter.installSegment5>(Compiler::Ai::opcodeFaceExplicit); } } } diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 8bb6cc6ada..12fe5256be 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -44,7 +44,7 @@ namespace MWScript if (!ptr.getRefData().isEnabled()) return; - std::string group = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view group = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer mode = 0; @@ -58,7 +58,7 @@ namespace MWScript throw std::runtime_error ("animation mode out of range"); } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits::max(), true); + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, std::string{group}, mode, std::numeric_limits::max(), true); } }; @@ -74,7 +74,7 @@ namespace MWScript if (!ptr.getRefData().isEnabled()) return; - std::string group = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view group = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer loops = runtime[0].mInteger; @@ -94,19 +94,19 @@ 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, std::string{group}, mode, loops + 1, true); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnim, new OpSkipAnim); - interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnimExplicit, new OpSkipAnim); - interpreter.installSegment3 (Compiler::Animation::opcodePlayAnim, new OpPlayAnim); - interpreter.installSegment3 (Compiler::Animation::opcodePlayAnimExplicit, new OpPlayAnim); - interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnim, new OpLoopAnim); - interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnimExplicit, new OpLoopAnim); + interpreter.installSegment5>(Compiler::Animation::opcodeSkipAnim); + interpreter.installSegment5>(Compiler::Animation::opcodeSkipAnimExplicit); + interpreter.installSegment3>(Compiler::Animation::opcodePlayAnim); + interpreter.installSegment3>(Compiler::Animation::opcodePlayAnimExplicit); + interpreter.installSegment3>(Compiler::Animation::opcodeLoopAnim); + interpreter.installSegment3>(Compiler::Animation::opcodeLoopAnimExplicit); } } } diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp index 3564281561..0419d37962 100644 --- a/apps/openmw/mwscript/cellextensions.cpp +++ b/apps/openmw/mwscript/cellextensions.cpp @@ -13,7 +13,6 @@ #include "../mwworld/actionteleport.hpp" #include "../mwworld/cellstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwworld/player.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -88,7 +87,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string cell = runtime.getStringLiteral (runtime[0].mInteger); + std::string cell{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); ESM::Position pos; @@ -161,7 +160,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) @@ -209,6 +208,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; + runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) { @@ -232,6 +232,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; + runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) { @@ -251,16 +252,16 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged); - interpreter.installSegment5 (Compiler::Cell::opcodeTestCells, new OpTestCells); - interpreter.installSegment5 (Compiler::Cell::opcodeTestInteriorCells, new OpTestInteriorCells); - interpreter.installSegment5 (Compiler::Cell::opcodeCOC, new OpCOC); - interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE); - interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior); - interpreter.installSegment5 (Compiler::Cell::opcodeGetPCCell, new OpGetPCCell); - interpreter.installSegment5 (Compiler::Cell::opcodeGetWaterLevel, new OpGetWaterLevel); - interpreter.installSegment5 (Compiler::Cell::opcodeSetWaterLevel, new OpSetWaterLevel); - interpreter.installSegment5 (Compiler::Cell::opcodeModWaterLevel, new OpModWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeCellChanged); + interpreter.installSegment5(Compiler::Cell::opcodeTestCells); + interpreter.installSegment5(Compiler::Cell::opcodeTestInteriorCells); + interpreter.installSegment5(Compiler::Cell::opcodeCOC); + interpreter.installSegment5(Compiler::Cell::opcodeCOE); + interpreter.installSegment5(Compiler::Cell::opcodeGetInterior); + interpreter.installSegment5(Compiler::Cell::opcodeGetPCCell); + interpreter.installSegment5(Compiler::Cell::opcodeGetWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeSetWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeModWaterLevel); } } } diff --git a/apps/openmw/mwscript/compilercontext.cpp b/apps/openmw/mwscript/compilercontext.cpp index 4a7038e1cb..72537d606b 100644 --- a/apps/openmw/mwscript/compilercontext.cpp +++ b/apps/openmw/mwscript/compilercontext.cpp @@ -2,7 +2,7 @@ #include "../mwworld/esmstore.hpp" -#include +#include #include @@ -86,14 +86,4 @@ namespace MWScript store.get().search (name) || store.get().search (name); } - - bool CompilerContext::isJournalId (const std::string& name) const - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Dialogue *topic = store.get().search (name); - - return topic && topic->mType==ESM::Dialogue::Journal; - } } diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 00b10ea06d..d800781fd8 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -39,9 +39,6 @@ namespace MWScript bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? }; } diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 186940dd99..bb63ec84cc 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -13,7 +13,8 @@ #include -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -26,6 +27,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/levelledlist.hpp" @@ -50,7 +52,7 @@ namespace void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& owner, MWWorld::ContainerStore& store, bool topLevel = true) { - if(itemPtr.getTypeName() == typeid(ESM::ItemLevList).name()) + if(itemPtr.getType() == ESM::ItemLevList::sRecordId) { const ESM::ItemLevList* levItemList = itemPtr.get()->mBase; @@ -61,7 +63,8 @@ namespace } else { - std::string itemId = MWMechanics::getLevelledItem(itemPtr.get()->mBase, false); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + std::string itemId = MWMechanics::getLevelledItem(itemPtr.get()->mBase, false, prng); if (itemId.empty()) return; MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), itemId, 1); @@ -86,14 +89,14 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); if (count<0) - throw std::runtime_error ("second argument for AddItem must be non-negative"); + count = static_cast(count); // no-op if (count == 0) @@ -108,22 +111,22 @@ namespace MWScript // Check if "item" can be placed in a container MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), item, 1); MWWorld::Ptr itemPtr = manualRef.getPtr(); - bool isLevelledList = itemPtr.getClass().getTypeName() == typeid(ESM::ItemLevList).name(); + bool isLevelledList = itemPtr.getClass().getType() == ESM::ItemLevList::sRecordId; if(!isLevelledList) MWWorld::ContainerStore::getType(itemPtr); // Explicit calls to non-unique actors affect the base record if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), std::string{item}, count); return; } // Calls to unresolved containers affect the base record - if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && (!ptr.getRefData().getCustomData() || + if(ptr.getClass().getType() == ESM::Container::sRecordId && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), std::string{item}, count); const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); for(const auto& container : ptrs) @@ -182,7 +185,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if(::Misc::StringUtils::ciEqual(item, "gold_005") @@ -206,7 +209,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; @@ -228,14 +231,14 @@ namespace MWScript // Explicit calls to non-unique actors affect the base record if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), std::string{item}, -count); return; } // Calls to unresolved containers affect the base record instead - else if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && + else if(ptr.getClass().getType() == ESM::Container::sRecordId && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), std::string{item}, -count); const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); for(const auto& container : ptrs) @@ -296,7 +299,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); @@ -308,7 +311,8 @@ namespace MWScript } if (it == invStore.end()) { - it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr); + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), item, 1); + it = ptr.getClass().getContainerStore (ptr).add (ref.getPtr(), 1, ptr, false); Log(Debug::Warning) << "Implicitly adding one " << item << " to the inventory store of " << ptr.getCellRef().getRefId() << " to fulfill the requirements of Equip instruction"; @@ -318,7 +322,7 @@ namespace MWScript MWBase::Environment::get().getWindowManager()->useItem(*it, true); else { - std::shared_ptr action = it->getClass().use(*it, true); + std::unique_ptr action = it->getClass().use(*it, true); action->execute(ptr, true); } } @@ -379,7 +383,7 @@ namespace MWScript const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); - if (it == invStore.end() || it->getTypeName () != typeid(ESM::Armor).name()) + if (it == invStore.end() || it->getType () != ESM::Armor::sRecordId) { runtime.push(-1); return; @@ -406,7 +410,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); @@ -432,7 +436,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - const std::string &name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int count = 0; @@ -463,13 +467,13 @@ namespace MWScript runtime.push(-1); return; } - else if (it->getTypeName() != typeid(ESM::Weapon).name()) + else if (it->getType() != ESM::Weapon::sRecordId) { - if (it->getTypeName() == typeid(ESM::Lockpick).name()) + if (it->getType() == ESM::Lockpick::sRecordId) { runtime.push(-2); } - else if (it->getTypeName() == typeid(ESM::Probe).name()) + else if (it->getType() == ESM::Probe::sRecordId) { runtime.push(-3); } @@ -487,22 +491,22 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Container::opcodeAddItem, new OpAddItem); - interpreter.installSegment5 (Compiler::Container::opcodeAddItemExplicit, new OpAddItem); - interpreter.installSegment5 (Compiler::Container::opcodeGetItemCount, new OpGetItemCount); - interpreter.installSegment5 (Compiler::Container::opcodeGetItemCountExplicit, new OpGetItemCount); - interpreter.installSegment5 (Compiler::Container::opcodeRemoveItem, new OpRemoveItem); - interpreter.installSegment5 (Compiler::Container::opcodeRemoveItemExplicit, new OpRemoveItem); - interpreter.installSegment5 (Compiler::Container::opcodeEquip, new OpEquip); - interpreter.installSegment5 (Compiler::Container::opcodeEquipExplicit, new OpEquip); - interpreter.installSegment5 (Compiler::Container::opcodeGetArmorType, new OpGetArmorType); - interpreter.installSegment5 (Compiler::Container::opcodeGetArmorTypeExplicit, new OpGetArmorType); - interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquipped, new OpHasItemEquipped); - interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquippedExplicit, new OpHasItemEquipped); - interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGem, new OpHasSoulGem); - interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGemExplicit, new OpHasSoulGem); - interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponType, new OpGetWeaponType); - interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponTypeExplicit, new OpGetWeaponType); + interpreter.installSegment5>(Compiler::Container::opcodeAddItem); + interpreter.installSegment5>(Compiler::Container::opcodeAddItemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetItemCount); + interpreter.installSegment5>(Compiler::Container::opcodeGetItemCountExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeRemoveItem); + interpreter.installSegment5>(Compiler::Container::opcodeRemoveItemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeEquip); + interpreter.installSegment5>(Compiler::Container::opcodeEquipExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetArmorType); + interpreter.installSegment5>(Compiler::Container::opcodeGetArmorTypeExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeHasItemEquipped); + interpreter.installSegment5>(Compiler::Container::opcodeHasItemEquippedExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeHasSoulGem); + interpreter.installSegment5>(Compiler::Container::opcodeHasSoulGemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetWeaponType); + interpreter.installSegment5>(Compiler::Container::opcodeGetWeaponTypeExplicit); } } } diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index 5362759e10..d216474c6d 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -1,6 +1,5 @@ #include "controlextensions.hpp" -#include #include #include @@ -15,7 +14,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" -#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "interpretercontext.hpp" #include "ref.hpp" @@ -191,67 +190,51 @@ namespace MWScript }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - for (int i=0; i(Compiler::Control::opcodeEnable + i, Compiler::Control::controls[i], true); + interpreter.installSegment5(Compiler::Control::opcodeDisable + i, Compiler::Control::controls[i], false); + interpreter.installSegment5(Compiler::Control::opcodeGetDisabled + i, Compiler::Control::controls[i]); } - interpreter.installSegment5 (Compiler::Control::opcodeToggleCollision, new OpToggleCollision); + interpreter.installSegment5(Compiler::Control::opcodeToggleCollision); //Force Run - interpreter.installSegment5 (Compiler::Control::opcodeClearForceRun, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceRunExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRun, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRunExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceRun, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceRunExplicit, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>(Compiler::Control::opcodeForceRun, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>(Compiler::Control::opcodeForceRunExplicit, MWMechanics::CreatureStats::Flag_ForceRun); //Force Jump - interpreter.installSegment5 (Compiler::Control::opcodeClearForceJump, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceJumpExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceJump, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceJumpExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceJump, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceJumpExplicit, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>(Compiler::Control::opcodeForceJump, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>(Compiler::Control::opcodeForceJumpExplicit, MWMechanics::CreatureStats::Flag_ForceJump); //Force MoveJump - interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJump, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJumpExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJump, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJumpExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceMoveJump, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceMoveJumpExplicit, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>(Compiler::Control::opcodeForceMoveJump, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>(Compiler::Control::opcodeForceMoveJumpExplicit, MWMechanics::CreatureStats::Flag_ForceMoveJump); //Force Sneak - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneakExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneakExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - - interpreter.installSegment5 (Compiler::Control::opcodeGetPcRunning, new OpGetPcRunning); - interpreter.installSegment5 (Compiler::Control::opcodeGetPcSneaking, new OpGetPcSneaking); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceRun, new OpGetForceRun); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceRunExplicit, new OpGetForceRun); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceJump, new OpGetForceJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceJumpExplicit, new OpGetForceJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJump, new OpGetForceMoveJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJumpExplicit, new OpGetForceMoveJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneak, new OpGetForceSneak); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneakExplicit, new OpGetForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceSneak, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeClearForceSneakExplicit, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeForceSneak, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeForceSneakExplicit, MWMechanics::CreatureStats::Flag_ForceSneak); + + interpreter.installSegment5(Compiler::Control::opcodeGetPcRunning); + interpreter.installSegment5(Compiler::Control::opcodeGetPcSneaking); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceRun); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceRunExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceJump); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceJumpExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceMoveJump); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceMoveJumpExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceSneakExplicit); } } } diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index b99a043bf5..f06ee022e2 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -34,7 +34,7 @@ namespace MWScript if (ptr.isEmpty()) ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); + std::string quest{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); Interpreter::Type_Integer index = runtime[0].mInteger; @@ -59,7 +59,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); + std::string quest{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); Interpreter::Type_Integer index = runtime[0].mInteger; @@ -75,7 +75,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); + std::string quest{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); int index = MWBase::Environment::get().getJournal()->getJournalIndex (quest); @@ -91,7 +91,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string topic = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view topic = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getDialogueManager()->addTopic(topic); @@ -107,7 +107,7 @@ namespace MWScript MWBase::DialogueManager* dialogue = MWBase::Environment::get().getDialogueManager(); while(arg0>0) { - std::string question = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view question = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); arg0 = arg0 -1; Interpreter::Type_Integer choice = 1; @@ -220,10 +220,10 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction1 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction2 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int modReaction = runtime[0].mInteger; @@ -239,10 +239,10 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction1 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction2 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); runtime.push(MWBase::Environment::get().getDialogueManager() @@ -256,10 +256,10 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction1 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view faction2 = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int newValue = runtime[0].mInteger; @@ -284,28 +284,28 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Dialogue::opcodeJournal, new OpJournal); - interpreter.installSegment5 (Compiler::Dialogue::opcodeJournalExplicit, new OpJournal); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetJournalIndex, new OpSetJournalIndex); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetJournalIndex, new OpGetJournalIndex); - interpreter.installSegment5 (Compiler::Dialogue::opcodeAddTopic, new OpAddTopic); - interpreter.installSegment3 (Compiler::Dialogue::opcodeChoice,new OpChoice); - interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreeting, new OpForceGreeting); - interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreetingExplicit, new OpForceGreeting); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGoodbye, new OpGoodbye); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputation, new OpGetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputation, new OpSetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputation, new OpModReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputationExplicit, new OpSetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputationExplicit, new OpModReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputationExplicit, new OpGetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFaction, new OpSameFaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFactionExplicit, new OpSameFaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModFactionReaction, new OpModFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetFactionReaction, new OpSetFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetFactionReaction, new OpGetFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActor, new OpClearInfoActor); - interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActorExplicit, new OpClearInfoActor); + interpreter.installSegment5>(Compiler::Dialogue::opcodeJournal); + interpreter.installSegment5>(Compiler::Dialogue::opcodeJournalExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeSetJournalIndex); + interpreter.installSegment5(Compiler::Dialogue::opcodeGetJournalIndex); + interpreter.installSegment5(Compiler::Dialogue::opcodeAddTopic); + interpreter.installSegment3(Compiler::Dialogue::opcodeChoice); + interpreter.installSegment5>(Compiler::Dialogue::opcodeForceGreeting); + interpreter.installSegment5>(Compiler::Dialogue::opcodeForceGreetingExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeGoodbye); + interpreter.installSegment5>(Compiler::Dialogue::opcodeGetReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSetReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeModReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSetReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeModReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeGetReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSameFaction); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSameFactionExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeModFactionReaction); + interpreter.installSegment5(Compiler::Dialogue::opcodeSetFactionReaction); + interpreter.installSegment5(Compiler::Dialogue::opcodeGetFactionReaction); + interpreter.installSegment5>(Compiler::Dialogue::opcodeClearInfoActor); + interpreter.installSegment5>(Compiler::Dialogue::opcodeClearInfoActorExplicit); } } diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index ccc579b30d..aaba5e5986 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -479,5 +479,7 @@ op 0x200031c: GetDisabled, explicit op 0x200031d: StartScript, explicit op 0x200031e: GetDistance op 0x200031f: GetDistance, explicit +op 0x2000320: Help +op 0x2000321: ReloadLua -opcodes 0x2000320-0x3ffffff unused +opcodes 0x2000322-0x3ffffff unused diff --git a/apps/openmw/mwscript/globalscripts.cpp b/apps/openmw/mwscript/globalscripts.cpp index 1a7e3ebbc8..c1a25513e6 100644 --- a/apps/openmw/mwscript/globalscripts.cpp +++ b/apps/openmw/mwscript/globalscripts.cpp @@ -2,8 +2,8 @@ #include #include -#include -#include +#include +#include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -12,13 +12,11 @@ #include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" -#include "../mwmechanics/creaturestats.hpp" - #include "interpretercontext.hpp" namespace { - struct ScriptCreatingVisitor : public boost::static_visitor + struct ScriptCreatingVisitor { ESM::GlobalScript operator()(const MWWorld::Ptr &ptr) const { @@ -48,7 +46,7 @@ namespace } }; - struct PtrGettingVisitor : public boost::static_visitor + struct PtrGettingVisitor { const MWWorld::Ptr* operator()(const MWWorld::Ptr &ptr) const { @@ -61,7 +59,7 @@ namespace } }; - struct PtrResolvingVisitor : public boost::static_visitor + struct PtrResolvingVisitor { MWWorld::Ptr operator()(const MWWorld::Ptr &ptr) const { @@ -78,7 +76,7 @@ namespace } }; - class MatchPtrVisitor : public boost::static_visitor + class MatchPtrVisitor { const MWWorld::Ptr& mPtr; public: @@ -94,6 +92,21 @@ namespace return false; } }; + + struct IdGettingVisitor + { + std::string operator()(const MWWorld::Ptr& ptr) const + { + if(ptr.isEmpty()) + return {}; + return ptr.mRef->mRef.getRefId(); + } + + std::string operator()(const std::pair& pair) const + { + return pair.second; + } + }; } namespace MWScript @@ -102,35 +115,41 @@ namespace MWScript const MWWorld::Ptr* GlobalScriptDesc::getPtrIfPresent() const { - return boost::apply_visitor(PtrGettingVisitor(), mTarget); + return std::visit(PtrGettingVisitor(), mTarget); } MWWorld::Ptr GlobalScriptDesc::getPtr() { - MWWorld::Ptr ptr = boost::apply_visitor(PtrResolvingVisitor(), mTarget); + MWWorld::Ptr ptr = std::visit(PtrResolvingVisitor {}, mTarget); mTarget = ptr; return ptr; } + std::string GlobalScriptDesc::getId() const + { + return std::visit(IdGettingVisitor {}, mTarget); + } + GlobalScripts::GlobalScripts (const MWWorld::ESMStore& store) : mStore (store) {} - void GlobalScripts::addScript (const std::string& name, const MWWorld::Ptr& target) + void GlobalScripts::addScript(std::string_view name, const MWWorld::Ptr& target) { - const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); + std::string lowerName = ::Misc::StringUtils::lowerCase(name); + const auto iter = mScripts.find(lowerName); if (iter==mScripts.end()) { - if (const ESM::Script *script = mStore.get().search(name)) + if (const ESM::Script *script = mStore.get().search(lowerName)) { auto desc = std::make_shared(); MWWorld::Ptr ptr = target; desc->mTarget = ptr; desc->mRunning = true; desc->mLocals.configure (*script); - mScripts.insert (std::make_pair(name, desc)); + mScripts.insert (std::make_pair(lowerName, desc)); } else { @@ -145,7 +164,7 @@ namespace MWScript } } - void GlobalScripts::removeScript (const std::string& name) + void GlobalScripts::removeScript (std::string_view name) { const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); @@ -153,7 +172,7 @@ namespace MWScript iter->second->mRunning = false; } - bool GlobalScripts::isRunning (const std::string& name) const + bool GlobalScripts::isRunning (std::string_view name) const { const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); @@ -219,15 +238,15 @@ namespace MWScript void GlobalScripts::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (const auto& iter : mScripts) + for (const auto& [id, desc] : mScripts) { - ESM::GlobalScript script = boost::apply_visitor (ScriptCreatingVisitor(), iter.second->mTarget); + ESM::GlobalScript script = std::visit(ScriptCreatingVisitor {}, desc->mTarget); - script.mId = iter.first; + script.mId = id; - iter.second->mLocals.write (script.mLocals, iter.first); + desc->mLocals.write(script.mLocals, id); - script.mRunning = iter.second->mRunning ? 1 : 0; + script.mRunning = desc->mRunning ? 1 : 0; writer.startRecord (ESM::REC_GSCR); script.save (writer); @@ -235,13 +254,20 @@ namespace MWScript } } - bool GlobalScripts::readRecord (ESM::ESMReader& reader, uint32_t type) + bool GlobalScripts::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { if (type==ESM::REC_GSCR) { ESM::GlobalScript script; script.load (reader); + if (script.mTargetRef.hasContentFile()) + { + auto iter = contentFileMap.find(script.mTargetRef.mContentFile); + if (iter != contentFileMap.end()) + script.mTargetRef.mContentFile = iter->second; + } + auto iter = mScripts.find (script.mId); if (iter==mScripts.end()) @@ -281,14 +307,14 @@ namespace MWScript return false; } - Locals& GlobalScripts::getLocals (const std::string& name) + Locals& GlobalScripts::getLocals(std::string_view name) { std::string name2 = ::Misc::StringUtils::lowerCase (name); auto iter = mScripts.find (name2); if (iter==mScripts.end()) { - const ESM::Script *script = mStore.get().find (name); + const ESM::Script *script = mStore.get().find(name2); auto desc = std::make_shared(); desc->mLocals.configure (*script); @@ -299,7 +325,7 @@ namespace MWScript return iter->second->mLocals; } - const Locals* GlobalScripts::getLocalsIfPresent (const std::string& name) const + const Locals* GlobalScripts::getLocalsIfPresent(std::string_view name) const { std::string name2 = ::Misc::StringUtils::lowerCase (name); auto iter = mScripts.find (name2); @@ -313,7 +339,7 @@ namespace MWScript MatchPtrVisitor visitor(base); for (const auto& script : mScripts) { - if (boost::apply_visitor (visitor, script.second->mTarget)) + if (std::visit (visitor, script.second->mTarget)) script.second->mTarget = updated; } } diff --git a/apps/openmw/mwscript/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index c5c5a9a452..556f815238 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -1,14 +1,13 @@ #ifndef GAME_SCRIPT_GLOBALSCRIPTS_H #define GAME_SCRIPT_GLOBALSCRIPTS_H -#include - #include #include #include #include +#include -#include +#include #include "locals.hpp" @@ -37,13 +36,15 @@ namespace MWScript { bool mRunning; Locals mLocals; - boost::variant > mTarget; // Used to start targeted script + std::variant> mTarget; // Used to start targeted script GlobalScriptDesc(); const MWWorld::Ptr* getPtrIfPresent() const; // Returns a Ptr if one has been resolved MWWorld::Ptr getPtr(); // Resolves mTarget to a Ptr and caches the (potentially empty) result + + std::string getId() const; // Returns the target's ID -- if any }; class GlobalScripts @@ -55,11 +56,11 @@ namespace MWScript GlobalScripts (const MWWorld::ESMStore& store); - void addScript (const std::string& name, const MWWorld::Ptr& target = MWWorld::Ptr()); + void addScript(std::string_view name, const MWWorld::Ptr& target = MWWorld::Ptr()); - void removeScript (const std::string& name); + void removeScript (std::string_view name); - bool isRunning (const std::string& name) const; + bool isRunning (std::string_view name) const; void run(); ///< run all active global scripts @@ -73,16 +74,16 @@ namespace MWScript void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, uint32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); ///< Records for variables that do not exist are dropped silently. /// /// \return Known type? - Locals& getLocals (const std::string& name); + Locals& getLocals(std::string_view name); ///< If the script \a name has not been added as a global script yet, it is added /// automatically, but is not set to running state. - const Locals* getLocalsIfPresent (const std::string& name) const; + const Locals* getLocalsIfPresent(std::string_view name) const; void updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); ///< Update the Ptrs stored in mTarget. Should be called after the reference has been moved to a new cell. diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index cb1e5cd91d..f240747f6e 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -114,8 +114,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string cell = (runtime.getStringLiteral (runtime[0].mInteger)); - ::Misc::StringUtils::lowerCaseInPlace(cell); + std::string cell = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); // "Will match complete or partial cells, so ShowMap, "Vivec" will show cells Vivec and Vivec, Fred's House as well." @@ -124,17 +123,14 @@ namespace MWScript const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); - MWWorld::Store::iterator it = cells.extBegin(); - for (; it != cells.extEnd(); ++it) + MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) { std::string name = it->mName; ::Misc::StringUtils::lowerCaseInPlace(name); - if (name.find(cell) != std::string::npos) - MWBase::Environment::get().getWindowManager()->addVisitedLocation ( - it->mName, - it->getGridX(), - it->getGridY() - ); + if (name.length() >= cell.length() && name.substr(0, cell.length()) == cell) + winMgr->addVisitedLocation(it->mName, it->getGridX(), it->getGridY()); } } }; @@ -221,45 +217,33 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Gui::opcodeEnableBirthMenu, - new OpShowDialogue (MWGui::GM_Birth)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableClassMenu, - new OpShowDialogue (MWGui::GM_Class)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableNameMenu, - new OpShowDialogue (MWGui::GM_Name)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableRaceMenu, - new OpShowDialogue (MWGui::GM_Race)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsReviewMenu, - new OpShowDialogue (MWGui::GM_Review)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableLevelupMenu, - new OpShowDialogue (MWGui::GM_Levelup)); - - interpreter.installSegment5 (Compiler::Gui::opcodeEnableInventoryMenu, - new OpEnableWindow (MWGui::GW_Inventory)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableMagicMenu, - new OpEnableWindow (MWGui::GW_Magic)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableMapMenu, - new OpEnableWindow (MWGui::GW_Map)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsMenu, - new OpEnableWindow (MWGui::GW_Stats)); - - interpreter.installSegment5 (Compiler::Gui::opcodeEnableRest, - new OpEnableRest ()); - - interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenu, - new OpShowRestMenu); - interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenuExplicit, new OpShowRestMenu); - - interpreter.installSegment5 (Compiler::Gui::opcodeGetButtonPressed, new OpGetButtonPressed); - - interpreter.installSegment5 (Compiler::Gui::opcodeToggleFogOfWar, new OpToggleFogOfWar); - - interpreter.installSegment5 (Compiler::Gui::opcodeToggleFullHelp, new OpToggleFullHelp); - - interpreter.installSegment5 (Compiler::Gui::opcodeShowMap, new OpShowMap); - interpreter.installSegment5 (Compiler::Gui::opcodeFillMap, new OpFillMap); - interpreter.installSegment3 (Compiler::Gui::opcodeMenuTest, new OpMenuTest); - interpreter.installSegment5 (Compiler::Gui::opcodeToggleMenus, new OpToggleMenus); + interpreter.installSegment5(Compiler::Gui::opcodeEnableBirthMenu, MWGui::GM_Birth); + interpreter.installSegment5(Compiler::Gui::opcodeEnableClassMenu, MWGui::GM_Class); + interpreter.installSegment5(Compiler::Gui::opcodeEnableNameMenu, MWGui::GM_Name); + interpreter.installSegment5(Compiler::Gui::opcodeEnableRaceMenu, MWGui::GM_Race); + interpreter.installSegment5(Compiler::Gui::opcodeEnableStatsReviewMenu, MWGui::GM_Review); + interpreter.installSegment5(Compiler::Gui::opcodeEnableLevelupMenu, MWGui::GM_Levelup); + + interpreter.installSegment5(Compiler::Gui::opcodeEnableInventoryMenu, MWGui::GW_Inventory); + interpreter.installSegment5(Compiler::Gui::opcodeEnableMagicMenu, MWGui::GW_Magic); + interpreter.installSegment5(Compiler::Gui::opcodeEnableMapMenu, MWGui::GW_Map); + interpreter.installSegment5(Compiler::Gui::opcodeEnableStatsMenu, MWGui::GW_Stats); + + interpreter.installSegment5(Compiler::Gui::opcodeEnableRest); + + interpreter.installSegment5>(Compiler::Gui::opcodeShowRestMenu); + interpreter.installSegment5>(Compiler::Gui::opcodeShowRestMenuExplicit); + + interpreter.installSegment5(Compiler::Gui::opcodeGetButtonPressed); + + interpreter.installSegment5(Compiler::Gui::opcodeToggleFogOfWar); + + interpreter.installSegment5(Compiler::Gui::opcodeToggleFullHelp); + + interpreter.installSegment5(Compiler::Gui::opcodeShowMap); + interpreter.installSegment5(Compiler::Gui::opcodeFillMap); + interpreter.installSegment3(Compiler::Gui::opcodeMenuTest); + interpreter.installSegment5(Compiler::Gui::opcodeToggleMenus); } } } diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index f3895c8b6a..733bc9260f 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -5,8 +5,6 @@ #include -#include - #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" @@ -14,6 +12,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" @@ -90,7 +89,7 @@ namespace MWScript MissingImplicitRefError::MissingImplicitRefError() : std::runtime_error("no implicit reference") {} int InterpreterContext::findLocalVariableIndex (const std::string& scriptId, - const std::string& name, char type) const + std::string_view name, char type) const { int index = MWBase::Environment::get().getScriptManager()->getLocals (scriptId). searchIndex (type, name); @@ -131,6 +130,15 @@ namespace MWScript mGlobalScriptDesc = globalScriptDesc; } + std::string InterpreterContext::getTarget() const + { + if(!mReference.isEmpty()) + return mReference.mRef->mRef.getRefId(); + else if(mGlobalScriptDesc) + return mGlobalScriptDesc->getId(); + return {}; + } + int InterpreterContext::getLocalShort (int index) const { if (!mLocals) @@ -192,33 +200,33 @@ namespace MWScript { } - int InterpreterContext::getGlobalShort (const std::string& name) const + int InterpreterContext::getGlobalShort(std::string_view name) const { return MWBase::Environment::get().getWorld()->getGlobalInt (name); } - int InterpreterContext::getGlobalLong (const std::string& name) const + int InterpreterContext::getGlobalLong(std::string_view name) const { // a global long is internally a float. return MWBase::Environment::get().getWorld()->getGlobalInt (name); } - float InterpreterContext::getGlobalFloat (const std::string& name) const + float InterpreterContext::getGlobalFloat(std::string_view name) const { return MWBase::Environment::get().getWorld()->getGlobalFloat (name); } - void InterpreterContext::setGlobalShort (const std::string& name, int value) + void InterpreterContext::setGlobalShort(std::string_view name, int value) { MWBase::Environment::get().getWorld()->setGlobalInt (name, value); } - void InterpreterContext::setGlobalLong (const std::string& name, int value) + void InterpreterContext::setGlobalLong(std::string_view name, int value) { MWBase::Environment::get().getWorld()->setGlobalInt (name, value); } - void InterpreterContext::setGlobalFloat (const std::string& name, float value) + void InterpreterContext::setGlobalFloat(std::string_view name, float value) { MWBase::Environment::get().getWorld()->setGlobalFloat (name, value); } @@ -237,13 +245,13 @@ namespace MWScript return ids; } - char InterpreterContext::getGlobalType (const std::string& name) const + char InterpreterContext::getGlobalType(std::string_view name) const { MWBase::World *world = MWBase::Environment::get().getWorld(); return world->getGlobalVariableType(name); } - std::string InterpreterContext::getActionBinding(const std::string& targetAction) const + std::string InterpreterContext::getActionBinding(std::string_view targetAction) const { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); std::vector actions = input->getActionKeySorting (); @@ -408,9 +416,10 @@ namespace MWScript return MWBase::Environment::get().getWorld()->getCellName(); } - void InterpreterContext::executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor) + void InterpreterContext::executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) { - std::shared_ptr action = (ptr.getClass().activate(ptr, actor)); + MWBase::Environment::get().getLuaManager()->objectActivated(ptr, actor); + std::unique_ptr action = (ptr.getClass().activate(ptr, actor)); action->execute (actor); if (action->getTarget() != MWWorld::Ptr() && action->getTarget() != ptr) { @@ -418,7 +427,7 @@ namespace MWScript } } - int InterpreterContext::getMemberShort (const std::string& id, const std::string& name, + int InterpreterContext::getMemberShort(std::string_view id, std::string_view name, bool global) const { std::string scriptId (id); @@ -428,7 +437,7 @@ namespace MWScript return locals.mShorts[findLocalVariableIndex (scriptId, name, 's')]; } - int InterpreterContext::getMemberLong (const std::string& id, const std::string& name, + int InterpreterContext::getMemberLong(std::string_view id, std::string_view name, bool global) const { std::string scriptId (id); @@ -438,7 +447,7 @@ namespace MWScript return locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')]; } - float InterpreterContext::getMemberFloat (const std::string& id, const std::string& name, + float InterpreterContext::getMemberFloat(std::string_view id, std::string_view name, bool global) const { std::string scriptId (id); @@ -448,7 +457,7 @@ namespace MWScript return locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')]; } - void InterpreterContext::setMemberShort (const std::string& id, const std::string& name, + void InterpreterContext::setMemberShort(std::string_view id, std::string_view name, int value, bool global) { std::string scriptId (id); @@ -458,7 +467,7 @@ namespace MWScript locals.mShorts[findLocalVariableIndex (scriptId, name, 's')] = value; } - void InterpreterContext::setMemberLong (const std::string& id, const std::string& name, int value, bool global) + void InterpreterContext::setMemberLong(std::string_view id, std::string_view name, int value, bool global) { std::string scriptId (id); @@ -467,7 +476,7 @@ namespace MWScript locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')] = value; } - void InterpreterContext::setMemberFloat (const std::string& id, const std::string& name, float value, bool global) + void InterpreterContext::setMemberFloat(std::string_view id, std::string_view name, float value, bool global) { std::string scriptId (id); @@ -476,7 +485,7 @@ namespace MWScript locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')] = value; } - MWWorld::Ptr InterpreterContext::getReference(bool required) + MWWorld::Ptr InterpreterContext::getReference(bool required) const { return getReferenceImp ("", true, required); } diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index c1481d6d0a..b675e850d2 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -10,16 +10,6 @@ #include "../mwworld/ptr.hpp" -namespace MWSound -{ - class SoundManager; -} - -namespace MWInput -{ - struct MWInputManager; -} - namespace MWScript { class Locals; @@ -48,7 +38,7 @@ namespace MWScript ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before /// Throws an exception if local variable can't be found. - int findLocalVariableIndex (const std::string& scriptId, const std::string& name, + int findLocalVariableIndex (const std::string& scriptId, std::string_view name, char type) const; public: @@ -57,6 +47,8 @@ namespace MWScript InterpreterContext (MWScript::Locals *locals, const MWWorld::Ptr& reference); ///< The ownership of \a locals is not transferred. 0-pointer allowed. + std::string getTarget() const override; + int getLocalShort (int index) const override; int getLocalLong (int index) const override; @@ -77,23 +69,23 @@ namespace MWScript void report (const std::string& message) override; ///< By default, do nothing. - int getGlobalShort (const std::string& name) const override; + int getGlobalShort(std::string_view name) const override; - int getGlobalLong (const std::string& name) const override; + int getGlobalLong(std::string_view name) const override; - float getGlobalFloat (const std::string& name) const override; + float getGlobalFloat(std::string_view name) const override; - void setGlobalShort (const std::string& name, int value) override; + void setGlobalShort(std::string_view name, int value) override; - void setGlobalLong (const std::string& name, int value) override; + void setGlobalLong(std::string_view name, int value) override; - void setGlobalFloat (const std::string& name, float value) override; + void setGlobalFloat(std::string_view name, float value) override; std::vector getGlobals () const override; - char getGlobalType (const std::string& name) const override; + char getGlobalType(std::string_view name) const override; - std::string getActionBinding(const std::string& action) const override; + std::string getActionBinding(std::string_view action) const override; std::string getActorName() const override; @@ -119,22 +111,22 @@ namespace MWScript std::string getCurrentCellName() const override; - void executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor); + void executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor); ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. - int getMemberShort (const std::string& id, const std::string& name, bool global) const override; + int getMemberShort(std::string_view id, std::string_view name, bool global) const override; - int getMemberLong (const std::string& id, const std::string& name, bool global) const override; + int getMemberLong(std::string_view id, std::string_view name, bool global) const override; - float getMemberFloat (const std::string& id, const std::string& name, bool global) const override; + float getMemberFloat(std::string_view id, std::string_view name, bool global) const override; - void setMemberShort (const std::string& id, const std::string& name, int value, bool global) override; + void setMemberShort(std::string_view id, std::string_view name, int value, bool global) override; - void setMemberLong (const std::string& id, const std::string& name, int value, bool global) override; + void setMemberLong(std::string_view id, std::string_view name, int value, bool global) override; - void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) override; + void setMemberFloat(std::string_view id, std::string_view name, float value, bool global) override; - MWWorld::Ptr getReference(bool required=true); + MWWorld::Ptr getReference(bool required=true) const; ///< Reference, that the script is running from (can be empty) void updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 352dc67b3a..9b9fbc66ad 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -1,12 +1,11 @@ #include "locals.hpp" #include "globalscripts.hpp" -#include -#include -#include +#include +#include +#include #include #include -#include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" @@ -63,24 +62,17 @@ namespace MWScript return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } - bool Locals::hasVar(const std::string &script, const std::string &var) + bool Locals::hasVar(const std::string &script, std::string_view var) { - try - { - ensure (script); + ensure (script); - const Compiler::Locals& locals = - MWBase::Environment::get().getScriptManager()->getLocals(script); - int index = locals.getIndex(var); - return (index != -1); - } - catch (const Compiler::SourceException&) - { - return false; - } + const Compiler::Locals& locals = + MWBase::Environment::get().getScriptManager()->getLocals(script); + int index = locals.getIndex(var); + return (index != -1); } - int Locals::getIntVar(const std::string &script, const std::string &var) + int Locals::getIntVar(const std::string &script, std::string_view var) { ensure (script); @@ -106,7 +98,7 @@ namespace MWScript return 0; } - float Locals::getFloatVar(const std::string &script, const std::string &var) + float Locals::getFloatVar(const std::string &script, std::string_view var) { ensure (script); @@ -132,7 +124,7 @@ namespace MWScript return 0; } - bool Locals::setVarByInt(const std::string& script, const std::string& var, int val) + bool Locals::setVarByInt(const std::string& script, std::string_view var, int val) { ensure (script); @@ -162,42 +154,36 @@ namespace MWScript if (!mInitialised) return false; - try + const Compiler::Locals& declarations = + MWBase::Environment::get().getScriptManager()->getLocals(script); + + for (int i=0; i<3; ++i) { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); + char type = 0; - for (int i=0; i<3; ++i) + switch (i) { - char type = 0; + case 0: type = 's'; break; + case 1: type = 'l'; break; + case 2: type = 'f'; break; + } - switch (i) - { - case 0: type = 's'; break; - case 1: type = 'l'; break; - case 2: type = 'f'; break; - } + const std::vector& names = declarations.get (type); - const std::vector& names = declarations.get (type); + for (int i2=0; i2 (names.size()); ++i2) + { + ESM::Variant value; - for (int i2=0; i2 (names.size()); ++i2) + switch (i) { - ESM::Variant value; - - switch (i) - { - case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; - case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; - case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; - } - - locals.mVariables.emplace_back (names[i2], value); + case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; + case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; + case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; } + + locals.mVariables.emplace_back (names[i2], value); } } - catch (const Compiler::SourceException&) - { - } return true; } @@ -206,72 +192,66 @@ namespace MWScript { ensure (script); - try - { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& declarations = + MWBase::Environment::get().getScriptManager()->getLocals(script); - int index = 0, numshorts = 0, numlongs = 0; - for (unsigned int v=0; v >::const_iterator iter - = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter,++index) + for (std::vector >::const_iterator iter + = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter,++index) + { + if (iter->first.empty()) { - if (iter->first.empty()) + // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) + try { - // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) - try - { - if (index >= numshorts+numlongs) - mFloats.at(index - (numshorts+numlongs)) = iter->second.getFloat(); - else if (index >= numshorts) - mLongs.at(index - numshorts) = iter->second.getInteger(); - else - mShorts.at(index) = iter->second.getInteger(); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to read local variable state for script '" - << script << "' (legacy format): " << e.what() - << "\nNum shorts: " << numshorts << " / " << mShorts.size() - << " Num longs: " << numlongs << " / " << mLongs.size(); - } + if (index >= numshorts+numlongs) + mFloats.at(index - (numshorts+numlongs)) = iter->second.getFloat(); + else if (index >= numshorts) + mLongs.at(index - numshorts) = iter->second.getInteger(); + else + mShorts.at(index) = iter->second.getInteger(); } - else + catch (std::exception& e) { - char type = declarations.getType (iter->first); - int index2 = declarations.getIndex (iter->first); + Log(Debug::Error) << "Failed to read local variable state for script '" + << script << "' (legacy format): " << e.what() + << "\nNum shorts: " << numshorts << " / " << mShorts.size() + << " Num longs: " << numlongs << " / " << mLongs.size(); + } + } + else + { + char type = declarations.getType (iter->first); + int index2 = declarations.getIndex (iter->first); - // silently ignore locals that don't exist anymore - if (type == ' ' || index2 == -1) - continue; + // silently ignore locals that don't exist anymore + if (type == ' ' || index2 == -1) + continue; - try - { - switch (type) - { - case 's': mShorts.at (index2) = iter->second.getInteger(); break; - case 'l': mLongs.at (index2) = iter->second.getInteger(); break; - case 'f': mFloats.at (index2) = iter->second.getFloat(); break; - } - } - catch (...) + try + { + switch (type) { - // ignore type changes - /// \todo write to log + case 's': mShorts.at (index2) = iter->second.getInteger(); break; + case 'l': mLongs.at (index2) = iter->second.getInteger(); break; + case 'f': mFloats.at (index2) = iter->second.getFloat(); break; } } + catch (...) + { + // ignore type changes + /// \todo write to log + } } } - catch (const Compiler::SourceException&) - { - } } } diff --git a/apps/openmw/mwscript/locals.hpp b/apps/openmw/mwscript/locals.hpp index d63411a942..6f3d4d5f07 100644 --- a/apps/openmw/mwscript/locals.hpp +++ b/apps/openmw/mwscript/locals.hpp @@ -1,6 +1,8 @@ #ifndef GAME_SCRIPT_LOCALS_H #define GAME_SCRIPT_LOCALS_H +#include +#include #include #include @@ -37,25 +39,25 @@ namespace MWScript /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary - bool setVarByInt(const std::string& script, const std::string& var, int val); + bool setVarByInt(const std::string& script, std::string_view var, int val); /// \note Locals will be automatically configured first, if necessary // // \note If it can not be determined if the variable exists, the error will be // ignored and false will be returned. - bool hasVar(const std::string& script, const std::string& var); + bool hasVar(const std::string& script, std::string_view var); /// if var does not exist, returns 0 /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary - int getIntVar (const std::string& script, const std::string& var); + int getIntVar (const std::string& script, std::string_view var); /// if var does not exist, returns 0 /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary - float getFloatVar (const std::string& script, const std::string& var); + float getFloatVar (const std::string& script, std::string_view var); /// \note If locals have not been configured yet, no data is written. /// diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index a288d66730..e5b44bccae 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -2,7 +2,10 @@ #include #include +#include +#include +#include #include #include @@ -13,15 +16,22 @@ #include #include +#include -#include -#include +#include + +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" @@ -43,7 +53,7 @@ namespace { - void addToLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + void addToLevList(ESM::LevelledListBase* list, std::string_view itemId, int level) { for (auto& levelItem : list->mList) { @@ -52,12 +62,12 @@ namespace } ESM::LevelledListBase::LevelItem item; - item.mId = itemId; + item.mId = std::string{itemId}; item.mLevel = level; list->mList.push_back(item); } - void removeFromLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + void removeFromLevList(ESM::LevelledListBase* list, std::string_view itemId, int level) { // level of -1 removes all items with that itemId for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) @@ -103,7 +113,8 @@ namespace MWScript throw std::runtime_error ( "random: argument out of range (Don't be so negative!)"); - runtime.push (static_cast(::Misc::Rng::rollDice(limit))); // [o, limit) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + runtime.push (static_cast(::Misc::Rng::rollDice(limit, prng))); // [o, limit) } }; @@ -115,7 +126,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr target = R()(runtime, false); - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name, target); } @@ -127,7 +138,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); runtime.push(MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning (name)); } @@ -139,7 +150,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getScriptManager()->getGlobalScripts().removeScript (name); } @@ -197,7 +208,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string name{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); bool allowSkipping = runtime[0].mInteger != 0; @@ -273,7 +284,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); - if (ptr.getRefData().activateByScript()) + if (ptr.getRefData().activateByScript() || ptr.getContainerStore()) context.executeActivation(ptr, MWMechanics::getPlayer()); } }; @@ -302,7 +313,7 @@ namespace MWScript // Instantly reset door to closed state // This is done when using Lock in scripts, but not when using Lock spells. - if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) + if (ptr.getType() == ESM::Door::sRecordId && !ptr.getCellRef().getTeleport()) { MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); } @@ -539,7 +550,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string effect = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view effect = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().isActor()) @@ -549,19 +560,13 @@ namespace MWScript } char *end; - long key = strtol(effect.c_str(), &end, 10); + long key = strtol(effect.data(), &end, 10); if(key < 0 || key > 32767 || *end != '\0') - key = ESM::MagicEffect::effectStringToId(effect); + key = ESM::MagicEffect::effectStringToId({effect}); const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); - effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) - { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - effects += store.getMagicEffects(); - } + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); for (const auto& activeEffect : effects) { @@ -584,10 +589,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string creature = runtime.getStringLiteral (runtime[0].mInteger); + std::string creature{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - std::string gem = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view gem = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().hasInventoryStore(ptr)) @@ -616,7 +621,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string soul = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view soul = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); // throw away additional arguments @@ -648,7 +653,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view item = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer amount = runtime[0].mInteger; @@ -735,7 +740,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); - std::string soul = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view soul = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().hasInventoryStore(ptr)) @@ -778,7 +783,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); runtime.push((ptr.getClass().hasInventoryStore(ptr) || ptr.getClass().isBipedal(ptr)) && - ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Weapon); + ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState::Weapon); } }; @@ -791,7 +796,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Spell); + runtime.push(ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState::Spell); } }; @@ -803,7 +808,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view id = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().isActor()) @@ -813,7 +818,7 @@ namespace MWScript } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); + runtime.push(stats.getActiveSpells().isSpellActive(id)); } }; @@ -856,6 +861,9 @@ namespace MWScript float param = runtime[0].mFloat; runtime.pop(); + if (param < 0) + throw std::runtime_error("square root of negative number (we aren't that imaginary)"); + runtime.push(std::sqrt (param)); } }; @@ -967,13 +975,14 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view objectID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject())); - - stats.setLastHitObject(std::string()); + bool hit = ::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject()); + runtime.push(hit); + if(hit) + stats.clearLastHitObject(); } }; @@ -986,13 +995,14 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view objectID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitAttemptObject())); - - stats.setLastHitAttemptObject(std::string()); + bool hit = ::Misc::StringUtils::ciEqual(objectID, stats.getLastHitAttemptObject()); + runtime.push(hit); + if(hit) + stats.clearLastHitAttemptObject(); } }; @@ -1028,7 +1038,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); - std::string var = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view var = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); std::stringstream output; @@ -1205,10 +1215,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); + std::string spellId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral (runtime[0].mInteger)); + std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); @@ -1220,14 +1230,17 @@ namespace MWScript if (ptr == MWMechanics::getPlayer()) { - MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(targetId, spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(targetId, spell->mId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1251,7 +1264,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); + std::string spellId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); @@ -1263,14 +1276,17 @@ namespace MWScript if (ptr == MWMechanics::getPlayer()) { - MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spell->mId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1349,18 +1365,19 @@ namespace MWScript std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); msg << std::put_time(std::gmtime(¤tTime), "%Y.%m.%d %T UTC") << std::endl; - msg << "Content file: "; + msg << "Content file: " << ptr.getCellRef().getRefNum().mContentFile; if (!ptr.getCellRef().hasContentFile()) - msg << "[None]" << std::endl; + msg << " [None]" << std::endl; else { std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; - msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + msg << " [" << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << "]" << std::endl; } + msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; if (!ptr.getRefData().getCount()) @@ -1377,14 +1394,22 @@ namespace MWScript msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; osg::Vec3f pos (ptr.getRefData().getPosition().asVec3()); msg << "Coordinates: " << pos.x() << " " << pos.y() << " " << pos.z() << std::endl; - msg << "Model: " << ptr.getClass().getModel(ptr) << 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; + if(!model.empty()) + { + const std::string archive = vfs->getArchive(model); + if(!archive.empty()) + msg << "(" << archive << ")" << std::endl; + } if (!ptr.getClass().getScript(ptr).empty()) msg << "Script: " << ptr.getClass().getScript(ptr) << std::endl; } while (arg0 > 0) { - std::string notes = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view notes = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!notes.empty()) msg << "Notes: " << notes << std::endl; @@ -1402,9 +1427,9 @@ namespace MWScript public: void execute(Interpreter::Runtime &runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + std::string levId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view creatureId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); @@ -1420,9 +1445,9 @@ namespace MWScript public: void execute(Interpreter::Runtime &runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + std::string levId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view creatureId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); @@ -1438,9 +1463,9 @@ namespace MWScript public: void execute(Interpreter::Runtime &runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + std::string levId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view itemId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); @@ -1456,9 +1481,9 @@ namespace MWScript public: void execute(Interpreter::Runtime &runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + std::string levId{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view itemId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); @@ -1567,125 +1592,154 @@ namespace MWScript } }; + class OpHelp : public Interpreter::Opcode0 + { + public: + + void execute(Interpreter::Runtime& runtime) override + { + std::stringstream message; + message << MWBase::Environment::get().getWindowManager()->getVersionDescription() << "\n\n"; + std::vector commands; + MWBase::Environment::get().getScriptManager()->getExtensions().listKeywords(commands); + for(const auto& command : commands) + message << command << "\n"; + runtime.getContext().report(message.str()); + } + }; + + class OpReloadLua : public Interpreter::Opcode0 + { + public: + + void execute (Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getLuaManager()->reloadAllScripts(); + runtime.getContext().report("All Lua scripts are reloaded"); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode); - interpreter.installSegment5 (Compiler::Misc::opcodeRandom, new OpRandom); - interpreter.installSegment5 (Compiler::Misc::opcodeScriptRunning, new OpScriptRunning); - interpreter.installSegment5 (Compiler::Misc::opcodeStartScript, new OpStartScript); - interpreter.installSegment5 (Compiler::Misc::opcodeStartScriptExplicit, new OpStartScript); - interpreter.installSegment5 (Compiler::Misc::opcodeStopScript, new OpStopScript); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSecondsPassed, new OpGetSecondsPassed); - interpreter.installSegment5 (Compiler::Misc::opcodeEnable, new OpEnable); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableExplicit, new OpEnable); - interpreter.installSegment5 (Compiler::Misc::opcodeDisable, new OpDisable); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableExplicit, new OpDisable); - interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabled, new OpGetDisabled); - interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabledExplicit, new OpGetDisabled); - interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); - interpreter.installSegment5 (Compiler::Misc::opcodeOnActivate, new OpOnActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeOnActivateExplicit, new OpOnActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeActivate, new OpActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeActivateExplicit, new OpActivate); - interpreter.installSegment3 (Compiler::Misc::opcodeLock, new OpLock); - interpreter.installSegment3 (Compiler::Misc::opcodeLockExplicit, new OpLock); - interpreter.installSegment5 (Compiler::Misc::opcodeUnlock, new OpUnlock); - interpreter.installSegment5 (Compiler::Misc::opcodeUnlockExplicit, new OpUnlock); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionDebug, new OpToggleCollisionDebug); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionBoxes, new OpToggleCollisionBoxes); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWireframe, new OpToggleWireframe); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeIn, new OpFadeIn); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeOut, new OpFadeOut); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeTo, new OpFadeTo); - interpreter.installSegment5 (Compiler::Misc::opcodeTogglePathgrid, new OpTogglePathgrid); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWater, new OpToggleWater); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWorld, new OpToggleWorld); - interpreter.installSegment5 (Compiler::Misc::opcodeDontSaveObject, new OpDontSaveObject); - interpreter.installSegment5 (Compiler::Misc::opcodePcForce1stPerson, new OpPcForce1stPerson); - interpreter.installSegment5 (Compiler::Misc::opcodePcForce3rdPerson, new OpPcForce3rdPerson); - interpreter.installSegment5 (Compiler::Misc::opcodePcGet3rdPerson, new OpPcGet3rdPerson); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleVanityMode, new OpToggleVanityMode); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcSleep, new OpGetPcSleep); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcJumping, new OpGetPcJumping); - interpreter.installSegment5 (Compiler::Misc::opcodeWakeUpPc, new OpWakeUpPc); - interpreter.installSegment5 (Compiler::Misc::opcodePlayBink, new OpPlayBink); - interpreter.installSegment5 (Compiler::Misc::opcodePayFine, new OpPayFine); - interpreter.installSegment5 (Compiler::Misc::opcodePayFineThief, new OpPayFineThief); - interpreter.installSegment5 (Compiler::Misc::opcodeGoToJail, new OpGoToJail); - interpreter.installSegment5 (Compiler::Misc::opcodeGetLocked, new OpGetLocked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetLockedExplicit, new OpGetLocked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetEffect, new OpGetEffect); - interpreter.installSegment5 (Compiler::Misc::opcodeGetEffectExplicit, new OpGetEffect); - interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGem, new OpAddSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGemExplicit, new OpAddSoulGem); - interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGem, new OpRemoveSoulGem); - interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGemExplicit, new OpRemoveSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeDrop, new OpDrop); - interpreter.installSegment5 (Compiler::Misc::opcodeDropExplicit, new OpDrop); - interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGem, new OpDropSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGemExplicit, new OpDropSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeGetAttacked, new OpGetAttacked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetAttackedExplicit, new OpGetAttacked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawn, new OpGetWeaponDrawn); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawnExplicit, new OpGetWeaponDrawn); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadied, new OpGetSpellReadied); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadiedExplicit, new OpGetSpellReadied); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffects, new OpGetSpellEffects); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffectsExplicit, new OpGetSpellEffects); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCurrentTime, new OpGetCurrentTime); - interpreter.installSegment5 (Compiler::Misc::opcodeSetDelete, new OpSetDelete); - interpreter.installSegment5 (Compiler::Misc::opcodeSetDeleteExplicit, new OpSetDelete); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSquareRoot, new OpGetSquareRoot); - interpreter.installSegment5 (Compiler::Misc::opcodeFall, new OpFall); - interpreter.installSegment5 (Compiler::Misc::opcodeFallExplicit, new OpFall); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPc, new OpGetStandingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPcExplicit, new OpGetStandingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActor, new OpGetStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActorExplicit, new OpGetStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPc, new OpGetCollidingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPcExplicit, new OpGetCollidingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActor, new OpGetCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActorExplicit, new OpGetCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActor, new OpHurtStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActorExplicit, new OpHurtStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActor, new OpHurtCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActorExplicit, new OpHurtCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWindSpeed, new OpGetWindSpeed); - interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMe, new OpHitOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMeExplicit, new OpHitOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMe, new OpHitAttemptOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMeExplicit, new OpHitAttemptOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableTeleporting, new OpEnableTeleporting); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableTeleporting, new OpEnableTeleporting); - interpreter.installSegment5 (Compiler::Misc::opcodeShowVars, new OpShowVars); - interpreter.installSegment5 (Compiler::Misc::opcodeShowVarsExplicit, new OpShowVars); - interpreter.installSegment5 (Compiler::Misc::opcodeShow, new OpShow); - interpreter.installSegment5 (Compiler::Misc::opcodeShowExplicit, new OpShow); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleGodMode, new OpToggleGodMode); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleScripts, new OpToggleScripts); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableLevitation, new OpEnableLevitation); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableLevitation, new OpEnableLevitation); - interpreter.installSegment5 (Compiler::Misc::opcodeCast, new OpCast); - interpreter.installSegment5 (Compiler::Misc::opcodeCastExplicit, new OpCast); - interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpell, new OpExplodeSpell); - interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpellExplicit, new OpExplodeSpell); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcInJail, new OpGetPcInJail); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); - interpreter.installSegment3 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); - interpreter.installSegment3 (Compiler::Misc::opcodeBetaCommentExplicit, new OpBetaComment); - interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevCreature, new OpAddToLevCreature); - interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevCreature, new OpRemoveFromLevCreature); - interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevItem, new OpAddToLevItem); - interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); - interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph); - interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleNavMesh, new OpToggleNavMesh); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleActorsPaths, new OpToggleActorsPaths); - interpreter.installSegment5 (Compiler::Misc::opcodeSetNavMeshNumberToRender, new OpSetNavMeshNumberToRender); - interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMe, new OpRepairedOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh); + interpreter.installSegment5(Compiler::Misc::opcodeMenuMode); + interpreter.installSegment5(Compiler::Misc::opcodeRandom); + interpreter.installSegment5(Compiler::Misc::opcodeScriptRunning); + interpreter.installSegment5>(Compiler::Misc::opcodeStartScript); + interpreter.installSegment5>(Compiler::Misc::opcodeStartScriptExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeStopScript); + interpreter.installSegment5(Compiler::Misc::opcodeGetSecondsPassed); + interpreter.installSegment5>(Compiler::Misc::opcodeEnable); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDisable); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetDisabled); + interpreter.installSegment5>(Compiler::Misc::opcodeGetDisabledExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeXBox); + interpreter.installSegment5>(Compiler::Misc::opcodeOnActivate); + interpreter.installSegment5>(Compiler::Misc::opcodeOnActivateExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeActivate); + interpreter.installSegment5>(Compiler::Misc::opcodeActivateExplicit); + interpreter.installSegment3>(Compiler::Misc::opcodeLock); + interpreter.installSegment3>(Compiler::Misc::opcodeLockExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeUnlock); + interpreter.installSegment5>(Compiler::Misc::opcodeUnlockExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleCollisionDebug); + interpreter.installSegment5(Compiler::Misc::opcodeToggleCollisionBoxes); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWireframe); + interpreter.installSegment5(Compiler::Misc::opcodeFadeIn); + interpreter.installSegment5(Compiler::Misc::opcodeFadeOut); + interpreter.installSegment5(Compiler::Misc::opcodeFadeTo); + interpreter.installSegment5(Compiler::Misc::opcodeTogglePathgrid); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWater); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWorld); + interpreter.installSegment5(Compiler::Misc::opcodeDontSaveObject); + interpreter.installSegment5(Compiler::Misc::opcodePcForce1stPerson); + interpreter.installSegment5(Compiler::Misc::opcodePcForce3rdPerson); + interpreter.installSegment5(Compiler::Misc::opcodePcGet3rdPerson); + interpreter.installSegment5(Compiler::Misc::opcodeToggleVanityMode); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcSleep); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcJumping); + interpreter.installSegment5(Compiler::Misc::opcodeWakeUpPc); + interpreter.installSegment5(Compiler::Misc::opcodePlayBink); + interpreter.installSegment5(Compiler::Misc::opcodePayFine); + interpreter.installSegment5(Compiler::Misc::opcodePayFineThief); + interpreter.installSegment5(Compiler::Misc::opcodeGoToJail); + interpreter.installSegment5>(Compiler::Misc::opcodeGetLocked); + interpreter.installSegment5>(Compiler::Misc::opcodeGetLockedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetEffect); + interpreter.installSegment5>(Compiler::Misc::opcodeGetEffectExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeAddSoulGem); + interpreter.installSegment5>(Compiler::Misc::opcodeAddSoulGemExplicit); + interpreter.installSegment3>(Compiler::Misc::opcodeRemoveSoulGem); + interpreter.installSegment3>(Compiler::Misc::opcodeRemoveSoulGemExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDrop); + interpreter.installSegment5>(Compiler::Misc::opcodeDropExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDropSoulGem); + interpreter.installSegment5>(Compiler::Misc::opcodeDropSoulGemExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetAttacked); + interpreter.installSegment5>(Compiler::Misc::opcodeGetAttackedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetWeaponDrawn); + interpreter.installSegment5>(Compiler::Misc::opcodeGetWeaponDrawnExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellReadied); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellReadiedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellEffects); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellEffectsExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetCurrentTime); + interpreter.installSegment5>(Compiler::Misc::opcodeSetDelete); + interpreter.installSegment5>(Compiler::Misc::opcodeSetDeleteExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetSquareRoot); + interpreter.installSegment5>(Compiler::Misc::opcodeFall); + interpreter.installSegment5>(Compiler::Misc::opcodeFallExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingPc); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingPcExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingActor); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingPc); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingPcExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingActor); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtStandingActor); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtStandingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtCollidingActor); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtCollidingActorExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetWindSpeed); + interpreter.installSegment5>(Compiler::Misc::opcodeHitOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeHitOnMeExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHitAttemptOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeHitAttemptOnMeExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableTeleporting); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableTeleporting); + interpreter.installSegment5>(Compiler::Misc::opcodeShowVars); + interpreter.installSegment5>(Compiler::Misc::opcodeShowVarsExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeShow); + interpreter.installSegment5>(Compiler::Misc::opcodeShowExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleGodMode); + interpreter.installSegment5(Compiler::Misc::opcodeToggleScripts); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableLevitation); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableLevitation); + interpreter.installSegment5>(Compiler::Misc::opcodeCast); + interpreter.installSegment5>(Compiler::Misc::opcodeCastExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeExplodeSpell); + interpreter.installSegment5>(Compiler::Misc::opcodeExplodeSpellExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcInJail); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcTraveling); + interpreter.installSegment3>(Compiler::Misc::opcodeBetaComment); + interpreter.installSegment3>(Compiler::Misc::opcodeBetaCommentExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeAddToLevCreature); + interpreter.installSegment5(Compiler::Misc::opcodeRemoveFromLevCreature); + interpreter.installSegment5(Compiler::Misc::opcodeAddToLevItem); + interpreter.installSegment5(Compiler::Misc::opcodeRemoveFromLevItem); + interpreter.installSegment3>(Compiler::Misc::opcodeShowSceneGraph); + interpreter.installSegment3>(Compiler::Misc::opcodeShowSceneGraphExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleBorders); + interpreter.installSegment5(Compiler::Misc::opcodeToggleNavMesh); + interpreter.installSegment5(Compiler::Misc::opcodeToggleActorsPaths); + interpreter.installSegment5(Compiler::Misc::opcodeSetNavMeshNumberToRender); + interpreter.installSegment5>(Compiler::Misc::opcodeRepairedOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeRepairedOnMeExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleRecastMesh); + interpreter.installSegment5(Compiler::Misc::opcodeHelp); + interpreter.installSegment5(Compiler::Misc::opcodeReloadLua); } } } diff --git a/apps/openmw/mwscript/ref.cpp b/apps/openmw/mwscript/ref.cpp index 6347c2c2e5..145cd2cd25 100644 --- a/apps/openmw/mwscript/ref.cpp +++ b/apps/openmw/mwscript/ref.cpp @@ -10,7 +10,7 @@ MWWorld::Ptr MWScript::ExplicitRef::operator() (Interpreter::Runtime& runtime, bool required, bool activeOnly) const { - std::string id = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view id = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (required) diff --git a/apps/openmw/mwscript/ref.hpp b/apps/openmw/mwscript/ref.hpp index e572f51471..8c7657280f 100644 --- a/apps/openmw/mwscript/ref.hpp +++ b/apps/openmw/mwscript/ref.hpp @@ -1,8 +1,6 @@ #ifndef GAME_MWSCRIPT_REF_H #define GAME_MWSCRIPT_REF_H -#include - #include "../mwworld/ptr.hpp" namespace Interpreter @@ -14,7 +12,7 @@ namespace MWScript { struct ExplicitRef { - static const bool implicit = false; + static constexpr bool implicit = false; MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; @@ -22,7 +20,7 @@ namespace MWScript struct ImplicitRef { - static const bool implicit = true; + static constexpr bool implicit = true; MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; diff --git a/apps/openmw/mwscript/scriptmanagerimp.cpp b/apps/openmw/mwscript/scriptmanagerimp.cpp index e1652b311c..7efb5148fa 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.cpp +++ b/apps/openmw/mwscript/scriptmanagerimp.cpp @@ -7,7 +7,7 @@ #include -#include +#include #include @@ -109,7 +109,8 @@ namespace MWScript } // execute script - if (!iter->second.mByteCode.empty() && iter->second.mActive) + std::string target = Misc::StringUtils::lowerCase(interpreterContext.getTarget()); + if (!iter->second.mByteCode.empty() && iter->second.mInactive.find(target) == iter->second.mInactive.end()) try { if (!mOpcodesInstalled) @@ -129,7 +130,7 @@ namespace MWScript { Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); - iter->second.mActive = false; // don't execute again. + iter->second.mInactive.insert(target); // don't execute again. } return false; } @@ -138,7 +139,7 @@ namespace MWScript { for (auto& script : mScripts) { - script.second.mActive = true; + script.second.mInactive.clear(); } mGlobalScripts.clear(); @@ -169,14 +170,14 @@ namespace MWScript std::string name2 = Misc::StringUtils::lowerCase (name); { - ScriptCollection::iterator iter = mScripts.find (name2); + auto iter = mScripts.find (name2); if (iter!=mScripts.end()) return iter->second.mLocals; } { - std::map::iterator iter = mOtherLocals.find (name2); + auto iter = mOtherLocals.find (name2); if (iter!=mOtherLocals.end()) return iter->second; @@ -191,10 +192,22 @@ namespace MWScript std::istringstream stream (script->mScriptText); Compiler::QuickFileParser parser (mErrorHandler, mCompilerContext, locals); Compiler::Scanner scanner (mErrorHandler, stream, mCompilerContext.getExtensions()); - scanner.scan (parser); + try + { + scanner.scan (parser); + } + catch (const Compiler::SourceException&) + { + // error has already been reported via error handler + locals.clear(); + } + catch (const std::exception& error) + { + Log(Debug::Error) << "Error: An exception has been thrown: " << error.what(); + locals.clear(); + } - std::map::iterator iter = - mOtherLocals.emplace(name2, locals).first; + auto iter = mOtherLocals.emplace(name2, locals).first; return iter->second; } @@ -206,4 +219,9 @@ namespace MWScript { return mGlobalScripts; } + + const Compiler::Extensions& ScriptManager::getExtensions() const + { + return *mCompilerContext.getExtensions(); + } } diff --git a/apps/openmw/mwscript/scriptmanagerimp.hpp b/apps/openmw/mwscript/scriptmanagerimp.hpp index 7ddcd2489d..a82e3f92e8 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.hpp +++ b/apps/openmw/mwscript/scriptmanagerimp.hpp @@ -2,6 +2,7 @@ #define GAME_SCRIPT_SCRIPTMANAGER_H #include +#include #include #include @@ -45,14 +46,11 @@ namespace MWScript { std::vector mByteCode; Compiler::Locals mLocals; - bool mActive; - - CompiledScript(const std::vector& code, const Compiler::Locals& locals) - { - mByteCode = code; - mLocals = locals; - mActive = true; - } + std::set mInactive; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals): + mByteCode(code), mLocals(locals) + {} }; typedef std::map ScriptCollection; @@ -85,6 +83,8 @@ namespace MWScript ///< Return locals for script \a name. GlobalScripts& getGlobalScripts() override; + + const Compiler::Extensions& getExtensions() const override; }; } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 2b6bf826f9..f81645b280 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -11,6 +11,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" + #include "interpretercontext.hpp" namespace MWScript @@ -85,13 +87,17 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string region = runtime.getStringLiteral (runtime[0].mInteger); + std::string region{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); Interpreter::Type_Integer id = runtime[0].mInteger; runtime.pop(); - MWBase::Environment::get().getWorld()->changeWeather(region, id); + const ESM::Region* reg = MWBase::Environment::get().getWorld()->getStore().get().search(region); + if (reg) + MWBase::Environment::get().getWorld()->changeWeather(region, id); + else + runtime.getContext().report("Warning: Region \"" + region + "\" was not found"); } }; @@ -101,14 +107,14 @@ namespace MWScript void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { - std::string region = runtime.getStringLiteral (runtime[0].mInteger); + std::string region{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); std::vector chances; chances.reserve(10); while(arg0 > 0) { - chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 127)); runtime.pop(); arg0--; } @@ -120,14 +126,14 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Sky::opcodeToggleSky, new OpToggleSky); - interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonWhite, new OpTurnMoonWhite); - interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonRed, new OpTurnMoonRed); - interpreter.installSegment5 (Compiler::Sky::opcodeGetMasserPhase, new OpGetMasserPhase); - interpreter.installSegment5 (Compiler::Sky::opcodeGetSecundaPhase, new OpGetSecundaPhase); - interpreter.installSegment5 (Compiler::Sky::opcodeGetCurrentWeather, new OpGetCurrentWeather); - interpreter.installSegment5 (Compiler::Sky::opcodeChangeWeather, new OpChangeWeather); - interpreter.installSegment3 (Compiler::Sky::opcodeModRegion, new OpModRegion); + interpreter.installSegment5(Compiler::Sky::opcodeToggleSky); + interpreter.installSegment5(Compiler::Sky::opcodeTurnMoonWhite); + interpreter.installSegment5(Compiler::Sky::opcodeTurnMoonRed); + interpreter.installSegment5(Compiler::Sky::opcodeGetMasserPhase); + interpreter.installSegment5(Compiler::Sky::opcodeGetSecundaPhase); + interpreter.installSegment5(Compiler::Sky::opcodeGetCurrentWeather); + interpreter.installSegment5(Compiler::Sky::opcodeChangeWeather); + interpreter.installSegment3(Compiler::Sky::opcodeModRegion); } } } diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index 6eab758f19..1e0ad12946 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -33,10 +33,10 @@ namespace MWScript MWScript::InterpreterContext& context = static_cast (runtime.getContext()); - std::string file = runtime.getStringLiteral (runtime[0].mInteger); + std::string file{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - std::string text = runtime.getStringLiteral (runtime[0].mInteger); + std::string text{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); MWBase::Environment::get().getSoundManager()->say (ptr, file); @@ -65,7 +65,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string sound{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); MWBase::Environment::get().getSoundManager()->streamMusic (sound); @@ -78,7 +78,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view sound = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound(sound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); @@ -91,7 +91,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view sound = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float volume = runtime[0].mFloat; @@ -104,43 +104,35 @@ namespace MWScript } }; - template + template class OpPlaySound3D : public Interpreter::Opcode0 { - bool mLoop; - public: - OpPlaySound3D (bool loop) : mLoop (loop) {} - void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view sound = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, MWSound::Type::Sfx, - mLoop ? MWSound::PlayMode::LoopRemoveAtDistance + TLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); } }; - template + template class OpPlaySoundVP3D : public Interpreter::Opcode0 { - bool mLoop; - public: - OpPlaySoundVP3D (bool loop) : mLoop (loop) {} - void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view sound = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float volume = runtime[0].mFloat; @@ -151,7 +143,7 @@ namespace MWScript MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, MWSound::Type::Sfx, - mLoop ? MWSound::PlayMode::LoopRemoveAtDistance + TLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); } @@ -166,7 +158,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view sound = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr, sound); @@ -205,34 +197,28 @@ namespace MWScript }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Sound::opcodeSay, new OpSay); - interpreter.installSegment5 (Compiler::Sound::opcodeSayDone, new OpSayDone); - interpreter.installSegment5 (Compiler::Sound::opcodeStreamMusic, new OpStreamMusic); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound, new OpPlaySound); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySoundVP, new OpPlaySoundVP); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3D, new OpPlaySound3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVP, new OpPlaySoundVP3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3D, new OpPlaySound3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVP, - new OpPlaySoundVP3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodeStopSound, new OpStopSound); - interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlaying, new OpGetSoundPlaying); - - interpreter.installSegment5 (Compiler::Sound::opcodeSayExplicit, new OpSay); - interpreter.installSegment5 (Compiler::Sound::opcodeSayDoneExplicit, new OpSayDone); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DExplicit, - new OpPlaySound3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVPExplicit, - new OpPlaySoundVP3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DExplicit, - new OpPlaySound3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVPExplicit, - new OpPlaySoundVP3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodeStopSoundExplicit, new OpStopSound); - interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlayingExplicit, - new OpGetSoundPlaying); + interpreter.installSegment5>(Compiler::Sound::opcodeSay); + interpreter.installSegment5>(Compiler::Sound::opcodeSayDone); + interpreter.installSegment5(Compiler::Sound::opcodeStreamMusic); + interpreter.installSegment5(Compiler::Sound::opcodePlaySound); + interpreter.installSegment5(Compiler::Sound::opcodePlaySoundVP); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3D); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3DVP); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3D); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3DVP); + interpreter.installSegment5>(Compiler::Sound::opcodeStopSound); + interpreter.installSegment5>(Compiler::Sound::opcodeGetSoundPlaying); + + interpreter.installSegment5>(Compiler::Sound::opcodeSayExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeSayDoneExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3DExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3DVPExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3DExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3DVPExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeStopSoundExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeGetSoundPlayingExplicit); } } } diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 58a943e1a9..385a688afe 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include "../mwworld/esmstore.hpp" @@ -31,7 +31,7 @@ namespace { - std::string getDialogueActorFaction(MWWorld::ConstPtr actor) + std::string getDialogueActorFaction(const MWWorld::ConstPtr& actor) { std::string factionId = actor.getClass().getPrimaryFaction(actor); if (factionId.empty()) @@ -40,6 +40,28 @@ namespace return factionId; } + + void modStat(MWMechanics::AttributeValue& stat, float amount) + { + const float base = stat.getBase(); + const float modifier = stat.getModifier() - stat.getDamage(); + const float modified = base + modifier; + // Clamp to 100 unless base < 100 and we have a fortification going + if((modifier <= 0.f || base >= 100.f) && amount > 0.f) + amount = std::clamp(100.f - modified, 0.f, amount); + // Clamp the modified value in a way that doesn't properly account for negative numbers + float newModified = modified + amount; + if(newModified < 0.f) + { + if(modified >= 0.f) + newModified = 0.f; + else if(newModified < modified) + newModified = modified; + } + // Calculate damage/fortification based on the clamped base value + stat.setBase(std::clamp(base + amount, 0.f, 100.f), true); + stat.setModifier(newModified - stat.getBase()); + } } namespace MWScript @@ -122,7 +144,7 @@ namespace MWScript runtime.pop(); MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); - attribute.setBase (value); + attribute.setBase(value, true); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; @@ -146,19 +168,7 @@ namespace MWScript MWMechanics::AttributeValue attribute = ptr.getClass() .getCreatureStats(ptr) .getAttribute(mIndex); - - if (value == 0) - return; - - if (((attribute.getBase() <= 0) && (value < 0)) - || ((attribute.getBase() >= 100) && (value > 0))) - return; - - if (value < 0) - attribute.setBase(std::max(0.f, attribute.getBase() + value)); - else - attribute.setBase(std::min(100.f, attribute.getBase() + value)); - + modStat(attribute, value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; @@ -187,6 +197,9 @@ namespace MWScript .getCreatureStats(ptr) .getDynamic(mIndex) .getCurrent(); + // GetMagicka shouldn't return negative values + if(mIndex == 1 && value < 0) + value = 0; } runtime.push (value); } @@ -211,8 +224,8 @@ namespace MWScript MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); - stat.setModified (value, 0); - stat.setCurrent(value); + stat.setBase(value); + stat.setCurrent(stat.getModified(false), true, true); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } @@ -252,19 +265,18 @@ namespace MWScript } } - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - - Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); - - MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) - .getDynamic (mIndex)); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - stat.setModified (diff + stat.getModified(), 0); - stat.setCurrentModified (diff + stat.getCurrentModified()); + MWMechanics::DynamicStat stat = stats.getDynamic(mIndex); - stat.setCurrent (diff + current); + float current = stat.getCurrent(); + float base = diff + stat.getBase(); + if(mIndex != 2) + base = std::max(base, 0.f); + stat.setBase(base); + stat.setCurrent(diff + current, true, true); - ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); + stats.setDynamic (mIndex, stat); } }; @@ -318,17 +330,9 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - - Interpreter::Type_Float value = 0; - - Interpreter::Type_Float max = stats.getDynamic(mIndex).getModified(); - - if (max>0) - value = stats.getDynamic(mIndex).getCurrent() / max; - - runtime.push (value); + runtime.push(stats.getDynamic(mIndex).getRatio()); } }; @@ -369,7 +373,7 @@ namespace MWScript MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats (ptr); - stats.getSkill (mIndex).setBase (value); + stats.getSkill(mIndex).setBase(value, true); } }; @@ -392,18 +396,7 @@ namespace MWScript MWMechanics::SkillValue &skill = ptr.getClass() .getNpcStats(ptr) .getSkill(mIndex); - - if (value == 0) - return; - - if (((skill.getBase() <= 0.f) && (value < 0.f)) - || ((skill.getBase() >= 100.f) && (value > 0.f))) - return; - - if (value < 0) - skill.setBase(std::max(0.f, skill.getBase() + value)); - else - skill.setBase(std::min(100.f, skill.getBase() + value)); + modStat(skill, value); } }; @@ -460,16 +453,18 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); + std::string id{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find (id); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - creatureStats.getSpells().add(id); + creatureStats.getSpells().add(spell); ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) { + // Add spell effect to *this actor's* queue immediately + creatureStats.getActiveSpells().addSpell(spell, ptr); // Apply looping particles immediately for constant effects MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } @@ -485,21 +480,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); + std::string id{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - // The spell may have an instant effect which must be handled before the spell's removal. - for (const auto& effect : creatureStats.getSpells().getMagicEffects()) - { - if (effect.second.getMagnitude() <= 0) - continue; - MWMechanics::CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) - creatureStats.getSpells().purgeEffect(effect.first.mId); - } - - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); creatureStats.getSpells().remove (id); MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); @@ -521,11 +505,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view spellid = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); - ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -541,7 +524,7 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; @@ -555,7 +538,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); + std::string id{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); Interpreter::Type_Integer value = 0; @@ -576,7 +559,7 @@ namespace MWScript { MWWorld::ConstPtr actor = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0==0) { @@ -608,7 +591,7 @@ namespace MWScript { MWWorld::ConstPtr actor = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0==0) { @@ -647,7 +630,7 @@ namespace MWScript { MWWorld::ConstPtr actor = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0==0) { @@ -679,7 +662,7 @@ namespace MWScript { MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0 >0) { factionID = runtime.getStringLiteral (runtime[0].mInteger); @@ -771,7 +754,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string id = runtime.getStringLiteral (runtime[0].mInteger); + std::string id{runtime.getStringLiteral(runtime[0].mInteger)}; runtime[0].mInteger = MWBase::Environment::get().getMechanicsManager()->countDeaths (id); } }; @@ -913,14 +896,12 @@ namespace MWScript { MWWorld::ConstPtr ptr = R()(runtime); - std::string race = runtime.getStringLiteral(runtime[0].mInteger); - ::Misc::StringUtils::lowerCaseInPlace(race); + std::string_view race = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string npcRace = ptr.get()->mBase->mRace; - ::Misc::StringUtils::lowerCaseInPlace(npcRace); + const std::string& npcRace = ptr.get()->mBase->mRace; - runtime.push (npcRace == race); + runtime.push(::Misc::StringUtils::ciEqual(race, npcRace)); } }; @@ -945,7 +926,7 @@ namespace MWScript { MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); @@ -977,7 +958,7 @@ namespace MWScript { MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); @@ -1004,7 +985,7 @@ namespace MWScript { MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionID = ""; + std::string factionID; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); @@ -1201,12 +1182,23 @@ namespace MWScript { bool wasEnabled = ptr.getRefData().isEnabled(); MWBase::Environment::get().getWorld()->undeleteObject(ptr); - MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); - + auto windowManager = MWBase::Environment::get().getWindowManager(); + bool wasOpen = windowManager->containsMode(MWGui::GM_Container); + windowManager->onDeleteCustomData(ptr); // HACK: disable/enable object to re-add it to the scene properly (need a new Animation). MWBase::Environment::get().getWorld()->disable(ptr); - // resets runtime state such as inventory, stats and AI. does not reset position in the world - ptr.getRefData().setCustomData(nullptr); + if (wasOpen && !windowManager->containsMode(MWGui::GM_Container)) + { + // Reopen the loot GUI if it was closed because we resurrected the actor we were looting + MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); + windowManager->forceLootMode(ptr); + } + else + { + MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + // resets runtime state such as inventory, stats and AI. does not reset position in the world + ptr.getRefData().setCustomData(nullptr); + } if (wasEnabled) MWBase::Environment::get().getWorld()->enable(ptr); } @@ -1220,6 +1212,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { // dummy + runtime.pop(); runtime.push(0); } }; @@ -1328,146 +1321,132 @@ namespace MWScript { for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetAttributeExplicit+i, - new OpGetAttribute (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeGetAttribute + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetAttributeExplicit + i, i); - interpreter.installSegment5 (Compiler::Stats::opcodeSetAttribute+i, new OpSetAttribute (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetAttributeExplicit+i, - new OpSetAttribute (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeSetAttribute + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeSetAttributeExplicit + i, i); - interpreter.installSegment5 (Compiler::Stats::opcodeModAttribute+i, new OpModAttribute (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModAttributeExplicit+i, - new OpModAttribute (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeModAttribute + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeModAttributeExplicit + i, i); } for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicExplicit+i, - new OpGetDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamic+i, new OpSetDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamicExplicit+i, - new OpSetDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModDynamic+i, new OpModDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModDynamicExplicit+i, - new OpModDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamic+i, - new OpModCurrentDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamicExplicit+i, - new OpModCurrentDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatio+i, - new OpGetDynamicGetRatio (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatioExplicit+i, - new OpGetDynamicGetRatio (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDynamic + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeSetDynamic + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeSetDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeModDynamic + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeModDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeModCurrentDynamic + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeModCurrentDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetDynamicGetRatio + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDynamicGetRatioExplicit + i, i); } for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetSkillExplicit+i, new OpGetSkill (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSkill + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSkillExplicit + i, i); - interpreter.installSegment5 (Compiler::Stats::opcodeSetSkill+i, new OpSetSkill (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetSkillExplicit+i, new OpSetSkill (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeSetSkill + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeSetSkillExplicit + i, i); - interpreter.installSegment5 (Compiler::Stats::opcodeModSkill+i, new OpModSkill (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModSkillExplicit+i, new OpModSkill (i)); + interpreter.installSegment5>(Compiler::Stats::opcodeModSkill + i, i); + interpreter.installSegment5>(Compiler::Stats::opcodeModSkillExplicit + i, i); } - interpreter.installSegment5 (Compiler::Stats::opcodeGetPCCrimeLevel, new OpGetPCCrimeLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetPCCrimeLevel, new OpSetPCCrimeLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeModPCCrimeLevel, new OpModPCCrimeLevel); - - interpreter.installSegment5 (Compiler::Stats::opcodeAddSpell, new OpAddSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeAddSpellExplicit, new OpAddSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpell, new OpRemoveSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellExplicit, - new OpRemoveSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffects, new OpRemoveSpellEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffectsExplicit, - new OpRemoveSpellEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeResurrect, new OpResurrect); - interpreter.installSegment5 (Compiler::Stats::opcodeResurrectExplicit, - new OpResurrect); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffects, new OpRemoveEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffectsExplicit, - new OpRemoveEffects); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetSpell, new OpGetSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeGetSpellExplicit, new OpGetSpell); - - interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRank,new OpPCRaiseRank); - interpreter.installSegment3(Compiler::Stats::opcodePCLowerRank,new OpPCLowerRank); - interpreter.installSegment3(Compiler::Stats::opcodePCJoinFaction,new OpPCJoinFaction); - interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRankExplicit,new OpPCRaiseRank); - interpreter.installSegment3(Compiler::Stats::opcodePCLowerRankExplicit,new OpPCLowerRank); - interpreter.installSegment3(Compiler::Stats::opcodePCJoinFactionExplicit,new OpPCJoinFaction); - interpreter.installSegment3(Compiler::Stats::opcodeGetPCRank,new OpGetPCRank); - interpreter.installSegment3(Compiler::Stats::opcodeGetPCRankExplicit,new OpGetPCRank); - - interpreter.installSegment5(Compiler::Stats::opcodeModDisposition,new OpModDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeModDispositionExplicit,new OpModDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeSetDisposition,new OpSetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeSetDispositionExplicit,new OpSetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeGetDisposition,new OpGetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeGetDispositionExplicit,new OpGetDisposition); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetLevel, new OpGetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeGetLevelExplicit, new OpGetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetLevel, new OpSetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetLevelExplicit, new OpSetLevel); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetDeadCount, new OpGetDeadCount); - - interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRep, new OpGetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRepExplicit, new OpGetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRep, new OpSetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRepExplicit, new OpSetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRep, new OpModPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRepExplicit, new OpModPCFacRep); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDisease, new OpGetCommonDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDiseaseExplicit, new OpGetCommonDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDisease, new OpGetBlightDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDiseaseExplicit, new OpGetBlightDisease); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetRace, new OpGetRace); - interpreter.installSegment5 (Compiler::Stats::opcodeGetRaceExplicit, new OpGetRace); - interpreter.installSegment5 (Compiler::Stats::opcodeGetWerewolfKills, new OpGetWerewolfKills); - - interpreter.installSegment3 (Compiler::Stats::opcodePcExpelled, new OpPcExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpelledExplicit, new OpPcExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpell, new OpPcExpell); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpellExplicit, new OpPcExpell); - interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelled, new OpPcClearExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelledExplicit, new OpPcClearExpelled); - interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRank, new OpRaiseRank); - interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRankExplicit, new OpRaiseRank); - interpreter.installSegment5 (Compiler::Stats::opcodeLowerRank, new OpLowerRank); - interpreter.installSegment5 (Compiler::Stats::opcodeLowerRankExplicit, new OpLowerRank); - - interpreter.installSegment5 (Compiler::Stats::opcodeOnDeath, new OpOnDeath); - interpreter.installSegment5 (Compiler::Stats::opcodeOnDeathExplicit, new OpOnDeath); - interpreter.installSegment5 (Compiler::Stats::opcodeOnMurder, new OpOnMurder); - interpreter.installSegment5 (Compiler::Stats::opcodeOnMurderExplicit, new OpOnMurder); - interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockout, new OpOnKnockout); - interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockoutExplicit, new OpOnKnockout); - - interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolf, new OpIsWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolfExplicit, new OpIsWerewolf); - - interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolf, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolfExplicit, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolf, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolfExplicit, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobatics, new OpSetWerewolfAcrobatics); - interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); - interpreter.installSegment5 (Compiler::Stats::opcodeGetStat, new OpGetStat); - interpreter.installSegment5 (Compiler::Stats::opcodeGetStatExplicit, new OpGetStat); + interpreter.installSegment5(Compiler::Stats::opcodeGetPCCrimeLevel); + interpreter.installSegment5(Compiler::Stats::opcodeSetPCCrimeLevel); + interpreter.installSegment5(Compiler::Stats::opcodeModPCCrimeLevel); + + interpreter.installSegment5>(Compiler::Stats::opcodeAddSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeAddSpellExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpellExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpellEffects); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpellEffectsExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeResurrect); + interpreter.installSegment5>(Compiler::Stats::opcodeResurrectExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveEffects); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveEffectsExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSpellExplicit); + + interpreter.installSegment3>(Compiler::Stats::opcodePCRaiseRank); + interpreter.installSegment3>(Compiler::Stats::opcodePCLowerRank); + interpreter.installSegment3>(Compiler::Stats::opcodePCJoinFaction); + interpreter.installSegment3>(Compiler::Stats::opcodePCRaiseRankExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePCLowerRankExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePCJoinFactionExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCRank); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCRankExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeModDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeModDispositionExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeSetDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeSetDispositionExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDispositionExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetLevel); + interpreter.installSegment5>(Compiler::Stats::opcodeGetLevelExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeSetLevel); + interpreter.installSegment5>(Compiler::Stats::opcodeSetLevelExplicit); + + interpreter.installSegment5(Compiler::Stats::opcodeGetDeadCount); + + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCFacRepExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeSetPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeSetPCFacRepExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeModPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeModPCFacRepExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetCommonDisease); + interpreter.installSegment5>(Compiler::Stats::opcodeGetCommonDiseaseExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetBlightDisease); + interpreter.installSegment5>(Compiler::Stats::opcodeGetBlightDiseaseExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetRace); + interpreter.installSegment5>(Compiler::Stats::opcodeGetRaceExplicit); + interpreter.installSegment5(Compiler::Stats::opcodeGetWerewolfKills); + + interpreter.installSegment3>(Compiler::Stats::opcodePcExpelled); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpelledExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpell); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpellExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePcClearExpelled); + interpreter.installSegment3>(Compiler::Stats::opcodePcClearExpelledExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRaiseRank); + interpreter.installSegment5>(Compiler::Stats::opcodeRaiseRankExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeLowerRank); + interpreter.installSegment5>(Compiler::Stats::opcodeLowerRankExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeOnDeath); + interpreter.installSegment5>(Compiler::Stats::opcodeOnDeathExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeOnMurder); + interpreter.installSegment5>(Compiler::Stats::opcodeOnMurderExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeOnKnockout); + interpreter.installSegment5>(Compiler::Stats::opcodeOnKnockoutExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeIsWerewolf); + interpreter.installSegment5>(Compiler::Stats::opcodeIsWerewolfExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeBecomeWerewolf); + interpreter.installSegment5>(Compiler::Stats::opcodeBecomeWerewolfExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeUndoWerewolf); + interpreter.installSegment5>(Compiler::Stats::opcodeUndoWerewolfExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeSetWerewolfAcrobatics); + interpreter.installSegment5>(Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetStat); + interpreter.installSegment5>(Compiler::Stats::opcodeGetStatExplicit); static const MagicEffect sMagicEffects[] = { { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka }, @@ -1501,14 +1480,14 @@ namespace MWScript int positive = sMagicEffects[i].mPositiveEffect; int negative = sMagicEffects[i].mNegativeEffect; - interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffect+i, new OpGetMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffectExplicit+i, new OpGetMagicEffect (positive, negative)); + interpreter.installSegment5>(Compiler::Stats::opcodeGetMagicEffect + i, positive, negative); + interpreter.installSegment5>(Compiler::Stats::opcodeGetMagicEffectExplicit + i, positive, negative); - interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffect+i, new OpSetMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffectExplicit+i, new OpSetMagicEffect (positive, negative)); + interpreter.installSegment5>(Compiler::Stats::opcodeSetMagicEffect + i, positive, negative); + interpreter.installSegment5>(Compiler::Stats::opcodeSetMagicEffectExplicit + i, positive, negative); - interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffect+i, new OpModMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffectExplicit+i, new OpModMagicEffect (positive, negative)); + interpreter.installSegment5>(Compiler::Stats::opcodeModMagicEffect + i, positive, negative); + interpreter.installSegment5>(Compiler::Stats::opcodeModMagicEffectExplicit + i, positive, negative); } } } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 41df1870c5..ef99e21880 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include @@ -17,6 +17,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/cellutils.hpp" #include "../mwmechanics/actorutil.hpp" @@ -32,7 +33,7 @@ namespace MWScript std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) - MWBase::Environment::get().getWorld()->queueMovement(actor, diff); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); } template @@ -42,10 +43,19 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - MWWorld::Ptr from = R()(runtime); - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + MWWorld::Ptr from = R()(runtime, !R::implicit); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); + if (from.isEmpty()) + { + std::string error = "Missing implicit ref"; + runtime.getContext().report(error); + Log(Debug::Error) << error; + runtime.push(0.f); + return; + } + if (from.getContainerStore()) // is the object contained? { MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(from); @@ -65,7 +75,7 @@ namespace MWScript const MWWorld::Ptr to = MWBase::Environment::get().getWorld()->searchPtr(name, false); if (to.isEmpty()) { - std::string error = "Failed to find an instance of object '" + name + "'"; + std::string error = "Failed to find an instance of object '" + std::string(name) + "'"; runtime.getContext().report(error); Log(Debug::Error) << error; runtime.push(0.f); @@ -146,7 +156,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float angle = osg::DegreesToRadians(runtime[0].mFloat); runtime.pop(); @@ -158,17 +168,17 @@ namespace MWScript // 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. if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(angle,ay,az),MWBase::RotationFlag_inverseOrder); else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,angle,az),MWBase::RotationFlag_inverseOrder); else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,ay,angle),MWBase::RotationFlag_inverseOrder); else if (axis == "u") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(angle,ay,az),MWBase::RotationFlag_none); else if (axis == "w") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,angle,az),MWBase::RotationFlag_none); else if (axis == "v") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,ay,angle),MWBase::RotationFlag_none); } }; @@ -181,21 +191,26 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - if (axis == "x") - { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0])); - } - else if (axis == "y") - { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1])); - } - else if (axis == "z") + float ret = 0.f; + if (!axis.empty()) { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2])); + if (axis[0] == 'x') + { + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0]); + } + else if (axis[0] == 'y') + { + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1]); + } + else if (axis[0] == 'z') + { + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2]); + } } + runtime.push(ret); } }; @@ -208,21 +223,26 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - if (axis=="x") + float ret = 0.f; + if (!axis.empty()) { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0])); - } - else if (axis=="y") - { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1])); - } - else if (axis=="z") - { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2])); + if (axis[0] == 'x') + { + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0]); + } + else if (axis[0] == 'y') + { + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1]); + } + else if (axis[0] == 'z') + { + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2]); + } } + runtime.push(ret); } }; @@ -235,21 +255,26 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - if(axis == "x") - { - runtime.push(ptr.getRefData().getPosition().pos[0]); - } - else if(axis == "y") + float ret = 0.f; + if (!axis.empty()) { - runtime.push(ptr.getRefData().getPosition().pos[1]); - } - else if(axis == "z") - { - runtime.push(ptr.getRefData().getPosition().pos[2]); + if (axis[0] == 'x') + { + ret = ptr.getRefData().getPosition().pos[0]; + } + else if (axis[0] == 'y') + { + ret = ptr.getRefData().getPosition().pos[1]; + } + else if (axis[0] == 'z') + { + ret = ptr.getRefData().getPosition().pos[2]; + } } + runtime.push(ret); } }; @@ -262,28 +287,25 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - if (!ptr.isInCell()) - return; - - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float pos = runtime[0].mFloat; runtime.pop(); - float ax = ptr.getRefData().getPosition().pos[0]; - float ay = ptr.getRefData().getPosition().pos[1]; - float az = ptr.getRefData().getPosition().pos[2]; + if (!ptr.isInCell()) + return; // Note: SetPos does not skip weather transitions in vanilla engine, so we do not call setTeleported(true) here. - MWWorld::Ptr updated = ptr; + const auto curPos = ptr.getRefData().getPosition().asVec3(); + auto newPos = curPos; if(axis == "x") { - updated = MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az,true); + newPos[0] = pos; } else if(axis == "y") { - updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az,true); + newPos[1] = pos; } else if(axis == "z") { @@ -292,20 +314,21 @@ namespace MWScript { float terrainHeight = -std::numeric_limits::max(); if (ptr.getCell()->isExterior()) - terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(osg::Vec3f(ax, ay, az)); - + terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(curPos); + if (pos < terrainHeight) pos = terrainHeight; } - - updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos,true); + + newPos[2] = pos; } else { return; } - dynamic_cast(runtime.getContext()).updatePtr(ptr,updated); + dynamic_cast(runtime.getContext()).updatePtr(ptr, + MWBase::Environment::get().getWorld()->moveObject(ptr, newPos, true, true)); } }; @@ -318,21 +341,26 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - if(axis == "x") - { - runtime.push(ptr.getCellRef().getPosition().pos[0]); - } - else if(axis == "y") + float ret = 0.f; + if (!axis.empty()) { - runtime.push(ptr.getCellRef().getPosition().pos[1]); - } - else if(axis == "z") - { - runtime.push(ptr.getCellRef().getPosition().pos[2]); + if (axis[0] == 'x') + { + ret = ptr.getCellRef().getPosition().pos[0]; + } + else if (axis[0] == 'y') + { + ret = ptr.getCellRef().getPosition().pos[1]; + } + else if (axis[0] == 'z') + { + ret = ptr.getCellRef().getPosition().pos[2]; + } } + runtime.push(ret); } }; @@ -345,14 +373,6 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - if (ptr.getContainerStore()) - return; - - if (ptr == MWMechanics::getPlayer()) - { - MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); - } - Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; @@ -361,42 +381,54 @@ namespace MWScript runtime.pop(); Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); + std::string cellID{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); - MWWorld::CellStore* store = 0; + if (ptr.getContainerStore()) + return; + + bool isPlayer = ptr == MWMechanics::getPlayer(); + if (isPlayer) + { + MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); + } + + MWWorld::CellStore* store = nullptr; try { store = MWBase::Environment::get().getWorld()->getInterior(cellID); } catch(std::exception&) { - // cell not found, move to exterior instead (vanilla PositionCell compatibility) + // cell not found, move to exterior instead if moving the player (vanilla PositionCell compatibility) const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); if(!cell) { - std::string error = "Warning: PositionCell: unknown interior cell (" + cellID + "), moving to exterior instead"; + std::string error = "Warning: PositionCell: unknown interior cell (" + cellID + ")"; + if(isPlayer) + error += ", moving to exterior instead"; runtime.getContext().report (error); Log(Debug::Warning) << error; + if(!isPlayer) + return; } + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(x, y); + store = MWBase::Environment::get().getWorld()->getExterior(cellIndex.x(), cellIndex.y()); } if(store) { MWWorld::Ptr base = ptr; - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,osg::Vec3f(x,y,z)); dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; + auto rot = ptr.getRefData().getPosition().asRotationVec3(); // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) // except for when you position the player, then degrees must be used. // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. - if(ptr != MWMechanics::getPlayer()) + if(!isPlayer) zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); + rot.z() = osg::DegreesToRadians(zRot); + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); ptr.getClass().adjustPosition(ptr, false); } @@ -412,14 +444,6 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - if (!ptr.isInCell()) - return; - - if (ptr == MWMechanics::getPlayer()) - { - MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); - } - Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; @@ -428,31 +452,38 @@ namespace MWScript runtime.pop(); Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); + + if (!ptr.isInCell()) + return; + + if (ptr == MWMechanics::getPlayer()) + { + MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); + } + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(x, y); // another morrowind oddity: player will be moved to the exterior cell at this location, // non-player actors will move within the cell they are in. MWWorld::Ptr base = ptr; if (ptr == MWMechanics::getPlayer()) { - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cellIndex.x(), cellIndex.y()); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, cell, osg::Vec3(x, y, z)); } else { - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, osg::Vec3f(x, y, z), true, true); } dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; + auto rot = ptr.getRefData().getPosition().asRotationVec3(); // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) // except for when you position the player, then degrees must be used. // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. if(ptr != MWMechanics::getPlayer()) zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); + rot.z() = osg::DegreesToRadians(zRot); + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); ptr.getClass().adjustPosition(ptr, false); } }; @@ -463,9 +494,9 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view itemID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); + std::string cellID{runtime.getStringLiteral(runtime[0].mInteger)}; runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; @@ -477,7 +508,7 @@ namespace MWScript Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; runtime.pop(); - MWWorld::CellStore* store = 0; + MWWorld::CellStore* store = nullptr; try { store = MWBase::Environment::get().getWorld()->getInterior(cellID); @@ -485,9 +516,8 @@ namespace MWScript catch(std::exception&) { const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(x, y); + store = MWBase::Environment::get().getWorld()->getExterior(cellIndex.x(), cellIndex.y()); if(!cell) { runtime.getContext().report ("unknown cell (" + cellID + ")"); @@ -503,6 +533,7 @@ namespace MWScript pos.rot[0] = pos.rot[1] = 0; pos.rot[2] = osg::DegreesToRadians(zRotDegrees); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); ref.getPtr().getCellRef().setPosition(pos); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); placed.getClass().adjustPosition(placed, true); @@ -516,7 +547,7 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view itemID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; @@ -536,9 +567,8 @@ namespace MWScript MWWorld::CellStore* store = nullptr; if (player.getCell()->isExterior()) { - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + const osg::Vec2i cellIndex = MWWorld::positionToCellIndex(x, y); + store = MWBase::Environment::get().getWorld()->getExterior(cellIndex.x(), cellIndex.y()); } else store = player.getCell(); @@ -550,6 +580,7 @@ namespace MWScript pos.rot[0] = pos.rot[1] = 0; pos.rot[2] = osg::DegreesToRadians(zRotDegrees); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); ref.getPtr().getCellRef().setPosition(pos); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); placed.getClass().adjustPosition(placed, true); @@ -567,7 +598,7 @@ namespace MWScript ? MWMechanics::getPlayer() : R()(runtime); - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view itemID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; @@ -590,6 +621,7 @@ namespace MWScript { // create item MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), itemID, 1); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), actor, actor.getCell(), direction, distance); MWBase::Environment::get().getWorld()->scaleObject(ptr, actor.getCellRef().getScale()); @@ -606,21 +638,20 @@ namespace MWScript { const MWWorld::Ptr& ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - float az = ptr.getRefData().getPosition().rot[2]; - - if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax+rotation,ay,az); + auto rot = ptr.getRefData().getPosition().asRotationVec3(); + // Regardless of the axis argument, the player may only be rotated on Z + if (axis == "z" || MWMechanics::getPlayer() == ptr) + rot.z() += rotation; + else if (axis == "x") + rot.x() += rotation; else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay+rotation,az); - else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,az+rotation); + rot.y() += rotation; + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); } }; @@ -633,7 +664,7 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); @@ -672,15 +703,10 @@ namespace MWScript if (!ptr.isInCell()) return; - float xr = ptr.getCellRef().getPosition().rot[0]; - float yr = ptr.getCellRef().getPosition().rot[1]; - float zr = ptr.getCellRef().getPosition().rot[2]; - - MWBase::Environment::get().getWorld()->rotateObject(ptr, xr, yr, zr); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3()); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3())); } }; @@ -697,7 +723,7 @@ namespace MWScript if (!ptr.isInCell()) return; - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); @@ -723,14 +749,12 @@ namespace MWScript return; osg::Vec3f diff = ptr.getRefData().getBaseNode()->getAttitude() * posChange; - osg::Vec3f worldPos(ptr.getRefData().getPosition().asVec3()); - worldPos += diff; // We should move actors, standing on moving object, too. // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, worldPos.x(), worldPos.y(), worldPos.z())); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -746,20 +770,19 @@ namespace MWScript if (!ptr.isInCell()) return; - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - const float *objPos = ptr.getRefData().getPosition().pos; osg::Vec3f diff; if (axis == "x") - diff.x() += movement; + diff.x() = movement; else if (axis == "y") - diff.y() += movement; + diff.y() = movement; else if (axis == "z") - diff.z() += movement; + diff.z() = movement; else return; @@ -767,7 +790,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+diff.x(), objPos[1]+diff.y(), objPos[2]+diff.z())); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -793,47 +816,47 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5(Compiler::Transformation::opcodeGetDistance, new OpGetDistance); - interpreter.installSegment5(Compiler::Transformation::opcodeGetDistanceExplicit, new OpGetDistance); - interpreter.installSegment5(Compiler::Transformation::opcodeSetScale,new OpSetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeSetScaleExplicit,new OpSetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAngle,new OpSetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAngleExplicit,new OpSetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetScale,new OpGetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeGetScaleExplicit,new OpGetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeGetAngle,new OpGetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetAngleExplicit,new OpGetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetPos,new OpGetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetPosExplicit,new OpGetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeSetPos,new OpSetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeSetPosExplicit,new OpSetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPos,new OpGetStartingPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPosExplicit,new OpGetStartingPos); - interpreter.installSegment5(Compiler::Transformation::opcodePosition,new OpPosition); - interpreter.installSegment5(Compiler::Transformation::opcodePositionExplicit,new OpPosition); - interpreter.installSegment5(Compiler::Transformation::opcodePositionCell,new OpPositionCell); - interpreter.installSegment5(Compiler::Transformation::opcodePositionCellExplicit,new OpPositionCell); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell,new OpPlaceItemCell); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem,new OpPlaceItem); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtPc,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMe,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMeExplicit,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodeModScale,new OpModScale); - interpreter.installSegment5(Compiler::Transformation::opcodeModScaleExplicit,new OpModScale); - interpreter.installSegment5(Compiler::Transformation::opcodeRotate,new OpRotate); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateExplicit,new OpRotate); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorld,new OpRotateWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorldExplicit,new OpRotateWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStart,new OpSetAtStart); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStartExplicit,new OpSetAtStart); - interpreter.installSegment5(Compiler::Transformation::opcodeMove,new OpMove); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveExplicit,new OpMove); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorld,new OpMoveWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorldExplicit,new OpMoveWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngle, new OpGetStartingAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngleExplicit, new OpGetStartingAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeResetActors, new OpResetActors); - interpreter.installSegment5(Compiler::Transformation::opcodeFixme, new OpFixme); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetDistance); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetDistanceExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAngle); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAngleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetAngle); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetAngleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetPos); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetPos); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetStartingPos); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetStartingPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodePosition); + interpreter.installSegment5>(Compiler::Transformation::opcodePositionExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodePositionCell); + interpreter.installSegment5>(Compiler::Transformation::opcodePositionCellExplicit); + interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell); + interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem); + interpreter.installSegment5>(Compiler::Transformation::opcodePlaceAtPc); + interpreter.installSegment5>(Compiler::Transformation::opcodePlaceAtMe); + interpreter.installSegment5>(Compiler::Transformation::opcodePlaceAtMeExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeModScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeModScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotate); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotateExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotateWorld); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotateWorldExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAtStart); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAtStartExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeMove); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveWorld); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveWorldExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetStartingAngle); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetStartingAngleExplicit); + interpreter.installSegment5(Compiler::Transformation::opcodeResetActors); + interpreter.installSegment5(Compiler::Transformation::opcodeFixme); } } } diff --git a/apps/openmw/mwscript/userextensions.cpp b/apps/openmw/mwscript/userextensions.cpp index 3f443304d7..f929425a9c 100644 --- a/apps/openmw/mwscript/userextensions.cpp +++ b/apps/openmw/mwscript/userextensions.cpp @@ -66,12 +66,12 @@ namespace MWScript void installOpcodes (Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::User::opcodeUser1, new OpUser1); - interpreter.installSegment5 (Compiler::User::opcodeUser2, new OpUser2); - interpreter.installSegment5 (Compiler::User::opcodeUser3, new OpUser3); - interpreter.installSegment5 (Compiler::User::opcodeUser3Explicit, new OpUser3); - interpreter.installSegment5 (Compiler::User::opcodeUser4, new OpUser4); - interpreter.installSegment5 (Compiler::User::opcodeUser4Explicit, new OpUser4); + interpreter.installSegment5(Compiler::User::opcodeUser1); + interpreter.installSegment5(Compiler::User::opcodeUser2); + interpreter.installSegment5>(Compiler::User::opcodeUser3); + interpreter.installSegment5>(Compiler::User::opcodeUser3Explicit); + interpreter.installSegment5>(Compiler::User::opcodeUser4); + interpreter.installSegment5>(Compiler::User::opcodeUser4Explicit); } } } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index 6c334978c0..997b4e30c6 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -18,11 +18,14 @@ int FFmpeg_Decoder::readPacket(void *user_data, uint8_t *buf, int buf_size) std::istream& stream = *static_cast(user_data)->mDataStream; stream.clear(); stream.read((char*)buf, buf_size); - return stream.gcount(); + std::streamsize count = stream.gcount(); + if (count == 0) + return AVERROR_EOF; + return count; } catch (std::exception& ) { - return 0; + return AVERROR_UNKNOWN; } } @@ -221,7 +224,7 @@ void FFmpeg_Decoder::open(const std::string &fname) if(!mStream) throw std::runtime_error("No audio streams in "+fname); - AVCodec *codec = avcodec_find_decoder((*mStream)->codecpar->codec_id); + const AVCodec *codec = avcodec_find_decoder((*mStream)->codecpar->codec_id); if(!codec) { std::string ss = "No codec found for id " + @@ -287,9 +290,9 @@ void FFmpeg_Decoder::close() mStream = nullptr; av_packet_unref(&mPacket); - av_freep(&mFrame); - swr_free(&mSwr); av_freep(&mDataBuf); + av_frame_free(&mFrame); + swr_free(&mSwr); if(mFormatCtx) { @@ -302,11 +305,13 @@ void FFmpeg_Decoder::close() // if (mFormatCtx->pb->buffer != nullptr) { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = nullptr; + av_freep(&mFormatCtx->pb->buffer); } - av_free(mFormatCtx->pb); - mFormatCtx->pb = nullptr; +#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); } @@ -437,7 +442,7 @@ FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) , mFrameSize(0) , mFramePos(0) , mNextPts(0.0) - , mSwr(0) + , mSwr(nullptr) , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) , mOutputChannelLayout(0) , mDataBuf(nullptr) diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index 92d046d85f..c51639a972 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -1,7 +1,13 @@ #ifndef GAME_SOUND_FFMPEG_DECODER_H #define GAME_SOUND_FFMPEG_DECODER_H -#include +#include + +#if defined(_MSC_VER) + #pragma warning (push) + #pragma warning (disable : 4244) +#endif + extern "C" { #include @@ -14,10 +20,13 @@ extern "C" #include } -#include +#if defined(_MSC_VER) + #pragma warning (pop) +#endif + +#include #include -#include #include "sound_decoder.hpp" @@ -69,15 +78,13 @@ namespace MWSound FFmpeg_Decoder& operator=(const FFmpeg_Decoder &rhs); FFmpeg_Decoder(const FFmpeg_Decoder &rhs); - FFmpeg_Decoder(const VFS::Manager* vfs); public: + explicit FFmpeg_Decoder(const VFS::Manager* vfs); + virtual ~FFmpeg_Decoder(); friend class SoundManager; }; -#ifndef DEFAULT_DECODER -#define DEFAULT_DECODER (::MWSound::FFmpeg_Decoder) -#endif } #endif diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index ae31d60949..ac44d1b40e 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -1,6 +1,6 @@ #include "loudness.hpp" -#include +#include #include #include @@ -40,7 +40,7 @@ void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) else if (mSampleType == SampleType_Float32) { value = *reinterpret_cast(&mQueue[sample*advance]); - value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already. } sum += value*value; @@ -64,8 +64,7 @@ float Sound_Loudness::getLoudnessAtTime(float sec) const if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; - size_t index = static_cast(sec * mSamplesPerSec); - index = std::max(0, std::min(index, mSamples.size()-1)); + size_t index = std::clamp(sec * mSamplesPerSec, 0, mSamples.size() - 1); return mSamples[index]; } diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp index d8c1c928ee..aef8f7fe93 100644 --- a/apps/openmw/mwsound/movieaudiofactory.cpp +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -38,8 +38,8 @@ namespace MWSound public: MovieAudioDecoder(Video::VideoState *videoState) : Video::MovieAudioDecoder(videoState), mAudioTrack(nullptr) + , mDecoderBridge(std::make_shared(this)) { - mDecoderBridge.reset(new MWSoundDecoderBridge(this)); } size_t getSampleOffset() @@ -153,9 +153,9 @@ namespace MWSound - std::shared_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) + std::unique_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) { - std::shared_ptr decoder(new MWSound::MovieAudioDecoder(videoState)); + auto decoder = std::make_unique(videoState); decoder->setupFormat(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); diff --git a/apps/openmw/mwsound/movieaudiofactory.hpp b/apps/openmw/mwsound/movieaudiofactory.hpp index 63b8fd7e90..0af1066af5 100644 --- a/apps/openmw/mwsound/movieaudiofactory.hpp +++ b/apps/openmw/mwsound/movieaudiofactory.hpp @@ -8,7 +8,7 @@ namespace MWSound class MovieAudioFactory : public Video::MovieAudioFactory { - std::shared_ptr createDecoder(Video::VideoState* videoState) override; + std::unique_ptr createDecoder(Video::VideoState* videoState) override; }; } diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index a7be5a743f..47f90bbcf3 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -6,13 +6,13 @@ #include #include #include -#include #include -#include +#include #include #include +#include #include #include "openal_output.hpp" @@ -428,7 +428,7 @@ bool OpenAL_SoundStream::init(bool getLoudnessData) mBufferSize *= mFrameSize; if (getLoudnessData) - mLoudnessAnalyzer.reset(new Sound_Loudness(sLoudnessFPS, mSampleRate, chans, type)); + mLoudnessAnalyzer = std::make_unique(sLoudnessFPS, mSampleRate, chans, type); mIsFinished = false; return true; @@ -624,7 +624,7 @@ bool OpenAL_Output::init(const std::string &devname, const std::string &hrtfname attrs.reserve(15); if(ALC.SOFT_HRTF) { - LPALCGETSTRINGISOFT alcGetStringiSOFT = 0; + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); attrs.push_back(ALC_HRTF_SOFT); @@ -850,13 +850,13 @@ void OpenAL_Output::deinit() alDeleteFilters(1, &mWaterFilter); mWaterFilter = 0; - alcMakeContextCurrent(0); + alcMakeContextCurrent(nullptr); if(mContext) alcDestroyContext(mContext); - mContext = 0; + mContext = nullptr; if(mDevice) alcCloseDevice(mDevice); - mDevice = 0; + mDevice = nullptr; mInitialized = false; } @@ -869,7 +869,7 @@ std::vector OpenAL_Output::enumerateHrtf() if(!mDevice || !ALC.SOFT_HRTF) return ret; - LPALCGETSTRINGISOFT alcGetStringiSOFT = 0; + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); ALCint num_hrtf; @@ -892,10 +892,10 @@ void OpenAL_Output::setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) return; } - LPALCGETSTRINGISOFT alcGetStringiSOFT = 0; + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); - LPALCRESETDEVICESOFT alcResetDeviceSOFT = 0; + LPALCRESETDEVICESOFT alcResetDeviceSOFT = nullptr; getALCFunc(alcResetDeviceSOFT, mDevice, "alcResetDeviceSOFT"); std::vector attrs; @@ -954,17 +954,7 @@ std::pair OpenAL_Output::loadSound(const std::string &fname try { DecoderPtr decoder = mManager.getDecoder(); - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(decoder->mResourceMgr->exists(fname)) - decoder->open(fname); - else - { - std::string file = fname; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); - } + decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, decoder->mResourceMgr)); ChannelConfig chans; SampleType type; @@ -1109,13 +1099,8 @@ void OpenAL_Output::initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat m alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } -void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d) +void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv) { - if(is3d) - { - if((pos - mListenerPos).length2() > maxdist*maxdist) - gain = 0.0f; - } if(useenv && mListenerEnv == Env_Underwater && !mWaterFilter) { gain *= 0.9f; @@ -1141,7 +1126,7 @@ bool OpenAL_Output::playSound(Sound *sound, Sound_Handle data, float offset) } source = mFreeSources.front(); - initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), + initCommon2D(source, sound->getPosition(), sound->getRealVolume(), getTimeScaledPitch(sound), sound->getIsLooping(), sound->getUseEnv()); alSourcei(source, AL_BUFFER, GET_PTRID(data)); alSourcef(source, AL_SEC_OFFSET, offset); @@ -1181,7 +1166,7 @@ bool OpenAL_Output::playSound3D(Sound *sound, Sound_Handle data, float offset) source = mFreeSources.front(); initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), - sound->getRealVolume(), sound->getPitch(), sound->getIsLooping(), + sound->getRealVolume(), getTimeScaledPitch(sound), sound->getIsLooping(), sound->getUseEnv()); alSourcei(source, AL_BUFFER, GET_PTRID(data)); alSourcef(source, AL_SEC_OFFSET, offset); @@ -1213,7 +1198,7 @@ void OpenAL_Output::finishSound(Sound *sound) { if(!sound->mHandle) return; ALuint source = GET_PTRID(sound->mHandle); - sound->mHandle = 0; + sound->mHandle = nullptr; // Rewind the stream to put the source back into an AL_INITIAL state, for // the next time it's used. @@ -1243,7 +1228,7 @@ void OpenAL_Output::updateSound(Sound *sound) ALuint source = GET_PTRID(sound->mHandle); updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); + getTimeScaledPitch(sound), sound->getUseEnv()); getALError(); } @@ -1260,7 +1245,7 @@ bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream *sound, bool getLoudn if(sound->getIsLooping()) Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; - initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), + initCommon2D(source, sound->getPosition(), sound->getRealVolume(), getTimeScaledPitch(sound), false, sound->getUseEnv()); if(getALError() != AL_NO_ERROR) return false; @@ -1292,7 +1277,7 @@ bool OpenAL_Output::streamSound3D(DecoderPtr decoder, Stream *sound, bool getLou Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), - sound->getRealVolume(), sound->getPitch(), false, sound->getUseEnv()); + sound->getRealVolume(), getTimeScaledPitch(sound), false, sound->getUseEnv()); if(getALError() != AL_NO_ERROR) return false; @@ -1316,7 +1301,7 @@ void OpenAL_Output::finishStream(Stream *sound) OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); ALuint source = stream->mSource; - sound->mHandle = 0; + sound->mHandle = nullptr; mStreamThread->remove(stream); // Rewind the stream to put the source back into an AL_INITIAL state, for @@ -1369,7 +1354,7 @@ void OpenAL_Output::updateStream(Stream *sound) ALuint source = stream->mSource; updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); + getTimeScaledPitch(sound), sound->getUseEnv()); getALError(); } @@ -1462,7 +1447,7 @@ void OpenAL_Output::pauseActiveDevice() if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { - LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = 0; + LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = nullptr; getALCFunc(alcDevicePauseSOFT, mDevice, "alcDevicePauseSOFT"); alcDevicePauseSOFT(mDevice); getALCError(mDevice); @@ -1478,7 +1463,7 @@ void OpenAL_Output::resumeActiveDevice() if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { - LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = 0; + LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = nullptr; getALCFunc(alcDeviceResumeSOFT, mDevice, "alcDeviceResumeSOFT"); alcDeviceResumeSOFT(mDevice); getALCError(mDevice); @@ -1513,10 +1498,10 @@ void OpenAL_Output::resumeSounds(int types) OpenAL_Output::OpenAL_Output(SoundManager &mgr) : Sound_Output(mgr) - , mDevice(0), mContext(0) + , mDevice(nullptr), mContext(nullptr) , mListenerPos(0.0f, 0.0f, 0.0f), mListenerEnv(Env_Normal) , mWaterFilter(0), mWaterEffect(0), mDefaultEffect(0), mEffectSlot(0) - , mStreamThread(new StreamThread) + , mStreamThread(std::make_unique()) { } @@ -1525,4 +1510,10 @@ OpenAL_Output::~OpenAL_Output() OpenAL_Output::deinit(); } +float OpenAL_Output::getTimeScaledPitch(SoundBase *sound) +{ + const bool shouldScale = !(sound->mParams.mFlags & PlayMode::NoScaling); + return shouldScale ? sound->getPitch() * mManager.getSimulationTimeScale() : sound->getPitch(); +} + } diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index d9ca924a78..c68c65c165 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -15,6 +15,7 @@ namespace MWSound { class SoundManager; + class SoundBase; class Sound; class Stream; @@ -53,7 +54,9 @@ namespace MWSound void initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv); void initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv); - void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d); + void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv); + + float getTimeScaledPitch(SoundBase *sound); OpenAL_Output& operator=(const OpenAL_Output &rhs); OpenAL_Output(const OpenAL_Output &rhs); @@ -98,9 +101,6 @@ namespace MWSound OpenAL_Output(SoundManager &mgr); virtual ~OpenAL_Output(); }; -#ifndef DEFAULT_OUTPUT -#define DEFAULT_OUTPUT(x) ::MWSound::OpenAL_Output((x)) -#endif } #endif diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index 9d264e1b69..2a07f05779 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -7,6 +7,17 @@ namespace MWSound { + // Extra play flags, not intended for caller use + enum PlayModeEx + { + Play_2D = 0, + Play_StopAtFadeEnd = 1 << 28, + Play_FadeExponential = 1 << 29, + Play_InFade = 1 << 30, + Play_3D = 1 << 31, + Play_FadeFlagsMask = (Play_StopAtFadeEnd | Play_FadeExponential), + }; + // For testing individual PlayMode flags inline int operator&(int a, PlayMode b) { return a & static_cast(b); } inline int operator&(PlayMode a, PlayMode b) { return static_cast(a) & static_cast(b); } @@ -14,13 +25,15 @@ namespace MWSound struct SoundParams { osg::Vec3f mPos; - float mVolume = 1; - float mBaseVolume = 1; - float mPitch = 1; - float mMinDistance = 1; - float mMaxDistance = 1000; + float mVolume = 1.0f; + float mBaseVolume = 1.0f; + float mPitch = 1.0f; + float mMinDistance = 1.0f; + float mMaxDistance = 1000.0f; int mFlags = 0; - float mFadeOutTime = 0; + float mFadeVolume = 1.0f; + float mFadeTarget = 0.0f; + float mFadeStep = 0.0f; }; class SoundBase { @@ -39,19 +52,97 @@ namespace MWSound void setPosition(const osg::Vec3f &pos) { mParams.mPos = pos; } void setVolume(float volume) { mParams.mVolume = volume; } void setBaseVolume(float volume) { mParams.mBaseVolume = volume; } - void setFadeout(float duration) { mParams.mFadeOutTime = duration; } - void updateFade(float duration) + void setFadeout(float duration) { setFade(duration, 0.0, Play_StopAtFadeEnd); } + + /// Fade to the given linear gain within the specified amount of time. + /// Note that the fade gain is independent of the sound volume. + /// + /// \param duration specifies the duration of the fade. For *linear* + /// fades (default) this will be exactly the time at which the desired + /// volume is reached. Let v0 be the initial volume, v1 be the target + /// volume, and t0 be the initial time. Then the volume over time is + /// given as + /// + /// v(t) = v0 + (v1 - v0) * (t - t0) / duration if t <= t0 + duration + /// v(t) = v1 if t > t0 + duration + /// + /// For *exponential* fades this determines the time-constant of the + /// exponential process describing the fade. In particular, we guarantee + /// that we reach v0 + 0.99 * (v1 - v0) within the given duration. + /// + /// v(t) = v1 + (v0 - v1) * exp(-4.6 * (t0 - t) / duration) + /// + /// where -4.6 is approximately log(1%) (i.e., -40 dB). + /// + /// This interpolation mode is meant for environmental sound effects to + /// achieve less jarring transitions. + /// + /// \param targetVolume is the linear gain that should be reached at + /// the end of the fade. + /// + /// \param flags may be a combination of Play_FadeExponential and + /// Play_StopAtFadeEnd. If Play_StopAtFadeEnd is set, stops the sound + /// once the fade duration has passed or the target volume has been + /// reached. If Play_FadeExponential is set, enables the exponential + /// fade mode (see above). + void setFade(float duration, float targetVolume, int flags = 0) { + // Approximation of log(1%) (i.e., -40 dB). + constexpr float minus40Decibel = -4.6f; + + // Do nothing if already at the target, unless we need to trigger a stop event + if ((mParams.mFadeVolume == targetVolume) && !(flags & Play_StopAtFadeEnd)) + return; + + mParams.mFadeTarget = targetVolume; + mParams.mFlags = (mParams.mFlags & ~Play_FadeFlagsMask) | (flags & Play_FadeFlagsMask) | Play_InFade; + if (duration > 0.0f) + { + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeStep = -minus40Decibel / duration; + else + mParams.mFadeStep = (mParams.mFadeTarget - mParams.mFadeVolume) / duration; + } + else + { + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFadeStep = 0.0f; + } + } + + /// Updates the internal fading logic. + /// + /// \param dt is the time in seconds since the last call to update. + /// + /// \return true if the sound is still active, false if the sound has + /// reached a fading destination that was marked with Play_StopAtFadeEnd. + bool updateFade(float dt) { - if (mParams.mFadeOutTime > 0.0f) + // Mark fade as done at this volume difference (-80dB when fading to zero) + constexpr float minVolumeDifference = 1e-4f; + + if (!getInFade()) + return true; + + // Perform the actual fade operation + const float deltaBefore = mParams.mFadeTarget - mParams.mFadeVolume; + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeVolume += mParams.mFadeStep * deltaBefore * dt; + else + mParams.mFadeVolume += mParams.mFadeStep * dt; + const float deltaAfter = mParams.mFadeTarget - mParams.mFadeVolume; + + // Abort fade if we overshot or reached the minimum difference + if ((std::signbit(deltaBefore) != std::signbit(deltaAfter)) || (std::abs(deltaAfter) < minVolumeDifference)) { - float soundDuration = std::min(duration, mParams.mFadeOutTime); - mParams.mVolume *= (mParams.mFadeOutTime - soundDuration) / mParams.mFadeOutTime; - mParams.mFadeOutTime -= soundDuration; + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFlags &= ~Play_InFade; } + + return getInFade() || !(mParams.mFlags & Play_StopAtFadeEnd); } const osg::Vec3f &getPosition() const { return mParams.mPos; } - float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume; } + float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume * mParams.mFadeVolume; } float getPitch() const { return mParams.mPitch; } float getMinDistance() const { return mParams.mMinDistance; } float getMaxDistance() const { return mParams.mMaxDistance; } @@ -62,6 +153,7 @@ namespace MWSound bool getIsLooping() const { return mParams.mFlags & MWSound::PlayMode::Loop; } bool getDistanceCull() const { return mParams.mFlags & MWSound::PlayMode::RemoveAtDistance; } bool getIs3D() const { return mParams.mFlags & Play_3D; } + bool getInFade() const { return mParams.mFlags & Play_InFade; } void init(const SoundParams& params) { @@ -78,7 +170,7 @@ namespace MWSound Sound(Sound&&) = delete; public: - Sound() { } + Sound() = default; }; class Stream : public SoundBase { @@ -87,7 +179,7 @@ namespace MWSound Stream(Stream&&) = delete; public: - Stream() { } + Stream() = default; }; } diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp new file mode 100644 index 0000000000..e64e89d775 --- /dev/null +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -0,0 +1,152 @@ +#include "sound_buffer.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" + +#include +#include +#include + +#include +#include + +namespace MWSound +{ + namespace + { + struct AudioParams + { + float mAudioDefaultMinDistance; + float mAudioDefaultMaxDistance; + float mAudioMinDistanceMult; + float mAudioMaxDistanceMult; + }; + + AudioParams makeAudioParams(const MWBase::World& world) + { + const auto& settings = world.getStore().get(); + AudioParams params; + params.mAudioDefaultMinDistance = settings.find("fAudioDefaultMinDistance")->mValue.getFloat(); + params.mAudioDefaultMaxDistance = settings.find("fAudioDefaultMaxDistance")->mValue.getFloat(); + params.mAudioMinDistanceMult = settings.find("fAudioMinDistanceMult")->mValue.getFloat(); + params.mAudioMaxDistanceMult = settings.find("fAudioMaxDistanceMult")->mValue.getFloat(); + return params; + } + } + + SoundBufferPool::SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output) : + mVfs(&vfs), + mOutput(&output), + mBufferCacheMax(std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1) * 1024 * 1024), + mBufferCacheMin(std::min(static_cast(std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1)) * 1024 * 1024, mBufferCacheMax)) + { + } + + SoundBufferPool::~SoundBufferPool() + { + clear(); + } + + Sound_Buffer* SoundBufferPool::lookup(const std::string& soundId) const + { + const auto it = mBufferNameMap.find(soundId); + if (it != mBufferNameMap.end()) + { + Sound_Buffer* sfx = it->second; + if (sfx->getHandle() != nullptr) + return sfx; + } + return nullptr; + } + + Sound_Buffer* SoundBufferPool::load(const std::string& soundId) + { + if (mBufferNameMap.empty()) + { + for (const ESM::Sound& sound : MWBase::Environment::get().getWorld()->getStore().get()) + insertSound(Misc::StringUtils::lowerCase(sound.mId), sound); + } + + Sound_Buffer* sfx; + const auto it = mBufferNameMap.find(soundId); + if (it != mBufferNameMap.end()) + sfx = it->second; + else + { + const ESM::Sound *sound = MWBase::Environment::get().getWorld()->getStore().get().search(soundId); + if (sound == nullptr) + return {}; + sfx = insertSound(soundId, *sound); + } + + if (sfx->getHandle() == nullptr) + { + auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); + if (handle == nullptr) + return {}; + + sfx->mHandle = handle; + + mBufferCacheSize += size; + if (mBufferCacheSize > mBufferCacheMax) + { + unloadUnused(); + if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) + Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; + } + mUnusedBuffers.push_front(sfx); + } + + return sfx; + } + + void SoundBufferPool::clear() + { + for (auto &sfx : mSoundBuffers) + { + if(sfx.mHandle) + mOutput->unloadSound(sfx.mHandle); + sfx.mHandle = nullptr; + } + mUnusedBuffers.clear(); + } + + Sound_Buffer* SoundBufferPool::insertSound(const std::string& soundId, const ESM::Sound& sound) + { + static const AudioParams audioParams = makeAudioParams(*MWBase::Environment::get().getWorld()); + + float volume = static_cast(std::pow(10.0, (sound.mData.mVolume / 255.0 * 3348.0 - 3348.0) / 2000.0)); + float min = sound.mData.mMinRange; + float max = sound.mData.mMaxRange; + if (min == 0 && max == 0) + { + min = audioParams.mAudioDefaultMinDistance; + max = audioParams.mAudioDefaultMaxDistance; + } + + min *= audioParams.mAudioMinDistanceMult; + max *= audioParams.mAudioMaxDistanceMult; + min = std::max(min, 1.0f); + max = std::max(min, max); + + Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); + sfx.mResourceName = mVfs->normalizeFilename(sfx.mResourceName); + + mBufferNameMap.emplace(soundId, &sfx); + return &sfx; + } + + void SoundBufferPool::unloadUnused() + { + while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) + { + Sound_Buffer* const unused = mUnusedBuffers.back(); + + mBufferCacheSize -= mOutput->unloadSound(unused->getHandle()); + unused->mHandle = nullptr; + + mUnusedBuffers.pop_back(); + } + } +} diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/sound_buffer.hpp index 5ca3a45da7..5c45ac08aa 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/sound_buffer.hpp @@ -1,27 +1,105 @@ #ifndef GAME_SOUND_SOUND_BUFFER_H #define GAME_SOUND_SOUND_BUFFER_H +#include #include +#include +#include #include "sound_output.hpp" +namespace ESM +{ + struct Sound; +} + +namespace VFS +{ + class Manager; +} + namespace MWSound { + class SoundBufferPool; + class Sound_Buffer { - public: - std::string mResourceName; + public: + template + Sound_Buffer(T&& resname, float volume, float mindist, float maxdist) + : mResourceName(std::forward(resname)), mVolume(volume), mMinDist(mindist), mMaxDist(maxdist) + {} + + const std::string& getResourceName() const noexcept { return mResourceName; } + + Sound_Handle getHandle() const noexcept { return mHandle; } + + float getVolume() const noexcept { return mVolume; } + + float getMinDist() const noexcept { return mMinDist; } + + float getMaxDist() const noexcept { return mMaxDist; } + + private: + std::string mResourceName; + float mVolume; + float mMinDist; + float mMaxDist; + Sound_Handle mHandle = nullptr; + std::size_t mUses = 0; + + friend class SoundBufferPool; + }; + + class SoundBufferPool + { + public: + SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output); + + SoundBufferPool(const SoundBufferPool&) = delete; + + ~SoundBufferPool(); + + /// Lookup a soundId for its sound data (resource name, local volume, + /// minRange, and maxRange) + Sound_Buffer* lookup(const std::string& soundId) const; + + /// Lookup a soundId for its sound data (resource name, local volume, + /// minRange, and maxRange), and ensure it's ready for use. + Sound_Buffer* load(const std::string& soundId); + + void use(Sound_Buffer& sfx) + { + if (sfx.mUses++ == 0) + { + const auto it = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), &sfx); + if (it != mUnusedBuffers.end()) + mUnusedBuffers.erase(it); + } + } + + void release(Sound_Buffer& sfx) + { + if (--sfx.mUses == 0) + mUnusedBuffers.push_front(&sfx); + } - float mVolume; - float mMinDist, mMaxDist; + void clear(); - Sound_Handle mHandle; + private: + const VFS::Manager* const mVfs; + Sound_Output* mOutput; + std::deque mSoundBuffers; + std::unordered_map mBufferNameMap; + std::size_t mBufferCacheMax; + std::size_t mBufferCacheMin; + std::size_t mBufferCacheSize = 0; + // NOTE: unused buffers are stored in front-newest order. + std::deque mUnusedBuffers; - size_t mUses; + inline Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound& sound); - Sound_Buffer(std::string resname, float volume, float mindist, float maxdist) - : mResourceName(resname), mVolume(volume), mMinDist(mindist), mMaxDist(maxdist), mHandle(0), mUses(0) - { } + inline void unloadUnused(); }; } diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index 4075e36ccd..9ec8b17dc9 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -5,7 +5,7 @@ #include #include -#include "soundmanagerimp.hpp" +#include "../mwbase/soundmanager.hpp" namespace MWSound { @@ -25,6 +25,12 @@ namespace MWSound Auto }; + enum Environment + { + Env_Normal, + Env_Underwater + }; + class Sound_Output { SoundManager &mManager; @@ -81,6 +87,7 @@ namespace MWSound friend class OpenAL_Output; friend class SoundManager; + friend class SoundBufferPool; }; } diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 1a90dceedf..3ee76326f4 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -3,9 +3,11 @@ #include #include #include +#include #include +#include #include #include #include @@ -33,6 +35,8 @@ namespace MWSound namespace { constexpr float sMinUpdateInterval = 1.0f / 30.0f; + constexpr float sSfxFadeInDuration = 1.0f; + constexpr float sSfxFadeOutDuration = 1.0f; WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings() { @@ -47,6 +51,24 @@ namespace MWSound return settings; } + + float initialFadeVolume(float squaredDist, Sound_Buffer *sfx, Type type, PlayMode mode) + { + // If a sound is farther away than its maximum distance, start playing it with a zero fade volume. + // It can still become audible once the player moves closer. + const float maxDist = sfx->getMaxDist(); + if (squaredDist > (maxDist * maxDist)) + return 0.0f; + + // This is a *heuristic* that causes environment sounds to fade in. The idea is the following: + // - Only looped sounds playing through the effects channel are environment sounds + // - Do not fade in sounds if the player is already so close that the sound plays at maximum volume + const float minDist = sfx->getMinDist(); + if ((squaredDist > (minDist * minDist)) && (type == Type::Sfx) && (mode & PlayMode::Loop)) + return 0.0f; + + return 1.0; + } } // For combining PlayMode and Type flags @@ -54,10 +76,9 @@ namespace MWSound SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound) : mVFS(vfs) - , mOutput(new DEFAULT_OUTPUT(*this)) + , mOutput(new OpenAL_Output(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) - , mSoundBuffers(new SoundBufferList::element_type()) - , mBufferCacheSize(0) + , mSoundBuffers(*vfs, *mOutput) , mListenerUnderwater(false) , mListenerPos(0,0,0) , mListenerDir(1,0,0) @@ -69,11 +90,6 @@ namespace MWSound , mLastCell(nullptr) , mCurrentRegionSound(nullptr) { - mBufferCacheMin = std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1); - mBufferCacheMax = std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1); - mBufferCacheMax *= 1024*1024; - mBufferCacheMin = std::min(mBufferCacheMin*1024*1024, mBufferCacheMax); - if(!useSound) { Log(Debug::Info) << "Sound disabled."; @@ -115,129 +131,15 @@ namespace MWSound SoundManager::~SoundManager() { - clear(); - for(Sound_Buffer &sfx : *mSoundBuffers) - { - if(sfx.mHandle) - mOutput->unloadSound(sfx.mHandle); - sfx.mHandle = 0; - } - mUnusedBuffers.clear(); + SoundManager::clear(); + mSoundBuffers.clear(); mOutput.reset(); } // Return a new decoder instance, used as needed by the output implementations DecoderPtr SoundManager::getDecoder() { - return DecoderPtr(new DEFAULT_DECODER (mVFS)); - } - - Sound_Buffer *SoundManager::insertSound(const std::string &soundId, const ESM::Sound *sound) - { - MWBase::World* world = MWBase::Environment::get().getWorld(); - static const float fAudioDefaultMinDistance = world->getStore().get().find("fAudioDefaultMinDistance")->mValue.getFloat(); - static const float fAudioDefaultMaxDistance = world->getStore().get().find("fAudioDefaultMaxDistance")->mValue.getFloat(); - static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->mValue.getFloat(); - static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->mValue.getFloat(); - float volume, min, max; - - volume = static_cast(pow(10.0, (sound->mData.mVolume / 255.0*3348.0 - 3348.0) / 2000.0)); - min = sound->mData.mMinRange; - max = sound->mData.mMaxRange; - if (min == 0 && max == 0) - { - min = fAudioDefaultMinDistance; - max = fAudioDefaultMaxDistance; - } - - min *= fAudioMinDistanceMult; - max *= fAudioMaxDistanceMult; - min = std::max(min, 1.0f); - max = std::max(min, max); - - Sound_Buffer *sfx = &*mSoundBuffers->insert(mSoundBuffers->end(), - Sound_Buffer("Sound/"+sound->mSound, volume, min, max) - ); - mVFS->normalizeFilename(sfx->mResourceName); - - mBufferNameMap.insert(std::make_pair(soundId, sfx)); - - return sfx; - } - - // Lookup a soundId for its sound data (resource name, local volume, - // minRange, and maxRange) - Sound_Buffer *SoundManager::lookupSound(const std::string &soundId) const - { - NameBufferMap::const_iterator snd = mBufferNameMap.find(soundId); - if(snd != mBufferNameMap.end()) - { - Sound_Buffer *sfx = snd->second; - if(sfx->mHandle) return sfx; - } - return nullptr; - } - - // Lookup a soundId for its sound data (resource name, local volume, - // minRange, and maxRange), and ensure it's ready for use. - Sound_Buffer *SoundManager::loadSound(const std::string &soundId) - { -#ifdef __GNUC__ -#define LIKELY(x) __builtin_expect((bool)(x), true) -#define UNLIKELY(x) __builtin_expect((bool)(x), false) -#else -#define LIKELY(x) (bool)(x) -#define UNLIKELY(x) (bool)(x) -#endif - if(UNLIKELY(mBufferNameMap.empty())) - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - for(const ESM::Sound &sound : world->getStore().get()) - insertSound(Misc::StringUtils::lowerCase(sound.mId), &sound); - } - - Sound_Buffer *sfx; - NameBufferMap::const_iterator snd = mBufferNameMap.find(soundId); - if(LIKELY(snd != mBufferNameMap.end())) - sfx = snd->second; - else - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const ESM::Sound *sound = world->getStore().get().search(soundId); - if(!sound) return nullptr; - sfx = insertSound(soundId, sound); - } -#undef LIKELY -#undef UNLIKELY - - if(!sfx->mHandle) - { - size_t size; - std::tie(sfx->mHandle, size) = mOutput->loadSound(sfx->mResourceName); - if(!sfx->mHandle) return nullptr; - - mBufferCacheSize += size; - if(mBufferCacheSize > mBufferCacheMax) - { - do { - if(mUnusedBuffers.empty()) - { - Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; - break; - } - Sound_Buffer *unused = mUnusedBuffers.back(); - - size = mOutput->unloadSound(unused->mHandle); - mBufferCacheSize -= size; - unused->mHandle = 0; - - mUnusedBuffers.pop_back(); - } while(mBufferCacheSize > mBufferCacheMin); - } - mUnusedBuffers.push_front(sfx); - } - - return sfx; + return std::make_shared(mVFS); } DecoderPtr SoundManager::loadVoice(const std::string &voicefile) @@ -245,19 +147,7 @@ namespace MWSound try { DecoderPtr decoder = getDecoder(); - - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(mVFS->exists(voicefile)) - decoder->open(voicefile); - else - { - std::string file = voicefile; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); - } - + decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, decoder->mResourceMgr)); return decoder; } catch(std::exception &e) @@ -344,13 +234,22 @@ namespace MWSound stopMusic(); DecoderPtr decoder = getDecoder(); - decoder->open(filename); + try + { + decoder->open(filename); + } + catch(std::exception &e) + { + Log(Debug::Error) << "Failed to load audio from " << filename << ": " << e.what(); + return; + } + mMusic = getStreamRef(); mMusic->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(Type::Music); - params.mFlags = PlayMode::NoEnv | Type::Music | Play_2D; + params.mFlags = PlayMode::NoEnvNoScaling | Type::Music | Play_2D; return params; } ()); mOutput->streamSound(decoder, mMusic.get()); @@ -414,20 +313,9 @@ namespace MWSound if (mMusicFiles.find(playlist) == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); - std::string pattern = "Music/" + playlist; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) - { - if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern) - filelist.push_back(found->first); - else - break; - ++found; - } + for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist)) + filelist.push_back(name); mMusicFiles[playlist] = filelist; } @@ -447,13 +335,11 @@ namespace MWSound if (mMusicFiles.find("Title") == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); // Is there an ini setting for this filename or something? std::string filename = "music/special/morrowind title.mp3"; - auto found = index.find(filename); - if (found != index.end()) + if (mVFS->exists(filename)) { - filelist.emplace_back(found->first); + filelist.emplace_back(filename); mMusicFiles["Title"] = filelist; } else @@ -475,10 +361,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; @@ -489,15 +372,15 @@ namespace MWSound StreamPtr sound = playVoice(decoder, pos, (ptr == MWMechanics::getPlayer())); if(!sound) return; - mSaySoundsQueue.emplace(ptr, std::move(sound)); + mSaySoundsQueue.emplace(ptr.mRef, SaySound {ptr.mCell, std::move(sound)}); } float SoundManager::getSaySoundLoudness(const MWWorld::ConstPtr &ptr) const { - SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); + SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); if(snditer != mActiveSaySounds.end()) { - Stream *sound = snditer->second.get(); + Stream *sound = snditer->second.mStream.get(); return mOutput->getStreamLoudness(sound); } @@ -509,10 +392,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; @@ -520,15 +400,15 @@ namespace MWSound StreamPtr sound = playVoice(decoder, osg::Vec3f(), true); if(!sound) return; - mActiveSaySounds.emplace(MWWorld::ConstPtr(), std::move(sound)); + mActiveSaySounds.emplace(nullptr, SaySound {nullptr, std::move(sound)}); } bool SoundManager::sayDone(const MWWorld::ConstPtr &ptr) const { - SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); + SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); if(snditer != mActiveSaySounds.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if(mOutput->isStreamPlaying(snditer->second.mStream.get())) return false; return true; } @@ -537,18 +417,18 @@ namespace MWSound bool SoundManager::sayActive(const MWWorld::ConstPtr &ptr) const { - SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr); + SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr.mRef); if(snditer != mSaySoundsQueue.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if(mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } - snditer = mActiveSaySounds.find(ptr); + snditer = mActiveSaySounds.find(ptr.mRef); if(snditer != mActiveSaySounds.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if(mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } @@ -558,17 +438,17 @@ namespace MWSound void SoundManager::stopSay(const MWWorld::ConstPtr &ptr) { - SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr); + SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr.mRef); if(snditer != mSaySoundsQueue.end()) { - mOutput->finishStream(snditer->second.get()); + mOutput->finishStream(snditer->second.mStream.get()); mSaySoundsQueue.erase(snditer); } - snditer = mActiveSaySounds.find(ptr); + snditer = mActiveSaySounds.find(ptr.mRef); if(snditer != mActiveSaySounds.end()) { - mOutput->finishStream(snditer->second.get()); + mOutput->finishStream(snditer->second.mStream.get()); mActiveSaySounds.erase(snditer); } } @@ -583,7 +463,7 @@ namespace MWSound track->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(type); - params.mFlags = PlayMode::NoEnv | type | Play_2D; + params.mFlags = PlayMode::NoEnvNoScaling | type | Play_2D; return params; } ()); if(!mOutput->streamSound(decoder, track.get())) @@ -610,12 +490,12 @@ namespace MWSound } - Sound* SoundManager::playSound(const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) + Sound* SoundManager::playSound(std::string_view soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { if(!mOutput->isInitialized()) return nullptr; - Sound_Buffer *sfx = loadSound(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; // Only one copy of given sound can be played at time, so stop previous copy @@ -624,27 +504,22 @@ namespace MWSound SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; - params.mVolume = volume * sfx->mVolume; + params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; } ()); - if(!mOutput->playSound(sound.get(), sfx->mHandle, offset)) + if(!mOutput->playSound(sound.get(), sfx->getHandle(), offset)) return nullptr; - if(sfx->mUses++ == 0) - { - SoundList::iterator iter = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), sfx); - if(iter != mUnusedBuffers.end()) - mUnusedBuffers.erase(iter); - } Sound* result = sound.get(); - mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); + mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); + mSoundBuffers.use(*sfx); return result; } - Sound *SoundManager::playSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId, + Sound *SoundManager::playSound3D(const MWWorld::ConstPtr &ptr, std::string_view soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { @@ -652,11 +527,12 @@ namespace MWSound return nullptr; const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); - if ((mode & PlayMode::RemoveAtDistance) && (mListenerPos - objpos).length2() > 2000 * 2000) + const float squaredDist = (mListenerPos - objpos).length2(); + if ((mode & PlayMode::RemoveAtDistance) && squaredDist > 2000 * 2000) return nullptr; // Look up the sound in the ESM data - Sound_Buffer *sfx = loadSound(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; // Only one copy of given sound can be played at time on ptr, so stop previous copy @@ -668,44 +544,43 @@ namespace MWSound { sound->init([&] { SoundParams params; - params.mVolume = volume * sfx->mVolume; + params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; } ()); - played = mOutput->playSound(sound.get(), sfx->mHandle, offset); + played = mOutput->playSound(sound.get(), sfx->getHandle(), offset); } else { sound->init([&] { SoundParams params; params.mPos = objpos; - params.mVolume = volume * sfx->mVolume; + params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; - params.mMinDistance = sfx->mMinDist; - params.mMaxDistance = sfx->mMaxDist; + params.mMinDistance = sfx->getMinDist(); + params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; } ()); - played = mOutput->playSound3D(sound.get(), sfx->mHandle, offset); + played = mOutput->playSound3D(sound.get(), sfx->getHandle(), offset); } if(!played) return nullptr; - if(sfx->mUses++ == 0) - { - SoundList::iterator iter = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), sfx); - if(iter != mUnusedBuffers.end()) - mUnusedBuffers.erase(iter); - } Sound* result = sound.get(); - mActiveSounds[ptr].emplace_back(std::move(sound), sfx); + auto it = mActiveSounds.find(ptr.mRef); + if (it == mActiveSounds.end()) + it = mActiveSounds.emplace(ptr.mRef, ActiveSound {ptr.mCell, {}}).first; + it->second.mList.emplace_back(std::move(sound), sfx); + mSoundBuffers.use(*sfx); return result; } - Sound *SoundManager::playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, + Sound *SoundManager::playSound3D(const osg::Vec3f& initialPos, std::string_view soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { @@ -713,32 +588,30 @@ namespace MWSound return nullptr; // Look up the sound in the ESM data - Sound_Buffer *sfx = loadSound(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; + const float squaredDist = (mListenerPos - initialPos).length2(); + SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; params.mPos = initialPos; - params.mVolume = volume * sfx->mVolume; + params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; - params.mMinDistance = sfx->mMinDist; - params.mMaxDistance = sfx->mMaxDist; + params.mMinDistance = sfx->getMinDist(); + params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; } ()); - if(!mOutput->playSound3D(sound.get(), sfx->mHandle, offset)) + if(!mOutput->playSound3D(sound.get(), sfx->getHandle(), offset)) return nullptr; - if(sfx->mUses++ == 0) - { - SoundList::iterator iter = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), sfx); - if(iter != mUnusedBuffers.end()) - mUnusedBuffers.erase(iter); - } Sound* result = sound.get(); - mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); + mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); + mSoundBuffers.use(*sfx); return result; } @@ -750,10 +623,10 @@ namespace MWSound void SoundManager::stopSound(Sound_Buffer *sfx, const MWWorld::ConstPtr &ptr) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if(snditer != mActiveSounds.end()) { - for(SoundBufferRefPair &snd : snditer->second) + for (SoundBufferRefPair &snd : snditer->second.mList) { if(snd.second == sfx) mOutput->finishSound(snd.first.get()); @@ -761,12 +634,12 @@ namespace MWSound } } - void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId) + void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr, std::string_view soundId) { if(!mOutput->isInitialized()) return; - Sound_Buffer *sfx = lookupSound(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); if (!sfx) return; stopSound(sfx, ptr); @@ -774,54 +647,54 @@ namespace MWSound void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if(snditer != mActiveSounds.end()) { - for(SoundBufferRefPair &snd : snditer->second) + for (SoundBufferRefPair &snd : snditer->second.mList) mOutput->finishSound(snd.first.get()); } - SaySoundMap::iterator sayiter = mSaySoundsQueue.find(ptr); + SaySoundMap::iterator sayiter = mSaySoundsQueue.find(ptr.mRef); if(sayiter != mSaySoundsQueue.end()) - mOutput->finishStream(sayiter->second.get()); - sayiter = mActiveSaySounds.find(ptr); + mOutput->finishStream(sayiter->second.mStream.get()); + sayiter = mActiveSaySounds.find(ptr.mRef); if(sayiter != mActiveSaySounds.end()) - mOutput->finishStream(sayiter->second.get()); + mOutput->finishStream(sayiter->second.mStream.get()); } void SoundManager::stopSound(const MWWorld::CellStore *cell) { - for(SoundMap::value_type &snd : mActiveSounds) + for (auto& [ref, sound] : mActiveSounds) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair& sndbuf : sound.mList) mOutput->finishSound(sndbuf.first.get()); } } - for(SaySoundMap::value_type &snd : mSaySoundsQueue) + for (const auto& [ref, sound] : mSaySoundsQueue) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) - mOutput->finishStream(snd.second.get()); + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) + mOutput->finishStream(sound.mStream.get()); } - for(SaySoundMap::value_type &snd : mActiveSaySounds) + for (const auto& [ref, sound]: mActiveSaySounds) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) - mOutput->finishStream(snd.second.get()); + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) + mOutput->finishStream(sound.mStream.get()); } } void SoundManager::fadeOutSound3D(const MWWorld::ConstPtr &ptr, - const std::string& soundId, float duration) + std::string_view soundId, float duration) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if(snditer != mActiveSounds.end()) { - Sound_Buffer *sfx = lookupSound(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); if (sfx == nullptr) return; - for(SoundBufferRefPair &sndbuf : snditer->second) + for (SoundBufferRefPair &sndbuf : snditer->second.mList) { if(sndbuf.second == sfx) sndbuf.first->setFadeout(duration); @@ -829,16 +702,16 @@ namespace MWSound } } - bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr &ptr, const std::string& soundId) const + bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr &ptr, std::string_view soundId) const { - SoundMap::const_iterator snditer = mActiveSounds.find(ptr); + SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); if(snditer != mActiveSounds.end()) { - Sound_Buffer *sfx = lookupSound(Misc::StringUtils::lowerCase(soundId)); - return std::find_if(snditer->second.cbegin(), snditer->second.cend(), + Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); + return std::find_if(snditer->second.mList.cbegin(), snditer->second.mList.cend(), [this,sfx](const SoundBufferRefPair &snd) -> bool { return snd.second == sfx && mOutput->isSoundPlaying(snd.first.get()); } - ) != snditer->second.cend(); + ) != snditer->second.mList.cend(); } return false; } @@ -921,11 +794,11 @@ namespace MWSound case WaterSoundAction::DoNothing: break; case WaterSoundAction::SetVolume: - mNearWaterSound->setVolume(update.mVolume * sfx->mVolume); + mNearWaterSound->setVolume(update.mVolume * sfx->getVolume()); + mNearWaterSound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); break; case WaterSoundAction::FinishSound: - mOutput->finishSound(mNearWaterSound); - mNearWaterSound = nullptr; + mNearWaterSound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | Play_StopAtFadeEnd); break; case WaterSoundAction::PlaySound: if (mNearWaterSound) @@ -947,18 +820,18 @@ namespace MWSound bool soundIdChanged = false; - Sound_Buffer* sfx = lookupSound(update.mId); + Sound_Buffer* sfx = mSoundBuffers.lookup(update.mId); if (mLastCell != cell) { - const auto snditer = mActiveSounds.find(MWWorld::ConstPtr()); + const auto snditer = mActiveSounds.find(nullptr); if (snditer != mActiveSounds.end()) { const auto pairiter = std::find_if( - snditer->second.begin(), snditer->second.end(), + snditer->second.mList.begin(), snditer->second.mList.end(), [this](const SoundBufferRefPairList::value_type &item) -> bool { return mNearWaterSound == item.first.get(); } ); - if (pairiter != snditer->second.end() && pairiter->second != sfx) + if (pairiter != snditer->second.mList.end() && pairiter->second != sfx) soundIdChanged = true; } } @@ -975,6 +848,28 @@ namespace MWSound return {WaterSoundAction::DoNothing, nullptr}; } + void SoundManager::cull3DSound(SoundBase *sound) + { + // Hard-coded distance of 2000.0f is from vanilla Morrowind + const float maxDist = sound->getDistanceCull() ? 2000.0f : sound->getMaxDistance(); + const float squaredMaxDist = maxDist * maxDist; + + const osg::Vec3f pos = sound->getPosition(); + const float squaredDist = (mListenerPos - pos).length2(); + + if (squaredDist > squaredMaxDist) + { + // If getDistanceCull() is set, delete the sound after it has faded out + sound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | (sound->getDistanceCull() ? Play_StopAtFadeEnd : 0)); + } + else + { + // Fade sounds back in once they are in range + sound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); + } + } + + void SoundManager::updateSounds(float duration) { // We update active say sounds map for specific actors here @@ -1024,45 +919,36 @@ namespace MWSound while(snditer != mActiveSounds.end()) { MWWorld::ConstPtr ptr = snditer->first; - SoundBufferRefPairList::iterator sndidx = snditer->second.begin(); - while(sndidx != snditer->second.end()) + SoundBufferRefPairList::iterator sndidx = snditer->second.mList.begin(); + while(sndidx != snditer->second.mList.end()) { Sound *sound = sndidx->first.get(); - Sound_Buffer *sfx = sndidx->second; - if(!ptr.isEmpty() && sound->getIs3D()) + if (sound->getIs3D()) { - const ESM::Position &pos = ptr.getRefData().getPosition(); - const osg::Vec3f objpos(pos.asVec3()); - sound->setPosition(objpos); - - if(sound->getDistanceCull()) - { - if((mListenerPos - objpos).length2() > 2000*2000) - mOutput->finishSound(sound); - } + if (!ptr.isEmpty()) + sound->setPosition(ptr.getRefData().getPosition().asVec3()); + + cull3DSound(sound); } - if(!mOutput->isSoundPlaying(sound)) + if(!sound->updateFade(duration) || !mOutput->isSoundPlaying(sound)) { mOutput->finishSound(sound); if (sound == mUnderwaterSound) mUnderwaterSound = nullptr; if (sound == mNearWaterSound) mNearWaterSound = nullptr; - if(sfx->mUses-- == 1) - mUnusedBuffers.push_front(sfx); - sndidx = snditer->second.erase(sndidx); + mSoundBuffers.release(*sndidx->second); + sndidx = snditer->second.mList.erase(sndidx); } else { - sound->updateFade(duration); - mOutput->updateSound(sound); ++sndidx; } } - if(snditer->second.empty()) + if(snditer->second.mList.empty()) snditer = mActiveSounds.erase(snditer); else ++snditer; @@ -1072,29 +958,25 @@ namespace MWSound while(sayiter != mActiveSaySounds.end()) { MWWorld::ConstPtr ptr = sayiter->first; - Stream *sound = sayiter->second.get(); - if(!ptr.isEmpty() && sound->getIs3D()) + Stream *sound = sayiter->second.mStream.get(); + if (sound->getIs3D()) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); - sound->setPosition(pos); - - if(sound->getDistanceCull()) + if (!ptr.isEmpty()) { - if((mListenerPos - pos).length2() > 2000*2000) - mOutput->finishStream(sound); + MWBase::World *world = MWBase::Environment::get().getWorld(); + sound->setPosition(world->getActorHeadTransform(ptr).getTrans()); } + + cull3DSound(sound); } - if(!mOutput->isStreamPlaying(sound)) + if(!sound->updateFade(duration) || !mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); - mActiveSaySounds.erase(sayiter++); + sayiter = mActiveSaySounds.erase(sayiter); } else { - sound->updateFade(duration); - mOutput->updateStream(sound); ++sayiter; } @@ -1169,7 +1051,7 @@ namespace MWSound mOutput->startUpdate(); for(SoundMap::value_type &snd : mActiveSounds) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair& sndbuf : snd.second.mList) { Sound *sound = sndbuf.first.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); @@ -1178,13 +1060,13 @@ namespace MWSound } for(SaySoundMap::value_type &snd : mActiveSaySounds) { - Stream *sound = snd.second.get(); + Stream *sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } for(SaySoundMap::value_type &snd : mSaySoundsQueue) { - Stream *sound = snd.second.get(); + Stream *sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } @@ -1214,29 +1096,15 @@ namespace MWSound void SoundManager::updatePtr(const MWWorld::ConstPtr &old, const MWWorld::ConstPtr &updated) { - SoundMap::iterator snditer = mActiveSounds.find(old); + SoundMap::iterator snditer = mActiveSounds.find(old.mRef); if(snditer != mActiveSounds.end()) - { - SoundBufferRefPairList sndlist = std::move(snditer->second); - mActiveSounds.erase(snditer); - mActiveSounds.emplace(updated, std::move(sndlist)); - } + snditer->second.mCell = updated.mCell; - SaySoundMap::iterator sayiter = mSaySoundsQueue.find(old); - if(sayiter != mSaySoundsQueue.end()) - { - StreamPtr stream = std::move(sayiter->second); - mSaySoundsQueue.erase(sayiter); - mSaySoundsQueue.emplace(updated, std::move(stream)); - } + if (const auto it = mSaySoundsQueue.find(old.mRef); it != mSaySoundsQueue.end()) + it->second.mCell = updated.mCell; - sayiter = mActiveSaySounds.find(old); - if(sayiter != mActiveSaySounds.end()) - { - StreamPtr stream = std::move(sayiter->second); - mActiveSaySounds.erase(sayiter); - mActiveSaySounds.emplace(updated, std::move(stream)); - } + if (const auto it = mActiveSaySounds.find(old.mRef); it != mActiveSaySounds.end()) + it->second.mCell = updated.mCell; } // Default readAll implementation, for decoders that can't do anything @@ -1310,12 +1178,10 @@ namespace MWSound for(SoundMap::value_type &snd : mActiveSounds) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair &sndbuf : snd.second.mList) { mOutput->finishSound(sndbuf.first.get()); - Sound_Buffer *sfx = sndbuf.second; - if(sfx->mUses-- == 1) - mUnusedBuffers.push_front(sfx); + mSoundBuffers.release(*sndbuf.second); } } mActiveSounds.clear(); @@ -1323,11 +1189,11 @@ namespace MWSound mNearWaterSound = nullptr; for(SaySoundMap::value_type &snd : mSaySoundsQueue) - mOutput->finishStream(snd.second.get()); + mOutput->finishStream(snd.second.mStream.get()); mSaySoundsQueue.clear(); for(SaySoundMap::value_type &snd : mActiveSaySounds) - mOutput->finishStream(snd.second.get()); + mOutput->finishStream(snd.second.mStream.get()); mActiveSaySounds.clear(); for(StreamPtr& sound : mActiveTracks) diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index f69171a09e..ab74d4acd3 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -18,6 +17,7 @@ #include "watersoundupdater.hpp" #include "type.hpp" #include "volumesettings.hpp" +#include "sound_buffer.hpp" namespace VFS { @@ -34,19 +34,9 @@ namespace MWSound { class Sound_Output; struct Sound_Decoder; + class SoundBase; class Sound; class Stream; - class Sound_Buffer; - - enum Environment { - Env_Normal, - Env_Underwater - }; - // Extra play flags, not intended for caller use - enum PlayModeEx { - Play_2D = 0, - Play_3D = 1<<31 - }; using SoundPtr = Misc::ObjectPtr; using StreamPtr = Misc::ObjectPtr; @@ -66,21 +56,7 @@ namespace MWSound WaterSoundUpdater mWaterSoundUpdater; - typedef std::unique_ptr > SoundBufferList; - // List of sound buffers, grown as needed. New enties are added to the - // back, allowing existing Sound_Buffer references/pointers to remain - // valid. - SoundBufferList mSoundBuffers; - size_t mBufferCacheMin; - size_t mBufferCacheMax; - size_t mBufferCacheSize; - - typedef std::unordered_map NameBufferMap; - NameBufferMap mBufferNameMap; - - // NOTE: unused buffers are stored in front-newest order. - typedef std::deque SoundList; - SoundList mUnusedBuffers; + SoundBufferPool mSoundBuffers; Misc::ObjectPool mSounds; @@ -88,10 +64,23 @@ namespace MWSound typedef std::pair SoundBufferRefPair; typedef std::vector SoundBufferRefPairList; - typedef std::map SoundMap; + + struct ActiveSound + { + const MWWorld::CellStore* mCell = nullptr; + SoundBufferRefPairList mList; + }; + + typedef std::map SoundMap; SoundMap mActiveSounds; - typedef std::map SaySoundMap; + struct SaySound + { + const MWWorld::CellStore* mCell; + StreamPtr mStream; + }; + + typedef std::map SaySoundMap; SaySoundMap mSaySoundsQueue; SaySoundMap mActiveSaySounds; @@ -124,9 +113,6 @@ namespace MWSound Sound_Buffer *insertSound(const std::string &soundId, const ESM::Sound *sound); - Sound_Buffer *lookupSound(const std::string &soundId) const; - Sound_Buffer *loadSound(const std::string &soundId); - // returns a decoder to start streaming, or nullptr if the sound was not found DecoderPtr loadVoice(const std::string &voicefile); @@ -139,6 +125,8 @@ namespace MWSound void advanceMusic(const std::string& filename); void startRandomTitle(); + void cull3DSound(SoundBase *sound); + void updateSounds(float duration); void updateRegionSound(float duration); void updateWaterSound(); @@ -169,7 +157,7 @@ namespace MWSound public: SoundManager(const VFS::Manager* vfs, bool useSound); - virtual ~SoundManager(); + ~SoundManager() override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; @@ -223,17 +211,17 @@ namespace MWSound /// returned by \ref playTrack). Only intended to be called by the track /// decoder's read method. - Sound *playSound(const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; + Sound *playSound(std::string_view soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. - Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, + Sound *playSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. - Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, + Sound *playSound3D(const osg::Vec3f& initialPos, std::string_view soundId, float volume, float pitch, Type type, PlayMode mode, float offset=0) override; ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. ///< @param offset Number of seconds into the sound to start playback. @@ -242,7 +230,7 @@ namespace MWSound ///< Stop the given sound from playing /// @note no-op if \a sound is null - void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) override; + void stopSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId) override; ///< Stop the given object from playing the given sound, void stopSound3D(const MWWorld::ConstPtr &reference) override; @@ -251,13 +239,13 @@ namespace MWSound void stopSound(const MWWorld::CellStore *cell) override; ///< Stop all sounds for the given cell. - void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) override; + void fadeOutSound3D(const MWWorld::ConstPtr &reference, std::string_view soundId, float duration) override; ///< Fade out given sound (that is already playing) of given object ///< @param reference Reference to object, whose sound is faded out ///< @param soundId ID of the sound to fade out. ///< @param duration Time until volume reaches 0. - bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const override; + bool getSoundPlaying(const MWWorld::ConstPtr &reference, std::string_view soundId) const override; ///< Is the given sound currently playing on the given object? void pauseSounds(MWSound::BlockerType blocker, int types=int(Type::Mask)) override; @@ -269,7 +257,7 @@ namespace MWSound void pausePlayback() override; void resumePlayback() override; - void update(float duration) override; + void update(float duration); void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) override; diff --git a/apps/openmw/mwsound/type.hpp b/apps/openmw/mwsound/type.hpp index 9f95bfa401..5f063c3954 100644 --- a/apps/openmw/mwsound/type.hpp +++ b/apps/openmw/mwsound/type.hpp @@ -5,11 +5,11 @@ namespace MWSound { enum class Type { - Sfx = 1 << 4, /* Normal SFX sound */ - Voice = 1 << 5, /* Voice sound */ - Foot = 1 << 6, /* Footstep sound */ - Music = 1 << 7, /* Music track */ - Movie = 1 << 8, /* Movie audio track */ + Sfx = 1 << 5, /* Normal SFX sound */ + Voice = 1 << 6, /* Voice sound */ + Foot = 1 << 7, /* Footstep sound */ + Music = 1 << 8, /* Music track */ + Movie = 1 << 9, /* Movie audio track */ Mask = Sfx | Voice | Foot | Music | Movie }; } diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp index cc4eac3d6d..fd79b97e9b 100644 --- a/apps/openmw/mwsound/volumesettings.cpp +++ b/apps/openmw/mwsound/volumesettings.cpp @@ -10,7 +10,7 @@ namespace MWSound { float clamp(float value) { - return std::max(0.0f, std::min(1.0f, value)); + return std::clamp(value, 0.f, 1.f); } } diff --git a/apps/openmw/mwsound/watersoundupdater.cpp b/apps/openmw/mwsound/watersoundupdater.cpp index b1646c404f..4ef9bfffc4 100644 --- a/apps/openmw/mwsound/watersoundupdater.cpp +++ b/apps/openmw/mwsound/watersoundupdater.cpp @@ -4,7 +4,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/ptr.hpp" -#include +#include #include diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index 3c5c4f8b2a..2d82602534 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -5,14 +5,25 @@ #include -#include +#include #include +#include + bool MWState::operator< (const Slot& left, const Slot& right) { return left.mTimeStamp& contentFiles) +{ + for (const std::string& c : contentFiles) + { + if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) + return c; + } + return ""; +} void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game) { @@ -30,8 +41,7 @@ void MWState::Character::addSlot (const boost::filesystem::path& path, const std slot.mProfile.load (reader); - if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= - Misc::StringUtils::lowerCase (game)) + if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore mSlots.push_back (slot); @@ -44,12 +54,14 @@ void MWState::Character::addSlot (const ESM::SavedGame& profile) std::ostringstream stream; // The profile description is user-supplied, so we need to escape the path - for (std::string::const_iterator it = profile.mDescription.begin(); it != profile.mDescription.end(); ++it) + Utf8Stream description(profile.mDescription); + while(!description.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = description.consume(); + if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } const std::string ext = ".omwsave"; @@ -64,7 +76,7 @@ void MWState::Character::addSlot (const ESM::SavedGame& profile) } slot.mProfile = profile; - slot.mTimeStamp = std::time (0); + slot.mTimeStamp = std::time (nullptr); mSlots.push_back (slot); } @@ -143,7 +155,7 @@ const MWState::Slot *MWState::Character::updateSlot (const Slot *slot, const ESM Slot newSlot = *slot; newSlot.mProfile = profile; - newSlot.mTimeStamp = std::time (0); + newSlot.mTimeStamp = std::time (nullptr); mSlots.erase (mSlots.begin()+index); diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 32c79a183e..2ecd888a7b 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace MWState { @@ -16,6 +16,8 @@ namespace MWState bool operator< (const Slot& left, const Slot& right); + std::string getFirstGameFile(const std::vector& contentFiles); + class Character { public: diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index b5868c3e5a..301f33c5df 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -5,9 +5,11 @@ #include +#include + MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, - const std::string& game) -: mPath (saves), mCurrent (0), mGame (game) + const std::vector& contentFiles) +: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles)) { if (!boost::filesystem::is_directory (mPath)) { @@ -57,12 +59,14 @@ MWState::Character* MWState::CharacterManager::createCharacter(const std::string std::ostringstream stream; // The character name is user-supplied, so we need to escape the path - for (std::string::const_iterator it = name.begin(); it != name.end(); ++it) + Utf8Stream nameStream(name); + while(!nameStream.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = nameStream.consume(); + if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } boost::filesystem::path path = mPath / stream.str(); diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 2daf73401f..8b3f2b8f8f 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -29,7 +29,7 @@ namespace MWState public: - CharacterManager (const boost::filesystem::path& saves, const std::string& game); + CharacterManager (const boost::filesystem::path& saves, const std::vector& contentFiles); Character *getCurrentCharacter (); ///< @note May return null diff --git a/apps/openmw/mwstate/quicksavemanager.cpp b/apps/openmw/mwstate/quicksavemanager.cpp index df078e026c..bf17815207 100644 --- a/apps/openmw/mwstate/quicksavemanager.cpp +++ b/apps/openmw/mwstate/quicksavemanager.cpp @@ -18,14 +18,14 @@ void MWState::QuickSaveManager::visitSave(const Slot *saveSlot) } } -bool MWState::QuickSaveManager::isOldestSave(const Slot *compare) +bool MWState::QuickSaveManager::isOldestSave(const Slot *compare) const { if(mOldestSlotVisited == nullptr) return true; return (compare->mTimeStamp <= mOldestSlotVisited->mTimeStamp); } -bool MWState::QuickSaveManager::shouldCreateNewSlot() +bool MWState::QuickSaveManager::shouldCreateNewSlot() const { return (mSlotsVisited < mMaxSaves); } diff --git a/apps/openmw/mwstate/quicksavemanager.hpp b/apps/openmw/mwstate/quicksavemanager.hpp index a5237d7c30..3272b24b51 100644 --- a/apps/openmw/mwstate/quicksavemanager.hpp +++ b/apps/openmw/mwstate/quicksavemanager.hpp @@ -4,7 +4,6 @@ #include #include "character.hpp" -#include "../mwbase/statemanager.hpp" namespace MWState{ class QuickSaveManager{ @@ -13,8 +12,8 @@ namespace MWState{ unsigned int mSlotsVisited; const Slot *mOldestSlotVisited; private: - bool shouldCreateNewSlot(); - bool isOldestSave(const Slot *compare); + bool shouldCreateNewSlot() const; + bool isOldestSave(const Slot *compare) const; public: QuickSaveManager(std::string &saveName, unsigned int maxSaves); ///< A utility class to manage multiple quicksave slots diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index e409e5b3b9..9f572fde9e 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -1,11 +1,13 @@ #include "statemanagerimp.hpp" +#include + #include -#include -#include -#include -#include +#include +#include +#include +#include #include @@ -27,6 +29,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" @@ -48,8 +51,8 @@ void MWState::StateManager::cleanup (bool force) MWBase::Environment::get().getDialogueManager()->clear(); MWBase::Environment::get().getJournal()->clear(); MWBase::Environment::get().getScriptManager()->clear(); - MWBase::Environment::get().getWorld()->clear(); MWBase::Environment::get().getWindowManager()->clear(); + MWBase::Environment::get().getWorld()->clear(); MWBase::Environment::get().getInputManager()->clear(); MWBase::Environment::get().getMechanicsManager()->clear(); @@ -59,6 +62,7 @@ void MWState::StateManager::cleanup (bool force) MWMechanics::CreatureStats::cleanup(); } + MWBase::Environment::get().getLuaManager()->clear(); } std::map MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader) @@ -86,8 +90,8 @@ std::map MWState::StateManager::buildContentFileIndexMap (const ESM::E return map; } -MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) -: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) +MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles) +: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, contentFiles), mTimePlayed (0) { } @@ -146,7 +150,7 @@ 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; @@ -186,6 +190,10 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot try { + const auto start = std::chrono::steady_clock::now(); + + MWBase::Environment::get().getWindowManager()->asyncPrepareSaveMap(); + if (!character) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); @@ -217,7 +225,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot profile.mTimePlayed = mTimePlayed; profile.mDescription = description; - Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'";; + Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'"; writeScreenshot(profile.mScreenshot); if (!slot) @@ -249,21 +257,21 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot int recordCount = 1 // saved game header +MWBase::Environment::get().getJournal()->countSavedGameRecords() + +MWBase::Environment::get().getLuaManager()->countSavedGameRecords() +MWBase::Environment::get().getWorld()->countSavedGameRecords() +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() - +MWBase::Environment::get().getWindowManager()->countSavedGameRecords() +MWBase::Environment::get().getMechanicsManager()->countSavedGameRecords() - +MWBase::Environment::get().getInputManager()->countSavedGameRecords(); + +MWBase::Environment::get().getInputManager()->countSavedGameRecords() + +MWBase::Environment::get().getWindowManager()->countSavedGameRecords(); writer.setRecordCount (recordCount); writer.save (stream); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount(); // Using only Cells for progress information, since they typically have the largest records by far listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells()); - listener.setLabel("#{sNotifyMessage4}", true, messagesCount > 0); + listener.setLabel("#{sNotifyMessage4}", true); Loading::ScopedLoad load(&listener); @@ -273,11 +281,14 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot MWBase::Environment::get().getJournal()->write (writer, listener); MWBase::Environment::get().getDialogueManager()->write (writer, listener); + // LuaManager::write should be called before World::write because world also saves + // local scripts that depend on LuaManager. + MWBase::Environment::get().getLuaManager()->write(writer, listener); MWBase::Environment::get().getWorld()->write (writer, listener); MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); - MWBase::Environment::get().getWindowManager()->write(writer, listener); MWBase::Environment::get().getMechanicsManager()->write(writer, listener); MWBase::Environment::get().getInputManager()->write(writer, listener); + MWBase::Environment::get().getWindowManager()->write(writer, listener); // Ensure we have written the number of records that was estimated if (writer.getRecordCount() != recordCount+1) // 1 extra for TES3 record @@ -297,6 +308,11 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot Settings::Manager::setString ("character", "Saves", slot->mPath.parent_path().filename().string()); + + const auto finish = std::chrono::steady_clock::now(); + + Log(Debug::Info) << '\'' << description << "' is saved in " + << std::chrono::duration_cast>(finish - start).count() << "ms"; } catch (const std::exception& e) { @@ -374,7 +390,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str { cleanup(); - Log(Debug::Info) << "Reading save file " << boost::filesystem::path(filepath).filename().string(); + Log(Debug::Info) << "Reading save file " << std::filesystem::path(filepath).filename().string(); ESM::ESMReader reader; reader.open (filepath); @@ -383,12 +399,12 @@ void MWState::StateManager::loadGame (const Character *character, const std::str throw std::runtime_error("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."); std::map contentFileMap = buildContentFileIndexMap (reader); + MWBase::Environment::get().getLuaManager()->setContentFileMapping(contentFileMap); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount(); listener.setProgressRange(100); - listener.setLabel("#{sLoadingMessage14}", false, messagesCount > 0); + listener.setLabel("#{sLoadingMessage14}"); Loading::ScopedLoad load(&listener); @@ -401,7 +417,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str ESM::NAME n = reader.getRecName(); reader.getRecHeader(); - switch (n.intval) + switch (n.toInt()) { case ESM::REC_SAVE: { @@ -422,12 +438,12 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: - MWBase::Environment::get().getJournal()->readRecord (reader, n.intval); + MWBase::Environment::get().getJournal()->readRecord (reader, n.toInt()); break; case ESM::REC_DIAS: - MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.intval); + MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.toInt()); break; case ESM::REC_ALCH: @@ -452,7 +468,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_LEVI: case ESM::REC_CREA: case ESM::REC_CONT: - MWBase::Environment::get().getWorld()->readRecord(reader, n.intval, contentFileMap); + case ESM::REC_RAND: + MWBase::Environment::get().getWorld()->readRecord(reader, n.toInt(), contentFileMap); break; case ESM::REC_CAM_: @@ -461,7 +478,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_GSCR: - MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.intval); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.toInt(), contentFileMap); break; case ESM::REC_GMAP: @@ -469,23 +486,27 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_ASPL: case ESM::REC_MARK: - MWBase::Environment::get().getWindowManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getWindowManager()->readRecord(reader, n.toInt()); break; case ESM::REC_DCOU: case ESM::REC_STLN: - MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.toInt()); break; case ESM::REC_INPU: - MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getInputManager()->readRecord(reader, n.toInt()); + break; + + case ESM::REC_LUAM: + MWBase::Environment::get().getLuaManager()->readRecord(reader, n.toInt()); break; default: // ignore invalid records - Log(Debug::Warning) << "Warning: Ignoring unknown record: " << n.toString(); + Log(Debug::Warning) << "Warning: Ignoring unknown record: " << n.toStringView(); reader.skipRecord(); } int progressPercent = static_cast(float(reader.getFileOffset())/total*100); @@ -505,6 +526,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str character->getPath().filename().string()); MWBase::Environment::get().getWindowManager()->setNewGame(false); + MWBase::Environment::get().getWorld()->saveLoaded(); MWBase::Environment::get().getWorld()->setupPlayer(); MWBase::Environment::get().getWorld()->renderPlayer(); MWBase::Environment::get().getWindowManager()->updatePlayer(); @@ -539,6 +561,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str MWBase::Environment::get().getWorld()->changeToCell(cell->getCell()->getCellId(), pos, true, false); } + MWBase::Environment::get().getWorld()->updateProjectilesCasters(); + // Vanilla MW will restart startup scripts when a save game is loaded. This is unintuitive, // but some mods may be using it as a reload detector. MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); @@ -546,6 +570,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); } catch (const std::exception& e) { diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 11984b7f50..3da71e8401 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -31,7 +31,7 @@ namespace MWState public: - StateManager (const boost::filesystem::path& saves, const std::string& game); + StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles); void requestQuit() override; @@ -46,14 +46,14 @@ namespace MWState /// /// \param bypass Skip new game mechanics. - void endGame() override; + void endGame(); void resumeGame() override; void deleteGame (const MWState::Character *character, const MWState::Slot *slot) override; ///< Delete a saved game slot from this character. If all save slots are deleted, the character will be deleted too. - void saveGame (const std::string& description, const Slot *slot = 0) override; + void saveGame (const std::string& description, const Slot *slot = nullptr) override; ///< Write a saved game to \a slot or create a new slot if \a slot == 0. /// /// \note Slot must belong to the current character. @@ -84,7 +84,7 @@ namespace MWState CharacterIterator characterEnd() override; - void update (float duration) override; + void update(float duration); }; } diff --git a/apps/openmw/mwworld/actionapply.cpp b/apps/openmw/mwworld/actionapply.cpp index e3699a6ac3..1699a6aa53 100644 --- a/apps/openmw/mwworld/actionapply.cpp +++ b/apps/openmw/mwworld/actionapply.cpp @@ -17,11 +17,7 @@ namespace MWWorld void ActionApply::executeImp (const Ptr& actor) { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); - - actor.getClass().apply (actor, mId, actor); - - actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); + actor.getClass().consume(getTarget(), actor); } @@ -32,11 +28,8 @@ namespace MWWorld void ActionApplyWithSkill::executeImp (const Ptr& actor) { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); - - if (actor.getClass().apply (actor, mId, actor) && mUsageType!=-1 && actor == MWMechanics::getPlayer()) + bool consumed = actor.getClass().consume(getTarget(), actor); + if (consumed && mUsageType != -1 && actor == MWMechanics::getPlayer()) actor.getClass().skillUsageSucceeded (actor, mSkillIndex, mUsageType); - - actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } } diff --git a/apps/openmw/mwworld/actioneat.cpp b/apps/openmw/mwworld/actioneat.cpp index ef435cca92..c97c83c0c2 100644 --- a/apps/openmw/mwworld/actioneat.cpp +++ b/apps/openmw/mwworld/actioneat.cpp @@ -1,6 +1,6 @@ #include "actioneat.hpp" -#include +#include #include "../mwworld/containerstore.hpp" @@ -12,13 +12,7 @@ namespace MWWorld { void ActionEat::executeImp (const Ptr& actor) { - // remove used item (assume the item is present in inventory) - getTarget().getContainerStore()->remove(getTarget(), 1, actor); - - // apply to actor - std::string id = getTarget().getCellRef().getRefId(); - - if (actor.getClass().apply (actor, id, actor) && actor == MWMechanics::getPlayer()) + if (actor.getClass().consume(getTarget(), actor) && actor == MWMechanics::getPlayer()) actor.getClass().skillUsageSucceeded (actor, ESM::Skill::Alchemy, 1); } diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 4cb0dbe511..8ce3a130ab 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -65,11 +65,7 @@ namespace MWWorld } if (it == invStore.end()) - { - std::stringstream error; - error << "ActionEquip can't find item " << object.getCellRef().getRefId(); - throw std::runtime_error(error.str()); - } + throw std::runtime_error("ActionEquip can't find item " + object.getCellRef().getRefId()); // equip the item in the first free slot std::vector::const_iterator slot=slots_.first.begin(); @@ -91,13 +87,29 @@ namespace MWWorld // move all slots one towards begin(), then equip the item in the slot that is now free if (slot == slots_.first.end()) { - for (slot=slots_.first.begin();slot!=slots_.first.end(); ++slot) + ContainerStoreIterator enchItem = invStore.getSelectedEnchantItem(); + bool reEquip = false; + for (slot = slots_.first.begin(); slot != slots_.first.end(); ++slot) { invStore.unequipSlot(*slot, actor, false); - if (slot+1 != slots_.first.end()) - invStore.equip(*slot, invStore.getSlot(*(slot+1)), actor); + if (slot + 1 != slots_.first.end()) + { + invStore.equip(*slot, invStore.getSlot(*(slot + 1)), actor); + } else + { invStore.equip(*slot, it, actor); + } + + //Fix for issue of selected enchated item getting remmoved on cycle + if (invStore.getSlot(*slot) == enchItem) + { + reEquip = true; + } + } + if (reEquip) + { + invStore.setSelectedEnchantItem(enchItem); } } } diff --git a/apps/openmw/mwworld/actionopen.cpp b/apps/openmw/mwworld/actionopen.cpp index 266ea4d95f..61f2f3e30c 100644 --- a/apps/openmw/mwworld/actionopen.cpp +++ b/apps/openmw/mwworld/actionopen.cpp @@ -5,6 +5,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/disease.hpp" +#include "../mwmechanics/actorutil.hpp" namespace MWWorld { diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index 9cd8469a39..bbf4952063 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -1,12 +1,16 @@ #include "actionteleport.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/cellutils.hpp" #include "player.hpp" @@ -24,7 +28,7 @@ namespace MWWorld { // Find any NPCs that are following the actor and teleport them with him std::set followers; - getFollowers(actor, followers, true); + getFollowers(actor, followers, mCellName.empty(), true); for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) teleport(*it); @@ -51,18 +55,15 @@ namespace MWWorld actor.getClass().getCreatureStats(actor).getAiSequence().stopCombat(); else if (mCellName.empty()) { - int cellX; - int cellY; - world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); - world->moveObject(actor,world->getExterior(cellX,cellY), - mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + const osg::Vec2i index = positionToCellIndex(mPosition.pos[0], mPosition.pos[1]); + world->moveObject(actor, world->getExterior(index.x(), index.y()), mPosition.asVec3(), true, true); } else - world->moveObject(actor,world->getInterior(mCellName),mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3(), true, true); } } - void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles) { + void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles) { std::set followers; MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor, followers); @@ -75,7 +76,7 @@ namespace MWWorld if (!includeHostiles && follower.getClass().getCreatureStats(follower).getAiSequence().isInCombat(actor)) continue; - if (!script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1) + if (!toExterior && !script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1 && follower.getCell()->getCell()->isExterior()) continue; if ((follower.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()).length2() > 800 * 800) diff --git a/apps/openmw/mwworld/actionteleport.hpp b/apps/openmw/mwworld/actionteleport.hpp index 0a981a418e..fcbf59a203 100644 --- a/apps/openmw/mwworld/actionteleport.hpp +++ b/apps/openmw/mwworld/actionteleport.hpp @@ -30,7 +30,7 @@ namespace MWWorld /// @param includeHostiles If true, include hostile followers (which won't actually be teleported) in the output, /// e.g. so that the teleport action can calm them. - static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles = false); + static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles = false); }; } diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 31af5b24bd..ff7869a393 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -12,18 +12,38 @@ #include #include #include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include +#include +#include #include "../mwrender/landmanager.hpp" #include "cellstore.hpp" -#include "manualref.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 { @@ -84,7 +104,7 @@ namespace MWWorld mTerrain->cacheCell(mTerrainView.get(), mX, mY); mPreloadedObjects.insert(mLandManager->getLand(mX, mY)); } - catch(std::exception& e) + catch(std::exception&) { } } @@ -98,7 +118,6 @@ namespace MWWorld { mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); - bool animated = false; size_t slashpos = mesh.find_last_of("/\\"); if (slashpos != std::string::npos && slashpos != mesh.size()-1) { @@ -110,24 +129,18 @@ namespace MWWorld { kfname.replace(kfname.size()-4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) - { mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - animated = true; - } } } } - if (mPreloadInstances && animated) - mPreloadedObjects.insert(mSceneManager->cacheInstance(mesh)); - else - mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); + mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); } - catch (std::exception& e) + catch (std::exception&) { // ignore error for now, would spam the log too much // error will be shown when visiting the cell @@ -161,29 +174,20 @@ namespace MWWorld public: TerrainPreloadItem(const std::vector >& views, Terrain::World* world, const std::vector& preloadPositions) : mAbort(false) - , mProgress(views.size()) - , mProgressRange(0) , mTerrainViews(views) , mWorld(world) , mPreloadPositions(preloadPositions) { } - bool storeViews(double referenceTime) - { - for (unsigned int i=0; istoreView(mTerrainViews[i], referenceTime)) - return false; - return true; - } - void doWork() override { for (unsigned int i=0; ireset(); - mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mProgress[i], mProgressRange); + mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mLoadingReporter); } + mLoadingReporter.complete(); } void abort() override @@ -191,16 +195,17 @@ namespace MWWorld mAbort = true; } - int getProgress() const { return !mProgress.empty() ? mProgress[0].load() : 0; } - int getProgressRange() const { return !mProgress.empty() && mProgress[0].load() ? mProgressRange : 0; } + void wait(Loading::Listener& listener) const + { + mLoadingReporter.wait(listener); + } private: std::atomic mAbort; - std::vector> mProgress; - int mProgressRange; std::vector > mTerrainViews; Terrain::World* mWorld; std::vector mPreloadPositions; + Loading::Reporter mLoadingReporter; }; /// Worker thread item: update the resource system's cache, effectively deleting unused entries. @@ -233,7 +238,7 @@ namespace MWWorld , mMaxCacheSize(0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) - , mStoreViewsFailCount(0) + , mLoadedTerrainTimestamp(0.0) { } @@ -317,11 +322,10 @@ namespace MWWorld PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { - // do the deletion in the background thread if (found->second.mWorkItem) { found->second.mWorkItem->abort(); - mUnrefQueue->push(mPreloadCells[cell].mWorkItem); + found->second.mWorkItem = nullptr; } mPreloadCells.erase(found); @@ -335,7 +339,7 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); @@ -351,7 +355,7 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); } @@ -369,18 +373,8 @@ namespace MWWorld if (mTerrainPreloadItem && mTerrainPreloadItem->isDone()) { - if (!mTerrainPreloadItem->storeViews(timestamp)) - { - if (++mStoreViewsFailCount > 100) - { - OSG_ALWAYS << "paging views are rebuilt every frame, please check for faulty enable/disable scripts." << std::endl; - mStoreViewsFailCount = 0; - } - setTerrainPreloadPositions(std::vector()); - } - else - mStoreViewsFailCount = 0; - mTerrainPreloadItem = nullptr; + mLoadedTerrainPositions = mTerrainPreloadPositions; + mLoadedTerrainTimestamp = timestamp; } } @@ -414,43 +408,25 @@ namespace MWWorld mWorkQueue = workQueue; } - void CellPreloader::setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue) - { - mUnrefQueue = unrefQueue; - } - - bool CellPreloader::syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp) + bool CellPreloader::syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener) { if (!mTerrainPreloadItem) return true; else if (mTerrainPreloadItem->isDone()) { - if (mTerrainPreloadItem->storeViews(timestamp)) - { - mTerrainPreloadItem = nullptr; - return true; - } - else - { - setTerrainPreloadPositions(std::vector()); - setTerrainPreloadPositions(positions); - return false; - } + return true; } else { - progress = mTerrainPreloadItem->getProgress(); - progressRange = mTerrainPreloadItem->getProgressRange(); - return false; + mTerrainPreloadItem->wait(listener); + return true; } } void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid *exceptPos) { - const float resetThreshold = ESM::Land::REAL_SIZE; - for (auto pos : mTerrainPreloadPositions) - if (exceptPos && (pos.first-exceptPos->first).length2() < resetThreshold*resetThreshold && pos.second == exceptPos->second) - return; + if (exceptPos && contains(mTerrainPreloadPositions, std::array {*exceptPos}, ESM::Land::REAL_SIZE)) + return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); @@ -459,40 +435,21 @@ namespace MWWorld setTerrainPreloadPositions(std::vector()); } - bool contains(const std::vector& container, const std::vector& contained) - { - for (auto pos : contained) - { - bool found = false; - for (auto pos2 : container) - { - if ((pos.first-pos2.first).length2() < 1 && pos.second == pos2.second) - { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } - void CellPreloader::setTerrainPreloadPositions(const std::vector &positions) { if (positions.empty()) + { mTerrainPreloadPositions.clear(); - else if (contains(mTerrainPreloadPositions, positions)) + mLoadedTerrainPositions.clear(); + } + else if (contains(mTerrainPreloadPositions, positions, 128.f)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; else { if (mTerrainViews.size() > positions.size()) - { - for (unsigned int i=positions.size(); ipush(mTerrainViews[i]); mTerrainViews.resize(positions.size()); - } else if (mTerrainViews.size() < positions.size()) { for (unsigned int i=mTerrainViews.size(); igetSceneManager()->getExpiryDelay() > referenceTime && contains(mLoadedTerrainPositions, std::array {position}, ESM::Land::REAL_SIZE); + } } diff --git a/apps/openmw/mwworld/cellpreloader.hpp b/apps/openmw/mwworld/cellpreloader.hpp index e719f2e606..185a25276b 100644 --- a/apps/openmw/mwworld/cellpreloader.hpp +++ b/apps/openmw/mwworld/cellpreloader.hpp @@ -19,14 +19,14 @@ namespace Terrain class View; } -namespace SceneUtil +namespace MWRender { - class UnrefQueue; + class LandManager; } -namespace MWRender +namespace Loading { - class LandManager; + class Listener; } namespace MWWorld @@ -67,13 +67,12 @@ namespace MWWorld void setWorkQueue(osg::ref_ptr workQueue); - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); - typedef std::pair PositionCellGrid; void setTerrainPreloadPositions(const std::vector& positions); - bool syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp); + bool syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener); void abortTerrainPreloadExcept(const PositionCellGrid *exceptPos); + bool isTerrainLoaded(const CellPreloader::PositionCellGrid &position, double referenceTime) const; private: Resource::ResourceSystem* mResourceSystem; @@ -81,14 +80,12 @@ namespace MWWorld Terrain::World* mTerrain; MWRender::LandManager* mLandManager; osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; double mExpiryDelay; unsigned int mMinCacheSize; unsigned int mMaxCacheSize; bool mPreloadInstances; double mLastResourceCacheUpdate; - int mStoreViewsFailCount; struct PreloadEntry { @@ -114,6 +111,9 @@ namespace MWWorld std::vector mTerrainPreloadPositions; osg::ref_ptr mTerrainPreloadItem; osg::ref_ptr mUpdateCacheItem; + + std::vector mLoadedTerrainPositions; + double mLoadedTerrainTimestamp; }; } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 188a80ae14..69b6a94da8 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -1,55 +1,38 @@ #include "cellref.hpp" -#include +#include + +#include +#include namespace MWWorld { - const ESM::RefNum& CellRef::getRefNum() const + const ESM::RefNum& CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) { + if (!mCellRef.mRefNum.isSet()) + { + // Generated RefNums have negative mContentFile + assert(lastAssignedRefNum.mContentFile < 0); + lastAssignedRefNum.mIndex++; + if (lastAssignedRefNum.mIndex == 0) // mIndex overflow, so mContentFile should be changed + { + if (lastAssignedRefNum.mContentFile > std::numeric_limits::min()) + lastAssignedRefNum.mContentFile--; + else + Log(Debug::Error) << "RefNum counter overflow in CellRef::getOrAssignRefNum"; + } + mCellRef.mRefNum = lastAssignedRefNum; + mChanged = true; + } return mCellRef.mRefNum; } - bool CellRef::hasContentFile() const - { - return mCellRef.mRefNum.hasContentFile(); - } - void CellRef::unsetRefNum() { mCellRef.mRefNum.unset(); } - std::string CellRef::getRefId() const - { - return mCellRef.mRefID; - } - - const std::string* CellRef::getRefIdPtr() const - { - return &mCellRef.mRefID; - } - - bool CellRef::getTeleport() const - { - return mCellRef.mTeleport; - } - - ESM::Position CellRef::getDoorDest() const - { - return mCellRef.mDoorDest; - } - - std::string CellRef::getDestCell() const - { - return mCellRef.mDestCell; - } - - float CellRef::getScale() const - { - return mCellRef.mScale; - } - void CellRef::setScale(float scale) { if (scale != mCellRef.mScale) @@ -59,22 +42,12 @@ namespace MWWorld } } - ESM::Position CellRef::getPosition() const - { - return mCellRef.mPos; - } - void CellRef::setPosition(const ESM::Position &position) { mChanged = true; mCellRef.mPos = position; } - float CellRef::getEnchantmentCharge() const - { - return mCellRef.mEnchantmentCharge; - } - float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const { if (maxCharge == 0) @@ -100,11 +73,6 @@ namespace MWWorld } } - int CellRef::getCharge() const - { - return mCellRef.mChargeInt; - } - void CellRef::setCharge(int charge) { if (charge != mCellRef.mChargeInt) @@ -132,11 +100,6 @@ namespace MWWorld } } - float CellRef::getChargeFloat() const - { - return mCellRef.mChargeFloat; - } - void CellRef::setChargeFloat(float charge) { if (charge != mCellRef.mChargeFloat) @@ -146,16 +109,6 @@ namespace MWWorld } } - std::string CellRef::getOwner() const - { - return mCellRef.mOwner; - } - - std::string CellRef::getGlobalVariable() const - { - return mCellRef.mGlobalVariable; - } - void CellRef::resetGlobalVariable() { if (!mCellRef.mGlobalVariable.empty()) @@ -174,11 +127,6 @@ namespace MWWorld } } - int CellRef::getFactionRank() const - { - return mCellRef.mFactionRank; - } - void CellRef::setOwner(const std::string &owner) { if (owner != mCellRef.mOwner) @@ -188,11 +136,6 @@ namespace MWWorld } } - std::string CellRef::getSoul() const - { - return mCellRef.mSoul; - } - void CellRef::setSoul(const std::string &soul) { if (soul != mCellRef.mSoul) @@ -202,11 +145,6 @@ namespace MWWorld } } - std::string CellRef::getFaction() const - { - return mCellRef.mFaction; - } - void CellRef::setFaction(const std::string &faction) { if (faction != mCellRef.mFaction) @@ -216,11 +154,6 @@ namespace MWWorld } } - int CellRef::getLockLevel() const - { - return mCellRef.mLockLevel; - } - void CellRef::setLockLevel(int lockLevel) { if (lockLevel != mCellRef.mLockLevel) @@ -243,16 +176,6 @@ namespace MWWorld setLockLevel(-abs(mCellRef.mLockLevel)); //Makes lockLevel negative } - std::string CellRef::getKey() const - { - return mCellRef.mKey; - } - - std::string CellRef::getTrap() const - { - return mCellRef.mTrap; - } - void CellRef::setTrap(const std::string& trap) { if (trap != mCellRef.mTrap) @@ -262,11 +185,6 @@ namespace MWWorld } } - int CellRef::getGoldValue() const - { - return mCellRef.mGoldValue; - } - void CellRef::setGoldValue(int value) { if (value != mCellRef.mGoldValue) @@ -281,9 +199,4 @@ namespace MWWorld state.mRef = mCellRef; } - bool CellRef::hasChanged() const - { - return mChanged; - } - } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index f9f6dbdda2..c7842919a1 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWWORLD_CELLREF_H #define OPENMW_MWWORLD_CELLREF_H -#include +#include namespace ESM { @@ -23,41 +23,42 @@ namespace MWWorld } // Note: Currently unused for items in containers - const ESM::RefNum& getRefNum() const; + const ESM::RefNum& getRefNum() const { return mCellRef.mRefNum; } + + // Returns RefNum. + // If RefNum is not set, assigns a generated one and changes the "lastAssignedRefNum" counter. + const ESM::RefNum& getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum); // Set RefNum to its default state. void unsetRefNum(); /// Does the RefNum have a content file? - bool hasContentFile() const; + bool hasContentFile() const { return mCellRef.mRefNum.hasContentFile(); } // Id of object being referenced - std::string getRefId() const; - - // Pointer to ID of the object being referenced - const std::string* getRefIdPtr() const; + const std::string& getRefId() const { return mCellRef.mRefID; } // For doors - true if this door teleports to somewhere else, false // if it should open through animation. - bool getTeleport() const; + bool getTeleport() const { return mCellRef.mTeleport; } // Teleport location for the door, if this is a teleporting door. - ESM::Position getDoorDest() const; + const ESM::Position& getDoorDest() const { return mCellRef.mDoorDest; } // Destination cell for doors (optional) - std::string getDestCell() const; + const std::string& getDestCell() const { return mCellRef.mDestCell; } // Scale applied to mesh - float getScale() const; + float getScale() const { return mCellRef.mScale; } void setScale(float scale); // The *original* position and rotation as it was given in the Construction Set. // Current position and rotation of the object is stored in RefData. - ESM::Position getPosition() const; + const ESM::Position& getPosition() const { return mCellRef.mPos; } void setPosition (const ESM::Position& position); // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). - float getEnchantmentCharge() const; + float getEnchantmentCharge() const { return mCellRef.mEnchantmentCharge; } // Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment). float getNormalizedEnchantmentCharge(int maxCharge) const; @@ -67,57 +68,57 @@ namespace MWWorld // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. // If this returns int(-1) it means full health. - int getCharge() const; - float getChargeFloat() const; // Implemented as union with int charge + int getCharge() const { return mCellRef.mChargeInt; } + float getChargeFloat() const { return mCellRef.mChargeFloat; } // Implemented as union with int charge void setCharge(int charge); void setChargeFloat(float charge); void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 // The NPC that owns this object (and will get angry if you steal it) - std::string getOwner() const; + const std::string& getOwner() const { return mCellRef.mOwner; } void setOwner(const std::string& owner); // Name of a global variable. If the global variable is set to '1', using the object is temporarily allowed // even if it has an Owner field. // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. - std::string getGlobalVariable() const; + const std::string& getGlobalVariable() const { return mCellRef.mGlobalVariable; } void resetGlobalVariable(); // ID of creature trapped in this soul gem - std::string getSoul() const; + const std::string& getSoul() const { return mCellRef.mSoul; } void setSoul(const std::string& soul); // The faction that owns this object (and will get angry if // you take it and are not a faction member) - std::string getFaction() const; + const std::string& getFaction() const { return mCellRef.mFaction; } void setFaction (const std::string& faction); // PC faction rank required to use the item. Sometimes is -1, which means "any rank". void setFactionRank(int factionRank); - int getFactionRank() const; + int getFactionRank() const { return mCellRef.mFactionRank; } // Lock level for doors and containers // Positive for a locked door. 0 for a door that was never locked. // For an unlocked door, it is set to -(previous locklevel) - int getLockLevel() const; + int getLockLevel() const { return mCellRef.mLockLevel; } void setLockLevel(int lockLevel); void lock(int lockLevel); void unlock(); // Key and trap ID names, if any - std::string getKey() const; - std::string getTrap() const; + const std::string& getKey() const { return mCellRef.mKey; } + const std::string& getTrap() const { return mCellRef.mTrap; } void setTrap(const std::string& trap); // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int getGoldValue() const; + int getGoldValue() const { return mCellRef.mGoldValue; } void setGoldValue(int value); // Write the content of this CellRef into the given ObjectState void writeState (ESM::ObjectState& state) const; // Has this CellRef changed since it was originally loaded? - bool hasChanged() const; + bool hasChanged() const { return mChanged; } private: bool mChanged; diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index 94c78b4332..1105da897a 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -1,11 +1,11 @@ #include "cells.hpp" #include -#include -#include +#include +#include #include -#include -#include +#include +#include #include #include @@ -36,7 +36,7 @@ namespace } bool cont = cell.second.forEach([&] (MWWorld::Ptr ptr) { - if(*ptr.getCellRef().getRefIdPtr() == id) + if (ptr.getCellRef().getRefId() == id) { return visitor(ptr); } @@ -68,9 +68,7 @@ MWWorld::CellStore *MWWorld::Cells::getCellStore (const ESM::Cell *cell) std::map::iterator result = mInteriors.find (lowerName); if (result==mInteriors.end()) - { - result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; - } + result = mInteriors.emplace(std::move(lowerName), CellStore(cell, mStore, mReaders)).first; return &result->second; } @@ -80,11 +78,8 @@ MWWorld::CellStore *MWWorld::Cells::getCellStore (const ESM::Cell *cell) mExteriors.find (std::make_pair (cell->getGridX(), cell->getGridY())); if (result==mExteriors.end()) - { - result = mExteriors.insert (std::make_pair ( - std::make_pair (cell->getGridX(), cell->getGridY()), CellStore (cell, mStore, mReader))).first; - - } + result = mExteriors.emplace(std::make_pair(cell->getGridX(), cell->getGridY()), + CellStore(cell, mStore, mReaders)).first; return &result->second; } @@ -94,7 +89,7 @@ void MWWorld::Cells::clear() { mInteriors.clear(); mExteriors.clear(); - std::fill(mIdCache.begin(), mIdCache.end(), std::make_pair("", (MWWorld::CellStore*)0)); + std::fill(mIdCache.begin(), mIdCache.end(), std::make_pair("", (MWWorld::CellStore*)nullptr)); mIdCacheIndex = 0; } @@ -130,11 +125,14 @@ void MWWorld::Cells::writeCell (ESM::ESMWriter& writer, CellStore& cell) const writer.endRecord (ESM::REC_CSTA); } -MWWorld::Cells::Cells (const MWWorld::ESMStore& store, std::vector& reader) -: mStore (store), mReader (reader), - mIdCache (Settings::Manager::getInt("pointers cache size", "Cells"), std::pair ("", (CellStore*)0)), - mIdCacheIndex (0) -{} +MWWorld::Cells::Cells (const MWWorld::ESMStore& store, ESM::ReadersCache& readers) + : mStore(store) + , mReaders(readers) + , mIdCacheIndex(0) +{ + int cacheSize = std::clamp(Settings::Manager::getInt("pointers cache size", "Cells"), 40, 1000); + mIdCache = IdCache(cacheSize, std::pair ("", (CellStore*)nullptr)); +} MWWorld::CellStore *MWWorld::Cells::getExterior (int x, int y) { @@ -163,8 +161,7 @@ MWWorld::CellStore *MWWorld::Cells::getExterior (int x, int y) cell = MWBase::Environment::get().getWorld()->createRecord (record); } - result = mExteriors.insert (std::make_pair ( - std::make_pair (x, y), CellStore (cell, mStore, mReader))).first; + result = mExteriors.emplace(std::make_pair(x, y), CellStore(cell, mStore, mReaders)).first; } if (result->second.getState()!=CellStore::State_Loaded) @@ -184,7 +181,7 @@ MWWorld::CellStore *MWWorld::Cells::getInterior (const std::string& name) { const ESM::Cell *cell = mStore.get().find(lowerName); - result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; + result = mInteriors.emplace(std::move(lowerName), CellStore(cell, mStore, mReaders)).first; } if (result->second.getState()!=CellStore::State_Loaded) @@ -259,8 +256,7 @@ MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name, CellStore& cell, MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name) { // First check the cache - for (std::vector >::iterator iter (mIdCache.begin()); - iter!=mIdCache.end(); ++iter) + for (IdCache::iterator iter (mIdCache.begin()); iter!=mIdCache.end(); ++iter) if (iter->first==name && iter->second) { Ptr ptr = getPtr (name, *iter->second); @@ -449,7 +445,7 @@ bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, uint32_t type, ESM::CellState state; state.mId.load (reader); - CellStore *cellStore = 0; + CellStore *cellStore = nullptr; try { diff --git a/apps/openmw/mwworld/cells.hpp b/apps/openmw/mwworld/cells.hpp index 90ede409b2..5c2244a2d6 100644 --- a/apps/openmw/mwworld/cells.hpp +++ b/apps/openmw/mwworld/cells.hpp @@ -11,6 +11,7 @@ namespace ESM { class ESMReader; class ESMWriter; + class ReadersCache; struct CellId; struct Cell; struct RefNum; @@ -28,11 +29,12 @@ namespace MWWorld /// \brief Cell container class Cells { + typedef std::vector > IdCache; const MWWorld::ESMStore& mStore; - std::vector& mReader; + ESM::ReadersCache& mReaders; mutable std::map mInteriors; mutable std::map, CellStore> mExteriors; - std::vector > mIdCache; + IdCache mIdCache; std::size_t mIdCacheIndex; Cells (const Cells&); @@ -50,7 +52,7 @@ namespace MWWorld void clear(); - Cells (const MWWorld::ESMStore& store, std::vector& reader); + explicit Cells(const MWWorld::ESMStore& store, ESM::ReadersCache& reader); CellStore *getExterior (int x, int y); diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 14b96b27c6..7b9dd57d09 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1,23 +1,26 @@ #include "cellstore.hpp" +#include "magiceffects.hpp" #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 "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" @@ -25,6 +28,7 @@ #include "../mwmechanics/recharge.hpp" #include "ptr.hpp" +#include "esmloader.hpp" #include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" @@ -37,7 +41,7 @@ namespace for (typename MWWorld::CellRefList::List::iterator iter (containerList.mList.begin()); iter!=containerList.mList.end(); ++iter) { - MWWorld::Ptr container (&*iter, 0); + MWWorld::Ptr container (&*iter, nullptr); if (container.getRefData().getCustomData() == nullptr) continue; @@ -104,6 +108,45 @@ namespace } } + template + void fixRestockingImpl(const T* base, RecordType& state) + { + // Workaround for old saves not containing negative quantities + for(const auto& baseItem : base->mInventory.mList) + { + if(baseItem.mCount < 0) + { + for(auto& item : state.mInventory.mItems) + { + if(item.mCount > 0 && Misc::StringUtils::ciEqual(baseItem.mItem, item.mRef.mRefID)) + item.mCount = -item.mCount; + } + } + } + } + + template + void fixRestocking(const T* base, RecordType& state) + {} + + template<> + void fixRestocking<>(const ESM::Creature* base, ESM::CreatureState& state) + { + fixRestockingImpl(base, state); + } + + template<> + void fixRestocking<>(const ESM::NPC* base, ESM::NpcState& state) + { + fixRestockingImpl(base, state); + } + + template<> + void fixRestocking<>(const ESM::Container* base, ESM::ContainerState& state) + { + fixRestockingImpl(base, state); + } + template void readReferenceCollection (ESM::ESMReader& reader, MWWorld::CellRefList& collection, const ESM::CellRef& cref, const std::map& contentFileMap, MWWorld::CellStore* cellstore) @@ -134,11 +177,26 @@ namespace if (!record) return; + if (state.mVersion < 15) + fixRestocking(record, state); + if (state.mVersion < 17) + { + 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) + { + if constexpr (std::is_same_v || std::is_same_v) + MWWorld::convertStats(state.mCreatureStats); + } + if (state.mRef.mRefNum.hasContentFile()) { for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) - if (iter->mRef.getRefNum()==state.mRef.mRefNum && *iter->mRef.getRefIdPtr() == state.mRef.mRefID) + if (iter->mRef.getRefNum()==state.mRef.mRefNum && iter->mRef.getRefId() == state.mRef.mRefID) { // overwrite existing reference float oldscale = iter->mRef.getScale(); @@ -147,12 +205,14 @@ namespace const ESM::Position & newpos = iter->mData.getPosition(); const MWWorld::Ptr ptr(&*iter, cellstore); if ((oldscale != iter->mRef.getScale() || oldpos.asVec3() != newpos.asVec3() || oldpos.rot[0] != newpos.rot[0] || oldpos.rot[1] != newpos.rot[1] || oldpos.rot[2] != newpos.rot[2]) && !ptr.getClass().isActor()) - MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.pos[0], newpos.pos[1], newpos.pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.asVec3()); if (!iter->mData.isEnabled()) { iter->mData.enable(); MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore)); } + else + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(&*iter, cellstore)); return; } @@ -164,6 +224,9 @@ namespace MWWorld::LiveCellRef ref (record); ref.load (state); collection.mList.push_back (ref); + + MWWorld::LiveCellRefBase* base = &collection.mList.back(); + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(base, cellstore)); } } @@ -244,16 +307,7 @@ namespace MWWorld if (searchViaRefNum(object.getCellRef().getRefNum()).isEmpty()) throw std::runtime_error("moveTo: object is not in this cell"); - - // Objects with no refnum can't be handled correctly in the merging process that happens - // on a save/load, so do a simple copy & delete for these objects. - if (!object.getCellRef().getRefNum().hasContentFile()) - { - MWWorld::Ptr copied = object.getClass().copyToCell(object, *cellToMoveTo, object.getRefData().getCount()); - object.getRefData().setCount(0); - object.getRefData().setBaseNode(nullptr); - return copied; - } + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(object.getBase(), cellToMoveTo)); MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); if (found != mMovedHere.end()) @@ -303,8 +357,8 @@ namespace MWWorld void merge() { - for (std::map::const_iterator it = mMovedHere.begin(); it != mMovedHere.end(); ++it) - mMergeTo.push_back(it->first); + for (const auto & [base, _] : mMovedHere) + mMergeTo.push_back(base); } private: @@ -334,8 +388,14 @@ namespace MWWorld return false; } - CellStore::CellStore (const ESM::Cell *cell, const MWWorld::ESMStore& esmStore, std::vector& readerList) - : mStore(esmStore), mReader(readerList), mCell (cell), mState (State_Unloaded), mHasState (false), mLastRespawn(0,0), mRechargingItemsUpToDate(false) + CellStore::CellStore(const ESM::Cell* cell, const MWWorld::ESMStore& esmStore, ESM::ReadersCache& readers) + : mStore(esmStore) + , mReaders(readers) + , mCell(cell) + , mState(State_Unloaded) + , mHasState(false) + , mLastRespawn(0, 0) + , mRechargingItemsUpToDate(false) { mWaterLevel = cell->mWater; } @@ -378,7 +438,7 @@ namespace MWWorld const std::string *mIdToFind; bool operator()(const PtrType& ptr) { - if (*ptr.getCellRef().getRefIdPtr() == *mIdToFind) + if (ptr.getCellRef().getRefId() == *mIdToFind) { mFound = ptr; return false; @@ -411,9 +471,9 @@ namespace MWWorld if (Ptr ptr = ::searchViaActorId (mCreatures, id, this, mMovedToAnotherCell)) return ptr; - for (MovedRefTracker::const_iterator it = mMovedHere.begin(); it != mMovedHere.end(); ++it) + for (const auto& [base, _] : mMovedHere) { - MWWorld::Ptr actor (it->first, this); + MWWorld::Ptr actor (base, this); if (!actor.getClass().isActor()) continue; if (actor.getClass().getCreatureStats (actor).matchesActorId (id) && actor.getRefData().getCount() > 0) @@ -492,8 +552,6 @@ namespace MWWorld void CellStore::listRefs() { - std::vector& esm = mReader; - assert (mCell); if (mCell->mContextList.empty()) @@ -505,16 +563,20 @@ namespace MWWorld try { // Reopen the ESM reader and seek to the right position. - int index = mCell->mContextList.at(i).index; - mCell->restore (esm[index], i); + const std::size_t index = static_cast(mCell->mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = mReaders.get(index); + mCell->restore(*reader, i); ESM::CellRef ref; // Get each reference in turn + ESM::MovedCellRef cMRef; + cMRef.mRefNum.mIndex = 0; bool deleted = false; - while (mCell->getNextRef (esm[index], ref, deleted)) + bool moved = false; + while (ESM::Cell::getNextRef(*reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { - if (deleted) + if (deleted || moved) continue; // Don't list reference if it was moved to a different cell. @@ -524,7 +586,8 @@ namespace MWWorld continue; } - mIds.push_back (Misc::StringUtils::lowerCase (ref.mRefID)); + Misc::StringUtils::lowerCaseInPlace(ref.mRefID); + mIds.push_back(std::move(ref.mRefID)); } } catch (std::exception& e) @@ -534,11 +597,8 @@ namespace MWWorld } // List moved references, from separately tracked list. - for (ESM::CellRefTracker::const_iterator it = mCell->mLeasedRefs.begin(); it != mCell->mLeasedRefs.end(); ++it) + for (const auto& [ref, deleted]: mCell->mLeasedRefs) { - const ESM::CellRef &ref = it->first; - bool deleted = it->second; - if (!deleted) mIds.push_back(Misc::StringUtils::lowerCase(ref.mRefID)); } @@ -548,8 +608,6 @@ namespace MWWorld void CellStore::loadRefs() { - std::vector& esm = mReader; - assert (mCell); if (mCell->mContextList.empty()) @@ -563,16 +621,23 @@ namespace MWWorld try { // Reopen the ESM reader and seek to the right position. - int index = mCell->mContextList.at(i).index; - mCell->restore (esm[index], i); + const std::size_t index = static_cast(mCell->mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = mReaders.get(index); + mCell->restore(*reader, i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); // Get each reference in turn + ESM::MovedCellRef cMRef; + cMRef.mRefNum.mIndex = 0; bool deleted = false; - while(mCell->getNextRef(esm[index], ref, deleted)) + bool moved = false; + while (ESM::Cell::getNextRef(*reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { + if (moved) + continue; + // Don't load reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); @@ -590,10 +655,10 @@ namespace MWWorld } // Load moved references, from separately tracked list. - for (ESM::CellRefTracker::const_iterator it = mCell->mLeasedRefs.begin(); it != mCell->mLeasedRefs.end(); ++it) + for (const auto& leasedRef : mCell->mLeasedRefs) { - ESM::CellRef &ref = const_cast(it->first); - bool deleted = it->second; + ESM::CellRef &ref = const_cast(leasedRef.first); + bool deleted = leasedRef.second; loadRef (ref, deleted, refNumToID); } @@ -732,7 +797,7 @@ namespace MWWorld void CellStore::readFog(ESM::ESMReader &reader) { - mFogState.reset(new ESM::FogState()); + mFogState = std::make_unique(); mFogState->load(reader); } @@ -760,11 +825,10 @@ namespace MWWorld writeReferenceCollection (writer, mWeapons); writeReferenceCollection (writer, mBodyParts); - for (MovedRefTracker::const_iterator it = mMovedToAnotherCell.begin(); it != mMovedToAnotherCell.end(); ++it) + for (const auto& [base, store] : mMovedToAnotherCell) { - LiveCellRefBase* base = it->first; ESM::RefNum refNum = base->mRef.getRefNum(); - ESM::CellId movedTo = it->second->getCell()->getCellId(); + ESM::CellId movedTo = store->getCell()->getCellId(); refNum.save(writer, true, "MVRF"); movedTo.save(writer); @@ -788,7 +852,12 @@ namespace MWWorld if (type == 0) { Log(Debug::Warning) << "Dropping reference to '" << cref.mRefID << "' (object no longer exists)"; - reader.skipHSubUntil("OBJE"); + // Skip until the next OBJE or MVRF + while(reader.hasMoreSubs() && !reader.peekNextSub("OBJE") && !reader.peekNextSub("MVRF")) + { + reader.getSubName(); + reader.skipHSub(); + } continue; } @@ -917,6 +986,13 @@ namespace MWWorld refnum.load(reader, true, "MVRF"); movedTo.load(reader); + if (refnum.hasContentFile()) + { + auto iter = contentFileMap.find(refnum.mContentFile); + if (iter != contentFileMap.end()) + refnum.mContentFile = iter->second; + } + // Search for the reference. It might no longer exist if its content file was removed. Ptr movedRef = searchViaRefNum(refnum); if (movedRef.isEmpty()) @@ -958,9 +1034,9 @@ namespace MWWorld return !(left==right); } - void CellStore::setFog(ESM::FogState *fog) + void CellStore::setFog(std::unique_ptr&& fog) { - mFogState.reset(fog); + mFogState = std::move(fog); } ESM::FogState* CellStore::getFog() const @@ -1084,9 +1160,9 @@ namespace MWWorld updateRechargingItems(); mRechargingItemsUpToDate = true; } - for (TRechargingItems::iterator it = mRechargingItems.begin(); it != mRechargingItems.end(); ++it) + for (const auto& [item, charge] : mRechargingItems) { - MWMechanics::rechargeItem(it->first, it->second, duration); + MWMechanics::rechargeItem(item, charge, duration); } } @@ -1094,41 +1170,25 @@ namespace MWWorld { mRechargingItems.clear(); - for (CellRefList::List::iterator it (mWeapons.mList.begin()); it!=mWeapons.mList.end(); ++it) - { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) - { - checkItem(ptr); - } - } - for (CellRefList::List::iterator it (mArmors.mList.begin()); it!=mArmors.mList.end(); ++it) - { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) - { - checkItem(ptr); - } - } - for (CellRefList::List::iterator it (mClothes.mList.begin()); it!=mClothes.mList.end(); ++it) + const auto update = [this](auto& list) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + for (auto & item : list) { - checkItem(ptr); - } - } - for (CellRefList::List::iterator it (mBooks.mList.begin()); it!=mBooks.mList.end(); ++it) - { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) - { - checkItem(ptr); + Ptr ptr = getCurrentPtr(&item); + if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + { + checkItem(ptr); + } } - } + }; + + update(mWeapons.mList); + update(mArmors.mList); + update(mClothes.mList); + update(mBooks.mList); } - void MWWorld::CellStore::checkItem(Ptr ptr) + void MWWorld::CellStore::checkItem(const Ptr& ptr) { if (ptr.getClass().getEnchantment(ptr).empty()) return; @@ -1145,4 +1205,18 @@ namespace MWWorld || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) mRechargingItems.emplace_back(ptr.getBase(), static_cast(enchantment->mData.mCharge)); } + + Ptr MWWorld::CellStore::getMovedActor(int actorId) const + { + for(const auto& [cellRef, cell] : mMovedToAnotherCell) + { + if(cellRef->mClass->isActor() && cellRef->mData.getCustomData()) + { + Ptr actor(cellRef, cell); + if(actor.getClass().getCreatureStats(actor).getActorId() == actorId) + return actor; + } + } + return {}; + } } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index edd8577ae0..091deaf2c5 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -11,35 +11,36 @@ #include "livecellref.hpp" #include "cellreflist.hpp" -#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 +#include +#include +#include +#include +#include +#include #include "timestamp.hpp" #include "ptr.hpp" namespace ESM { + class ReadersCache; struct Cell; struct CellState; - struct FogState; struct CellId; struct RefNum; } @@ -61,12 +62,12 @@ namespace MWWorld private: const MWWorld::ESMStore& mStore; - std::vector& mReader; + ESM::ReadersCache& mReaders; // Even though fog actually belongs to the player and not cells, // it makes sense to store it here since we need it once for each cell. // Note this is nullptr until the cell is explored to save some memory - std::shared_ptr mFogState; + std::unique_ptr mFogState; const ESM::Cell *mCell; State mState; @@ -127,7 +128,7 @@ namespace MWWorld void updateRechargingItems(); void rechargeItems(float duration); - void checkItem(Ptr ptr); + void checkItem(const Ptr& ptr); // helper function for forEachInternal template @@ -211,9 +212,7 @@ namespace MWWorld } /// @param readerList The readers to use for loading of the cell on-demand. - CellStore (const ESM::Cell *cell_, - const MWWorld::ESMStore& store, - std::vector& readerList); + CellStore(const ESM::Cell* cell, const MWWorld::ESMStore& store, ESM::ReadersCache& readers); const ESM::Cell *getCell() const; @@ -254,7 +253,7 @@ namespace MWWorld void setWaterLevel (float level); - void setFog (ESM::FogState* fog); + void setFog(std::unique_ptr&& fog); ///< \note Takes ownership of the pointer ESM::FogState* getFog () const; @@ -397,6 +396,8 @@ namespace MWWorld void respawn (); ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. + Ptr getMovedActor(int actorId) const; + private: /// Run through references and store IDs diff --git a/apps/openmw/mwworld/cellutils.hpp b/apps/openmw/mwworld/cellutils.hpp new file mode 100644 index 0000000000..c827974c35 --- /dev/null +++ b/apps/openmw/mwworld/cellutils.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_MWWORLD_CELLUTILS_H +#define OPENMW_MWWORLD_CELLUTILS_H + +#include + +#include + +#include + +namespace MWWorld +{ + inline osg::Vec2i positionToCellIndex(float x, float y) + { + return { + static_cast(std::floor(x / Constants::CellSizeInUnits)), + static_cast(std::floor(y / Constants::CellSizeInUnits)) + }; + } +} + +#endif diff --git a/apps/openmw/mwworld/cellvisitors.hpp b/apps/openmw/mwworld/cellvisitors.hpp index e68b383b77..77f33fa84b 100644 --- a/apps/openmw/mwworld/cellvisitors.hpp +++ b/apps/openmw/mwworld/cellvisitors.hpp @@ -13,17 +13,18 @@ namespace MWWorld { std::vector mObjects; - bool operator() (MWWorld::Ptr ptr) + bool operator() (const MWWorld::Ptr& ptr) { if (ptr.getRefData().getBaseNode()) { ptr.getRefData().setBaseNode(nullptr); - mObjects.push_back (ptr); } + mObjects.push_back (ptr); return true; } }; + } #endif diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 950c8a6d49..d43c4bbba1 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -10,7 +10,6 @@ #include "../mwworld/esmstore.hpp" #include "ptr.hpp" -#include "refdata.hpp" #include "nullaction.hpp" #include "failedaction.hpp" #include "actiontake.hpp" @@ -18,28 +17,30 @@ #include "../mwgui/tooltips.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWWorld { - std::map > Class::sClasses; - - Class::Class() {} - - Class::~Class() {} + std::map& Class::getClasses() + { + static std::map values; + return values; + } void Class::insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const { } - void Class::insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const + void Class::insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { } - bool Class::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const + void Class::insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const + {} + + bool Class::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const { return false; } @@ -114,14 +115,14 @@ namespace MWWorld throw std::runtime_error("class cannot be hit"); } - std::shared_ptr Class::activate (const Ptr& ptr, const Ptr& actor) const + std::unique_ptr Class::activate (const Ptr& ptr, const Ptr& actor) const { - return std::shared_ptr (new NullAction); + return std::make_unique(); } - std::shared_ptr Class::use (const Ptr& ptr, bool force) const + std::unique_ptr Class::use (const Ptr& ptr, bool force) const { - return std::shared_ptr (new NullAction); + return std::make_unique(); } ContainerStore& Class::getContainerStore (const Ptr& ptr) const @@ -229,15 +230,13 @@ namespace MWWorld throw std::runtime_error("Class does not support armor rating"); } - const Class& Class::get (const std::string& key) + const Class& Class::get (unsigned int key) { - if (key.empty()) - throw std::logic_error ("Class::get(): attempting to get an empty key"); - - std::map >::const_iterator iter = sClasses.find (key); + const auto& classes = getClasses(); + auto iter = classes.find(key); - if (iter==sClasses.end()) - throw std::logic_error ("Class::get(): unknown class key: " + key); + if (iter == classes.end()) + throw std::logic_error ("Class::get(): unknown class key: " + std::to_string(key)); return *iter->second; } @@ -247,10 +246,9 @@ namespace MWWorld throw std::runtime_error ("class does not support persistence"); } - void Class::registerClass(const std::string& key, std::shared_ptr instance) + void Class::registerClass(Class& instance) { - instance->mTypeName = key; - sClasses.insert(std::make_pair(key, instance)); + getClasses().emplace(instance.getType(), &instance); } std::string Class::getUpSoundId (const ConstPtr& ptr) const @@ -331,23 +329,24 @@ namespace MWWorld { } - std::shared_ptr Class::defaultItemActivate(const Ptr &ptr, const Ptr &actor) const + std::unique_ptr Class::defaultItemActivate(const Ptr &ptr, const Ptr &actor) const { if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr(new NullAction()); + return std::make_unique(); if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfItem"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound *sound = store.get().searchRandom("WolfItem", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if(sound) action->setSound(sound->mId); return action; } - std::shared_ptr action(new ActionTake(ptr)); + std::unique_ptr action = std::make_unique(ptr); action->setSound(getUpSoundId(ptr)); return action; @@ -522,7 +521,7 @@ namespace MWWorld return result; } - void Class::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Class::setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const { throw std::runtime_error ("class does not have creature stats"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 1b3d4029e4..f1f4c46df0 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -6,11 +6,13 @@ #include #include +#include #include #include "ptr.hpp" #include "doorstate.hpp" -#include "../mwmechanics/creaturestats.hpp" + +#include "../mwmechanics/aisetting.hpp" namespace ESM { @@ -31,6 +33,7 @@ namespace MWMechanics { class NpcStats; struct Movement; + class CreatureStats; } namespace MWGui @@ -53,34 +56,33 @@ namespace MWWorld /// \brief Base class for referenceable esm records class Class { - static std::map > sClasses; - - std::string mTypeName; + const unsigned mType; - // not implemented - Class (const Class&); - Class& operator= (const Class&); + static std::map& getClasses(); protected: - Class(); + explicit Class(unsigned type) : mType(type) {} - std::shared_ptr defaultItemActivate(const Ptr &ptr, const Ptr &actor) const; + std::unique_ptr defaultItemActivate(const Ptr &ptr, const Ptr &actor) const; ///< Generate default action for activating inventory items virtual Ptr copyToCellImpl(const ConstPtr &ptr, CellStore &cell) const; public: - virtual ~Class(); + virtual ~Class() = default; + Class (const Class&) = delete; + Class& operator= (const Class&) = delete; - const std::string& getTypeName() const { - return mTypeName; + unsigned int getType() const { + return mType; } virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; - virtual void insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const; + virtual void insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; ///< Add reference into a cell for rendering (default implementation: don't render anything). + virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; virtual std::string getName (const ConstPtr& ptr) const = 0; ///< \return name or ID; can return an empty string. @@ -139,10 +141,10 @@ namespace MWWorld ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield /// (default implementation: throw an exception) - virtual std::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; + virtual std::unique_ptr activate (const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). - virtual std::shared_ptr use (const Ptr& ptr, bool force=false) + virtual std::unique_ptr use (const Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu (default implementation: return a /// null action). @@ -220,13 +222,8 @@ namespace MWWorld virtual float getNormalizedEncumbrance (const MWWorld::Ptr& ptr) const; ///< Returns encumbrance re-scaled to capacity - virtual bool apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const; - ///< Apply \a id on \a ptr. - /// \param actor Actor that is resposible for the ID being applied to \a ptr. - /// \return Any effect? - /// - /// (default implementation: ignore and return false) + virtual bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const; + ///< Consume an item, e. g. a potion. virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. @@ -338,10 +335,10 @@ namespace MWWorld const; ///< Write additional state from \a ptr into \a state. - static const Class& get (const std::string& key); + static const Class& get (unsigned int key); ///< If there is no class for this \a key, an exception is thrown. - static void registerClass (const std::string& key, std::shared_ptr instance); + static void registerClass(Class& instance); virtual int getBaseGold(const MWWorld::ConstPtr& ptr) const; @@ -366,7 +363,7 @@ namespace MWWorld virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; - virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const; + virtual void setBaseAISetting(const std::string& id, MWMechanics::AiSetting setting, int value) const; virtual void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const; }; diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 6fad429263..b72b0e2db0 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -1,11 +1,11 @@ #include "containerstore.hpp" #include -#include #include #include -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -20,13 +20,14 @@ #include "class.hpp" #include "localscripts.hpp" #include "player.hpp" +#include "esmstore.hpp" namespace { void addScripts(MWWorld::ContainerStore& store, MWWorld::CellStore* cell) { auto& scripts = MWBase::Environment::get().getWorld()->getLocalScripts(); - for(const MWWorld::Ptr& ptr : store) + for(const auto&& ptr : store) { const std::string& script = ptr.getClass().getScript(ptr); if(!script.empty()) @@ -43,13 +44,10 @@ namespace { float sum = 0; - for (typename MWWorld::CellRefList::List::const_iterator iter ( - cellRefList.mList.begin()); - iter!=cellRefList.mList.end(); - ++iter) + for (const MWWorld::LiveCellRef& liveCellRef : cellRefList.mList) { - if (iter->mData.getCount()>0) - sum += iter->mData.getCount()*iter->mBase->mData.mWeight; + if (const int count = liveCellRef.mData.getCount(); count > 0) + sum += count * liveCellRef.mBase->mData.mWeight; } return sum; @@ -62,12 +60,11 @@ namespace store->resolve(); std::string id2 = Misc::StringUtils::lowerCase (id); - for (typename MWWorld::CellRefList::List::iterator iter (list.mList.begin()); - iter!=list.mList.end(); ++iter) + for (MWWorld::LiveCellRef& liveCellRef : list.mList) { - if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2) && iter->mData.getCount()) + if (Misc::StringUtils::ciEqual(liveCellRef.mBase->mId, id2) && liveCellRef.mData.getCount()) { - MWWorld::Ptr ptr (&*iter, 0); + MWWorld::Ptr ptr(&liveCellRef, nullptr); ptr.setContainerStore (store); return ptr; } @@ -79,13 +76,13 @@ namespace MWWorld::ResolutionListener::~ResolutionListener() { - if(!mStore.mModified && mStore.mResolved && !mStore.mPtr.isEmpty()) + try { - for(const MWWorld::Ptr& ptr : mStore) - ptr.getRefData().setCount(0); - mStore.fillNonRandom(mStore.mPtr.get()->mBase->mInventory, "", mStore.mSeed); - addScripts(mStore, mStore.mPtr.mCell); - mStore.mResolved = false; + mStore.unresolve(); + } + catch(const std::exception& e) + { + Log(Debug::Error) << "Failed to clear temporary container contents: " << e.what(); } } @@ -127,16 +124,15 @@ template void MWWorld::ContainerStore::storeStates (const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable) const { - for (typename CellRefList::List::const_iterator iter (collection.mList.begin()); - iter!=collection.mList.end(); ++iter) + for (const LiveCellRef& liveCellRef : collection.mList) { - if (iter->mData.getCount() == 0) + if (liveCellRef.mData.getCount() == 0) continue; ESM::ObjectState state; - storeState (*iter, state); + storeState(liveCellRef, state); if (equipable) - storeEquipmentState(*iter, index, inventory); - inventory.mItems.push_back (state); + storeEquipmentState(liveCellRef, index, inventory); + inventory.mItems.push_back(std::move(state)); ++index; } } @@ -185,10 +181,10 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() return ContainerStoreIterator (this); } -int MWWorld::ContainerStore::count(const std::string &id) const +int MWWorld::ContainerStore::count(std::string_view id) const { int total=0; - for (const auto& iter : *this) + for (const auto&& iter : *this) if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) total += iter.getRefData().getCount(); return total; @@ -284,7 +280,7 @@ bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) && cls2.getItemHealth(ptr2) == cls2.getItemMaxHealth(ptr2))); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const std::string &id, int count, const Ptr &actorPtr) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(std::string_view id, int count, const Ptr &actorPtr) { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), id, count); return add(ref.getPtr(), count, actorPtr); @@ -326,7 +322,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr if (actorPtr == player) { // Items in player's inventory have cell set to 0, so their scripts will never be removed - item.mCell = 0; + item.mCell = nullptr; } else { @@ -369,7 +365,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp (const Ptr& ptr, for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) { - if (Misc::StringUtils::ciEqual((*iter).getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); flagAsModified(); @@ -430,14 +426,14 @@ void MWWorld::ContainerStore::rechargeItems(float duration) updateRechargingItems(); mRechargingItemsUpToDate = true; } - for (TRechargingItems::iterator it = mRechargingItems.begin(); it != mRechargingItems.end(); ++it) + for (auto& it : mRechargingItems) { - if (!MWMechanics::rechargeItem(*it->first, it->second, duration)) + if (!MWMechanics::rechargeItem(*it.first, it.second, duration)) continue; // attempt to restack when fully recharged - if (it->first->getCellRef().getEnchantmentCharge() == it->second) - it->first = restack(*it->first); + if (it.first->getCellRef().getEnchantmentCharge() == it.second) + it.first = restack(*it.first); } } @@ -463,7 +459,7 @@ void MWWorld::ContainerStore::updateRechargingItems() } } -int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) +int MWWorld::ContainerStore::remove(std::string_view itemId, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) { if(resolveFirst) resolve(); @@ -481,9 +477,9 @@ int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const bool MWWorld::ContainerStore::hasVisibleItems() const { - for (auto iter(begin()); iter != end(); ++iter) + for (const auto&& iter : *this) { - if (iter->getClass().showsInInventory(*iter)) + if (iter.getClass().showsInInventory(iter)) return true; } @@ -520,12 +516,12 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor return count - toRemove; } -void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed) +void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Generator& prng) { for (const ESM::ContItem& iter : items.mList) { std::string id = Misc::StringUtils::lowerCase(iter.mItem); - addInitialItem(id, owner, iter.mCount, &seed); + addInitialItem(id, owner, iter.mCount, &prng); } flagAsModified(); @@ -546,7 +542,7 @@ void MWWorld::ContainerStore::fillNonRandom (const ESM::InventoryList& items, co } void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std::string& owner, int count, - Misc::Rng::Seed* seed, bool topLevel) + Misc::Rng::Generator* prng, bool topLevel) { if (count == 0) return; //Don't restock with nothing. try @@ -554,13 +550,13 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id, count); if (ref.getPtr().getClass().getScript(ref.getPtr()).empty()) { - addInitialItemImp(ref.getPtr(), owner, count, seed, topLevel); + addInitialItemImp(ref.getPtr(), owner, count, prng, topLevel); } else { // Adding just one item per time to make sure there isn't a stack of scripted items for (int i = 0; i < std::abs(count); i++) - addInitialItemImp(ref.getPtr(), owner, count < 0 ? -1 : 1, seed, topLevel); + addInitialItemImp(ref.getPtr(), owner, count < 0 ? -1 : 1, prng, topLevel); } } catch (const std::exception& e) @@ -570,26 +566,26 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: } void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const std::string& owner, int count, - Misc::Rng::Seed* seed, bool topLevel) + Misc::Rng::Generator* prng, bool topLevel) { - if (ptr.getTypeName()==typeid (ESM::ItemLevList).name()) + if (ptr.getType()==ESM::ItemLevList::sRecordId) { - if(!seed) + if(!prng) return; const ESM::ItemLevList* levItemList = ptr.get()->mBase; if (topLevel && std::abs(count) > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { for (int i=0; i 0 ? 1 : -1, seed, true); + addInitialItem(ptr.getCellRef().getRefId(), owner, count > 0 ? 1 : -1, prng, true); return; } else { - std::string itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *seed); + std::string itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *prng); if (itemId.empty()) return; - addInitialItem(itemId, owner, count, seed, false); + addInitialItem(itemId, owner, count, prng, false); } } else @@ -601,8 +597,8 @@ void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const s void MWWorld::ContainerStore::clear() { - for (ContainerStoreIterator iter (begin()); iter!=end(); ++iter) - iter->getRefData().setCount (0); + for (auto&& iter : *this) + iter.getRefData().setCount (0); flagAsModified(); mModified = true; @@ -623,10 +619,10 @@ void MWWorld::ContainerStore::resolve() { if(!mResolved && !mPtr.isEmpty()) { - for(const MWWorld::Ptr& ptr : *this) + for(const auto&& ptr : *this) ptr.getRefData().setCount(0); - Misc::Rng::Seed seed{mSeed}; - fill(mPtr.get()->mBase->mInventory, "", seed); + Misc::Rng::Generator prng{mSeed}; + fill(mPtr.get()->mBase->mInventory, "", prng); addScripts(*this, mPtr.mCell); } mModified = true; @@ -644,15 +640,30 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() } if(!mResolved && !mPtr.isEmpty()) { - for(const MWWorld::Ptr& ptr : *this) + for(const auto&& ptr : *this) ptr.getRefData().setCount(0); - Misc::Rng::Seed seed{mSeed}; - fill(mPtr.get()->mBase->mInventory, "", seed); + Misc::Rng::Generator prng{mSeed}; + fill(mPtr.get()->mBase->mInventory, "", prng); addScripts(*this, mPtr.mCell); } return {listener}; } +void MWWorld::ContainerStore::unresolve() +{ + if (mModified) + return; + + if (mResolved && !mPtr.isEmpty()) + { + for(const auto&& ptr : *this) + ptr.getRefData().setCount(0); + fillNonRandom(mPtr.get()->mBase->mInventory, "", mSeed); + addScripts(*this, mPtr.mCell); + mResolved = false; + } +} + float MWWorld::ContainerStore::getWeight() const { if (!mWeightUpToDate) @@ -683,54 +694,54 @@ int MWWorld::ContainerStore::getType (const ConstPtr& ptr) if (ptr.isEmpty()) throw std::runtime_error ("can't put a non-existent object into a container"); - if (ptr.getTypeName()==typeid (ESM::Potion).name()) + if (ptr.getType()==ESM::Potion::sRecordId) return Type_Potion; - if (ptr.getTypeName()==typeid (ESM::Apparatus).name()) + if (ptr.getType()==ESM::Apparatus::sRecordId) return Type_Apparatus; - if (ptr.getTypeName()==typeid (ESM::Armor).name()) + if (ptr.getType()==ESM::Armor::sRecordId) return Type_Armor; - if (ptr.getTypeName()==typeid (ESM::Book).name()) + if (ptr.getType()==ESM::Book::sRecordId) return Type_Book; - if (ptr.getTypeName()==typeid (ESM::Clothing).name()) + if (ptr.getType()==ESM::Clothing::sRecordId) return Type_Clothing; - if (ptr.getTypeName()==typeid (ESM::Ingredient).name()) + if (ptr.getType()==ESM::Ingredient::sRecordId) return Type_Ingredient; - if (ptr.getTypeName()==typeid (ESM::Light).name()) + if (ptr.getType()==ESM::Light::sRecordId) return Type_Light; - if (ptr.getTypeName()==typeid (ESM::Lockpick).name()) + if (ptr.getType()==ESM::Lockpick::sRecordId) return Type_Lockpick; - if (ptr.getTypeName()==typeid (ESM::Miscellaneous).name()) + if (ptr.getType()==ESM::Miscellaneous::sRecordId) return Type_Miscellaneous; - if (ptr.getTypeName()==typeid (ESM::Probe).name()) + if (ptr.getType()==ESM::Probe::sRecordId) return Type_Probe; - if (ptr.getTypeName()==typeid (ESM::Repair).name()) + if (ptr.getType()==ESM::Repair::sRecordId) return Type_Repair; - if (ptr.getTypeName()==typeid (ESM::Weapon).name()) + if (ptr.getType()==ESM::Weapon::sRecordId) return Type_Weapon; - throw std::runtime_error ( - "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); + throw std::runtime_error("Object '" + ptr.getCellRef().getRefId() + "' of type " + + std::string(ptr.getTypeDescription()) + " can not be placed into a container"); } MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) { MWWorld::Ptr item; int itemHealth = 1; - for (MWWorld::ContainerStoreIterator iter = begin(); iter != end(); ++iter) + for (auto&& iter : *this) { - int iterHealth = iter->getClass().hasItemHealth(*iter) ? iter->getClass().getItemHealth(*iter) : 1; - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) + int iterHealth = iter.getClass().hasItemHealth(iter) ? iter.getClass().getItemHealth(iter) : 1; + if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) { // Prefer the stack with the lowest remaining uses // Try to get item with zero durability only if there are no other items found @@ -738,7 +749,7 @@ MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) (iterHealth > 0 && iterHealth < itemHealth) || (itemHealth <= 0 && iterHealth > 0)) { - item = *iter; + item = iter; itemHealth = iterHealth; } } @@ -867,11 +878,8 @@ void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) mResolved = true; int index = 0; - for (std::vector::const_iterator - iter (inventory.mItems.begin()); iter!=inventory.mItems.end(); ++iter) + for (const ESM::ObjectState& state : inventory.mItems) { - const ESM::ObjectState& state = *iter; - int type = MWBase::Environment::get().getWorld()->getStore().find(state.mRef.mRefID); int thisIndex = index++; @@ -1138,18 +1146,18 @@ PtrType MWWorld::ContainerStoreIteratorBase::operator*() const switch (mType) { - case ContainerStore::Type_Potion: ptr = PtrType (&*mPotion, 0); break; - case ContainerStore::Type_Apparatus: ptr = PtrType (&*mApparatus, 0); break; - case ContainerStore::Type_Armor: ptr = PtrType (&*mArmor, 0); break; - case ContainerStore::Type_Book: ptr = PtrType (&*mBook, 0); break; - case ContainerStore::Type_Clothing: ptr = PtrType (&*mClothing, 0); break; - case ContainerStore::Type_Ingredient: ptr = PtrType (&*mIngredient, 0); break; - case ContainerStore::Type_Light: ptr = PtrType (&*mLight, 0); break; - case ContainerStore::Type_Lockpick: ptr = PtrType (&*mLockpick, 0); break; - case ContainerStore::Type_Miscellaneous: ptr = PtrType (&*mMiscellaneous, 0); break; - case ContainerStore::Type_Probe: ptr = PtrType (&*mProbe, 0); break; - case ContainerStore::Type_Repair: ptr = PtrType (&*mRepair, 0); break; - case ContainerStore::Type_Weapon: ptr = PtrType (&*mWeapon, 0); break; + case ContainerStore::Type_Potion: ptr = PtrType (&*mPotion, nullptr); break; + case ContainerStore::Type_Apparatus: ptr = PtrType (&*mApparatus, nullptr); break; + case ContainerStore::Type_Armor: ptr = PtrType (&*mArmor, nullptr); break; + case ContainerStore::Type_Book: ptr = PtrType (&*mBook, nullptr); break; + case ContainerStore::Type_Clothing: ptr = PtrType (&*mClothing, nullptr); break; + case ContainerStore::Type_Ingredient: ptr = PtrType (&*mIngredient, nullptr); break; + case ContainerStore::Type_Light: ptr = PtrType (&*mLight, nullptr); break; + case ContainerStore::Type_Lockpick: ptr = PtrType (&*mLockpick, nullptr); break; + case ContainerStore::Type_Miscellaneous: ptr = PtrType (&*mMiscellaneous, nullptr); break; + case ContainerStore::Type_Probe: ptr = PtrType (&*mProbe, nullptr); break; + case ContainerStore::Type_Repair: ptr = PtrType (&*mRepair, nullptr); break; + case ContainerStore::Type_Weapon: ptr = PtrType (&*mWeapon, nullptr); break; } if (ptr.isEmpty()) diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index e0843efbac..74e46ea5f4 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -6,18 +6,18 @@ #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 @@ -58,7 +58,7 @@ namespace MWWorld std::shared_ptr mListener; public: ResolutionHandle(std::shared_ptr listener) : mListener(listener) {} - ResolutionHandle() {} + ResolutionHandle() = default; }; class ContainerStoreListener @@ -73,22 +73,22 @@ namespace MWWorld { public: - static const int Type_Potion = 0x0001; - static const int Type_Apparatus = 0x0002; - static const int Type_Armor = 0x0004; - static const int Type_Book = 0x0008; - static const int Type_Clothing = 0x0010; - static const int Type_Ingredient = 0x0020; - static const int Type_Light = 0x0040; - static const int Type_Lockpick = 0x0080; - static const int Type_Miscellaneous = 0x0100; - static const int Type_Probe = 0x0200; - static const int Type_Repair = 0x0400; - static const int Type_Weapon = 0x0800; + static constexpr int Type_Potion = 0x0001; + static constexpr int Type_Apparatus = 0x0002; + static constexpr int Type_Armor = 0x0004; + static constexpr int Type_Book = 0x0008; + static constexpr int Type_Clothing = 0x0010; + static constexpr int Type_Ingredient = 0x0020; + static constexpr int Type_Light = 0x0040; + static constexpr int Type_Lockpick = 0x0080; + static constexpr int Type_Miscellaneous = 0x0100; + static constexpr int Type_Probe = 0x0200; + static constexpr int Type_Repair = 0x0400; + static constexpr int Type_Weapon = 0x0800; - static const int Type_Last = Type_Weapon; + static constexpr int Type_Last = Type_Weapon; - static const int Type_All = 0xffff; + static constexpr int Type_All = 0xffff; static const std::string sGoldId; @@ -126,8 +126,8 @@ namespace MWWorld std::weak_ptr mResolutionListener; ContainerStoreIterator addImp (const Ptr& ptr, int count, bool markModified = true); - void addInitialItem (const std::string& id, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); - void addInitialItemImp (const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); + void addInitialItem (const std::string& id, const std::string& owner, int count, Misc::Rng::Generator* prng, bool topLevel=true); + void addInitialItemImp (const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Generator* prng, bool topLevel=true); template ContainerStoreIterator getState (CellRefList& collection, @@ -153,7 +153,7 @@ namespace MWWorld virtual ~ContainerStore(); - virtual ContainerStore* clone() { return new ContainerStore(*this); } + virtual std::unique_ptr clone() { return std::make_unique(*this); } ConstContainerStoreIterator cbegin (int mask = Type_All) const; ConstContainerStoreIterator cend() const; @@ -175,10 +175,10 @@ namespace MWWorld /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. - ContainerStoreIterator add(const std::string& id, int count, const Ptr& actorPtr); + ContainerStoreIterator add(std::string_view id, int count, const Ptr& actorPtr); ///< Utility to construct a ManualRef and call add(ptr, count, actorPtr, true) - int remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); + int remove(std::string_view itemId, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); ///< Remove \a count item(s) designated by \a itemId from this container. /// /// @return the number of items actually removed @@ -201,7 +201,7 @@ namespace MWWorld /// If a compatible stack is found, the item's count is added to that stack, then the original is deleted. /// @return If the item was stacked, return the stack, otherwise return the old (untouched) item. - int count (const std::string& id) const; + int count(std::string_view id) const; ///< @return How many items with refID \a id are in this container? ContainerStoreListener* getContListener() const; @@ -221,7 +221,7 @@ namespace MWWorld virtual bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const; ///< @return true if the two specified objects can stack with each other - void fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed = Misc::Rng::getSeed()); + void fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Generator& seed); ///< Insert items into *this. void fillNonRandom (const ESM::InventoryList& items, const std::string& owner, unsigned int seed); @@ -250,6 +250,7 @@ namespace MWWorld void resolve(); ResolutionHandle resolveTemporarily(); + void unresolve(); friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; @@ -257,21 +258,19 @@ namespace MWWorld friend class MWClass::Container; }; - template class ContainerStoreIteratorBase - : public std::iterator { template struct IsConvertible { - static const bool value = true; + static constexpr bool value = true; }; template struct IsConvertible { - static const bool value = false; + static constexpr bool value = false; }; template @@ -361,6 +360,12 @@ namespace MWWorld /// \return reached the end? public: + using iterator_category = std::forward_iterator_tag; + using value_type = PtrType; + using difference_type = std::ptrdiff_t; + using pointer = PtrType*; + using reference = PtrType&; + template ContainerStoreIteratorBase (const ContainerStoreIteratorBase& other) { diff --git a/apps/openmw/mwworld/contentloader.hpp b/apps/openmw/mwworld/contentloader.hpp index b529ae9db8..55de77ad25 100644 --- a/apps/openmw/mwworld/contentloader.hpp +++ b/apps/openmw/mwworld/contentloader.hpp @@ -2,33 +2,20 @@ #define CONTENTLOADER_HPP #include -#include -#include -#include "components/loadinglistener/loadinglistener.hpp" +namespace Loading +{ + class Listener; +} namespace MWWorld { struct ContentLoader { - ContentLoader(Loading::Listener& listener) - : mListener(listener) - { - } - - virtual ~ContentLoader() - { - } - - virtual void load(const boost::filesystem::path& filepath, int& index) - { - Log(Debug::Info) << "Loading content file " << filepath.string(); - mListener.setLabel(MyGUI::TextIterator::toTagsString(filepath.string())); - } + virtual ~ContentLoader() = default; - protected: - Loading::Listener& mListener; + virtual void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) = 0; }; } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 8af45e36ad..7200e7684c 100644 --- a/apps/openmw/mwworld/customdata.hpp +++ b/apps/openmw/mwworld/customdata.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWWORLD_CUSTOMDATA_H #define GAME_MWWORLD_CUSTOMDATA_H +#include + namespace MWClass { class CreatureCustomData; @@ -19,7 +21,7 @@ namespace MWWorld virtual ~CustomData() {} - virtual CustomData *clone() const = 0; + virtual std::unique_ptr clone() const = 0; // Fast version of dynamic_cast. Needs to be overridden in the respective class. @@ -38,6 +40,15 @@ namespace MWWorld virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; }; + + template + struct TypedCustomData : CustomData + { + std::unique_ptr clone() const final + { + return std::make_unique(*static_cast(this)); + } + }; } #endif diff --git a/apps/openmw/mwworld/datetimemanager.cpp b/apps/openmw/mwworld/datetimemanager.cpp index 0894c974d3..67d1ce1fbc 100644 --- a/apps/openmw/mwworld/datetimemanager.cpp +++ b/apps/openmw/mwworld/datetimemanager.cpp @@ -159,7 +159,7 @@ namespace MWWorld return setting->mValue.getString(); } - bool DateTimeManager::updateGlobalFloat(const std::string& name, float value) + bool DateTimeManager::updateGlobalFloat(std::string_view name, float value) { if (name=="gamehour") { @@ -192,7 +192,7 @@ namespace MWWorld return false; } - bool DateTimeManager::updateGlobalInt(const std::string& name, int value) + bool DateTimeManager::updateGlobalInt(std::string_view name, int value) { if (name=="gamehour") { diff --git a/apps/openmw/mwworld/datetimemanager.hpp b/apps/openmw/mwworld/datetimemanager.hpp index b460be746a..e48eec5184 100644 --- a/apps/openmw/mwworld/datetimemanager.hpp +++ b/apps/openmw/mwworld/datetimemanager.hpp @@ -35,8 +35,8 @@ namespace MWWorld void advanceTime(double hours, Globals& globalVariables); void setup(Globals& globalVariables); - bool updateGlobalInt(const std::string& name, int value); - bool updateGlobalFloat(const std::string& name, float value); + bool updateGlobalInt(std::string_view name, int value); + bool updateGlobalFloat(std::string_view name, float value); }; } diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index b12d646e70..795b070dfa 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -1,31 +1,42 @@ #include "esmloader.hpp" #include "esmstore.hpp" -#include +#include +#include namespace MWWorld { -EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener) - : ContentLoader(listener) - , mEsm(readers) - , mStore(store) - , mEncoder(encoder) +EsmLoader::EsmLoader(MWWorld::ESMStore& store, ESM::ReadersCache& readers, ToUTF8::Utf8Encoder* encoder) + : mReaders(readers) + , mStore(store) + , mEncoder(encoder) + , mDialogue(nullptr) // A content file containing INFO records without a DIAL record appends them to the previous file's dialogue { } -void EsmLoader::load(const boost::filesystem::path& filepath, int& index) +void EsmLoader::load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) { - ContentLoader::load(filepath.filename(), index); - - ESM::ESMReader lEsm; - lEsm.setEncoder(mEncoder); - lEsm.setIndex(index); - lEsm.setGlobalReaderList(&mEsm); - lEsm.open(filepath.string()); - mEsm[index] = lEsm; - mStore.load(mEsm[index], &mListener); + const ESM::ReadersCache::BusyItem reader = mReaders.get(static_cast(index)); + + reader->setEncoder(mEncoder); + reader->setIndex(index); + reader->open(filepath.string()); + reader->resolveParentFileIndices(mReaders); + + assert(reader->getGameFiles().size() == reader->getParentFileIndices().size()); + for (std::size_t i = 0, n = reader->getParentFileIndices().size(); i < n; ++i) + if (i == static_cast(reader->getIndex())) + throw std::runtime_error("File " + reader->getName() + " asks for parent file " + + reader->getGameFiles()[i].name + + ", but it is not available or has been loaded in the wrong order. " + "Please run the launcher to fix this issue."); + + mStore.load(*reader, listener, mDialogue); + + if (!mMasterFileFormat.has_value() && (Misc::StringUtils::ciEndsWith(reader->getName(), ".esm") + || Misc::StringUtils::ciEndsWith(reader->getName(), ".omwgame"))) + mMasterFileFormat = reader->getFormat(); } } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index 506105bebb..c1cc406d92 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -1,7 +1,7 @@ #ifndef ESMLOADER_HPP #define ESMLOADER_HPP -#include +#include #include "contentloader.hpp" @@ -12,7 +12,8 @@ namespace ToUTF8 namespace ESM { - class ESMReader; + class ReadersCache; + struct Dialogue; } namespace MWWorld @@ -22,15 +23,18 @@ class ESMStore; struct EsmLoader : public ContentLoader { - EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener); + explicit EsmLoader(MWWorld::ESMStore& store, ESM::ReadersCache& readers, ToUTF8::Utf8Encoder* encoder); - void load(const boost::filesystem::path& filepath, int& index) override; + std::optional getMasterFileFormat() const { return mMasterFileFormat; } + + void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override; private: - std::vector& mEsm; - MWWorld::ESMStore& mStore; - ToUTF8::Utf8Encoder* mEncoder; + ESM::ReadersCache& mReaders; + MWWorld::ESMStore& mStore; + ToUTF8::Utf8Encoder* mEncoder; + ESM::Dialogue* mDialogue; + std::optional mMasterFileFormat; }; } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 942d5feeba..2c9b374645 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -1,50 +1,127 @@ #include "esmstore.hpp" -#include - -#include +#include +#include #include +#include +#include #include -#include -#include +#include +#include +#include +#include #include "../mwmechanics/spelllist.hpp" namespace { - void readRefs(const ESM::Cell& cell, std::map& refs, std::vector& readers) + struct Ref { + ESM::RefNum mRefNum; + std::size_t mRefID; + + Ref(ESM::RefNum refNum, std::size_t refID) : mRefNum(refNum), mRefID(refID) {} + }; + + constexpr std::size_t deletedRefID = std::numeric_limits::max(); + + void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, ESM::ReadersCache& readers) + { + // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { - size_t index = cell.mContextList[i].index; - if (readers.size() <= index) - readers.resize(index + 1); - cell.restore(readers[index], 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; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); bool deleted = false; - while(cell.getNextRef(readers[index], ref, deleted)) + while (cell.getNextRef(*reader, ref, deleted)) { if(deleted) - refs.erase(ref.mRefNum); + refs.emplace_back(ref.mRefNum, deletedRefID); else if (std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) == cell.mMovedRefs.end()) { - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - refs[ref.mRefNum] = ref.mRefID; + refs.emplace_back(ref.mRefNum, refIDs.size()); + refIDs.push_back(std::move(ref.mRefID)); } } } - for(const auto& it : cell.mLeasedRefs) + for(const auto& [value, deleted] : cell.mLeasedRefs) { - bool deleted = it.second; if(deleted) - refs.erase(it.first.mRefNum); + refs.emplace_back(value.mRefNum, deletedRefID); else { - ESM::CellRef ref = it.first; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - refs[ref.mRefNum] = ref.mRefID; + refs.emplace_back(value.mRefNum, refIDs.size()); + refIDs.push_back(value.mRefID); + } + } + } + + const std::string& getDefaultClass(const MWWorld::Store& classes) + { + auto it = classes.begin(); + if (it != classes.end()) + return it->mId; + throw std::runtime_error("List of NPC classes is empty!"); + } + + std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::unordered_map& npcs) + { + // Cache first class from store - we will use it if current class is not found + const std::string& defaultCls = getDefaultClass(classes); + + // Validate NPCs for non-existing class and faction. + // We will replace invalid entries by fixed ones + std::vector npcsToReplace; + + for (const auto& npcIter : npcs) + { + ESM::NPC npc = npcIter.second; + bool changed = false; + + const std::string& npcFaction = npc.mFaction; + if (!npcFaction.empty()) + { + const ESM::Faction *fact = factions.search(npcFaction); + if (!fact) + { + Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it."; + npc.mFaction.clear(); + npc.mNpdt.mRank = 0; + changed = true; + } + } + + const std::string& npcClass = npc.mClass; + const ESM::Class *cls = classes.search(npcClass); + if (!cls) + { + Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement."; + npc.mClass = defaultCls; + changed = true; + } + + if (changed) + npcsToReplace.push_back(npc); + } + + return npcsToReplace; + } + + // 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 + void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) + { + for(auto& [id, item] : items) + { + if(!item.mScript.empty() && !scripts.search(item.mScript)) + { + item.mScript.clear(); + Log(Debug::Verbose) << "Item '" << id << "' (" << item.mName << ") has nonexistent script '" << item.mScript << "', ignoring it."; } } } @@ -67,57 +144,32 @@ static bool isCacheableRecord(int id) return false; } -void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) +void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener, ESM::Dialogue*& dialogue) { - listener->setProgressRange(1000); - - ESM::Dialogue *dialogue = 0; + 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 to avoid continual resizes during loading, - // and so we can properly verify if valid plugin indices are being passed to the - // LandTexture Store retrieval methods. - mLandTextures.resize(esm.getGlobalReaderList()->size()); - - /// \todo Move this to somewhere else. ESMReader? - // Cache parent esX files by tracking their indices in the global list of - // all files/readers used by the engine. This will greaty accelerate - // refnumber mangling, as required for handling moved references. - const std::vector &masters = esm.getGameFiles(); - std::vector *allPlugins = esm.getGlobalReaderList(); - for (size_t j = 0; j < masters.size(); j++) { - const ESM::Header::MasterData &mast = masters[j]; - std::string fname = mast.name; - int index = ~0; - for (int i = 0; i < esm.getIndex(); i++) { - const std::string candidate = allPlugins->at(i).getContext().filename; - std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); - if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { - index = i; - break; - } - } - if (index == (int)~0) { - // Tried to load a parent file that has not been loaded yet. This is bad, - // the launcher should have taken care of this. - std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name - + ", but it has not been loaded yet. Please check your load order."; - esm.fail(fstring); - } - esm.addParentFileIndex(index); - } + // 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. + mLandTextures.resize(esm.getIndex()+1); // Loop through all records while(esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); + if (esm.getRecordFlags() & ESM::FLAG_Ignored) + { + esm.skipRecord(); + continue; + } // Look up the record type. - std::map::iterator it = mStores.find(n.intval); + std::map::iterator it = mStores.find(n.toInt()); if (it == mStores.end()) { - if (n.intval == ESM::REC_INFO) { + if (n.toInt() == ESM::REC_INFO) { if (dialogue) { dialogue->readInfo(esm, esm.getIndex() != 0); @@ -127,20 +179,25 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) Log(Debug::Error) << "Error: info record without dialog"; esm.skipRecord(); } - } else if (n.intval == ESM::REC_MGEF) { + } else if (n.toInt() == ESM::REC_MGEF) { mMagicEffects.load (esm); - } else if (n.intval == ESM::REC_SKIL) { + } else if (n.toInt() == ESM::REC_SKIL) { mSkills.load (esm); } - else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP) + else if (n.toInt() == ESM::REC_FILT || n.toInt() == ESM::REC_DBGP) { // ignore project file only records esm.skipRecord(); } + else if (n.toInt() == ESM::REC_LUAL) + { + ESM::LuaScriptsCfg cfg; + cfg.load(esm); + cfg.adjustRefNums(esm); + mLuaContent.push_back(std::move(cfg)); + } else { - std::stringstream error; - error << "Unknown record: " << n.toString(); - throw std::runtime_error(error.str()); + throw std::runtime_error("Unknown record: " + n.toString()); } } else { RecordId id = it->second->load(esm); @@ -150,17 +207,44 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) continue; } - if (n.intval==ESM::REC_DIAL) { + if (n.toInt() == ESM::REC_DIAL) { dialogue = const_cast(mDialogs.find(id.mId)); } else { - dialogue = 0; + dialogue = nullptr; } } - listener->setProgress(static_cast(esm.getFileOffset() / (float)esm.getFileSize() * 1000)); + if (listener != nullptr) + listener->setProgress(::EsmLoader::fileProgress * esm.getFileOffset() / esm.getFileSize()); } } -void ESMStore::setUp(bool validateRecords) +ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const +{ + ESM::LuaScriptsCfg cfg; + for (const LuaContent& c : mLuaContent) + { + if (std::holds_alternative(c)) + { + // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. + // It is important for the `reloadlua` console command. + try + { + auto file = std::ifstream(std::get(c)); + std::string fileContent(std::istreambuf_iterator(file), {}); + LuaUtil::parseOMWScripts(cfg, fileContent); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + else + { + const ESM::LuaScriptsCfg& addition = std::get(c); + cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); + } + } + return cfg; +} + +void ESMStore::setUp() { mIds.clear(); @@ -179,32 +263,47 @@ void ESMStore::setUp(bool validateRecords) } if (mStaticIds.empty()) - mStaticIds = mIds; + for (const auto& [k, v] : mIds) + mStaticIds.emplace(Misc::StringUtils::lowerCase(k), v); mSkills.setUp(); mMagicEffects.setUp(); mAttributes.setUp(); mDialogs.setUp(); +} - if (validateRecords) - { - validate(); - countRecords(); - } +void ESMStore::validateRecords(ESM::ReadersCache& readers) +{ + validate(); + countAllCellRefs(readers); } -void ESMStore::countRecords() +void ESMStore::countAllCellRefs(ESM::ReadersCache& readers) { + // TODO: We currently need to read entire files here again. + // We should consider consolidating or deferring this reading. if(!mRefCount.empty()) return; - std::map refs; - std::vector readers; - for(auto it = mCells.intBegin(); it != mCells.intEnd(); it++) - readRefs(*it, refs, readers); - for(auto it = mCells.extBegin(); it != mCells.extEnd(); it++) - readRefs(*it, refs, readers); - for(const auto& pair : refs) - mRefCount[pair.second]++; + std::vector refs; + std::vector refIDs; + for(auto it = mCells.intBegin(); it != mCells.intEnd(); ++it) + readRefs(*it, refs, refIDs, readers); + for(auto it = mCells.extBegin(); it != mCells.extEnd(); ++it) + readRefs(*it, refs, refIDs, readers); + const auto lessByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; + std::stable_sort(refs.begin(), refs.end(), lessByRefNum); + const auto equalByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum == r.mRefNum; }; + const auto incrementRefCount = [&] (const Ref& value) + { + if (value.mRefID != deletedRefID) + { + std::string& refId = refIDs[value.mRefID]; + // We manually lower case IDs here for the time being to improve performance. + Misc::StringUtils::lowerCaseInPlace(refId); + ++mRefCount[std::move(refId)]; + } + }; + Misc::forEachUnique(refs.rbegin(), refs.rend(), equalByRefNum, incrementRefCount); } int ESMStore::getRefCount(const std::string& id) const @@ -218,49 +317,7 @@ int ESMStore::getRefCount(const std::string& id) const void ESMStore::validate() { - // Cache first class from store - we will use it if current class is not found - std::string defaultCls = ""; - Store::iterator it = mClasses.begin(); - if (it != mClasses.end()) - defaultCls = it->mId; - else - throw std::runtime_error("List of NPC classes is empty!"); - - // Validate NPCs for non-existing class and faction. - // We will replace invalid entries by fixed ones - std::vector npcsToReplace; - for (ESM::NPC npc : mNpcs) - { - bool changed = false; - - const std::string npcFaction = npc.mFaction; - if (!npcFaction.empty()) - { - const ESM::Faction *fact = mFactions.search(npcFaction); - if (!fact) - { - Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it."; - npc.mFaction.clear(); - npc.mNpdt.mRank = 0; - changed = true; - } - } - - std::string npcClass = npc.mClass; - if (!npcClass.empty()) - { - const ESM::Class *cls = mClasses.search(npcClass); - if (!cls) - { - Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement."; - npc.mClass = defaultCls; - changed = true; - } - } - - if (changed) - npcsToReplace.push_back(npc); - } + std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mStatic); for (const ESM::NPC &npc : npcsToReplace) { @@ -331,6 +388,41 @@ void ESMStore::validate() } } +void ESMStore::validateDynamic() +{ + std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic); + + for (const ESM::NPC &npc : npcsToReplace) + mNpcs.insert(npc); + + removeMissingScripts(mScripts, mArmors.mDynamic); + removeMissingScripts(mScripts, mBooks.mDynamic); + removeMissingScripts(mScripts, mClothes.mDynamic); + removeMissingScripts(mScripts, mWeapons.mDynamic); + + removeMissingObjects(mCreatureLists); + removeMissingObjects(mItemLists); +} + +// Leveled lists can be modified by scripts. This removes items that no longer exist (presumably because the plugin was removed) from modified lists +template +void ESMStore::removeMissingObjects(Store& store) +{ + for(auto& entry : store.mDynamic) + { + auto first = std::remove_if(entry.second.mList.begin(), entry.second.mList.end(), [&] (const auto& item) + { + if(!find(item.mId)) + { + Log(Debug::Verbose) << "Leveled list '" << entry.first << "' has nonexistent object '" << item.mId << "', ignoring it."; + return true; + } + return false; + }); + entry.second.mList.erase(first, entry.second.mList.end()); + } +} + int ESMStore::countSavedGameRecords() const { return 1 // DYNA (dynamic name counter) @@ -384,12 +476,14 @@ void ESMStore::validate() case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: - case ESM::REC_NPC_: case ESM::REC_LEVI: case ESM::REC_LEVC: + mStores[type]->read (reader); + return true; + case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: - mStores[type]->read (reader); + mStores[type]->read (reader, true); return true; case ESM::REC_DYNA: @@ -414,9 +508,8 @@ void ESMStore::validate() throw std::runtime_error ("Invalid player record (race or class unavailable"); } - std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const + std::pair, bool> ESMStore::getSpellList(const std::string& id) const { - const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 99db96ac10..defefccb9c 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -2,9 +2,10 @@ #define OPENMW_MWWORLD_ESMSTORE_H #include -#include #include +#include +#include #include #include "store.hpp" @@ -18,6 +19,11 @@ namespace MWMechanics class SpellList; } +namespace ESM +{ + class ReadersCache; +} + namespace MWWorld { class ESMStore @@ -73,24 +79,35 @@ namespace MWWorld // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. - std::map mIds; - std::map mStaticIds; + using IDMap = std::unordered_map; + IDMap mIds; + std::unordered_map mStaticIds; - std::map mRefCount; + std::unordered_map mRefCount; std::map mStores; - ESM::NPC mPlayerTemplate; - unsigned int mDynamicCount; - mutable std::map > mSpellListCache; + mutable std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> mSpellListCache; /// Validate entries in store after setup void validate(); - void countRecords(); + void countAllCellRefs(ESM::ReadersCache& readers); + + template + void removeMissingObjects(Store& store); + + using LuaContent = std::variant< + ESM::LuaScriptsCfg, // data from an omwaddon + std::string>; // path to an omwscripts file + std::vector mLuaContent; + public: + void addOMWScripts(std::string filePath) { mLuaContent.push_back(std::move(filePath)); } + ESM::LuaScriptsCfg getLuaScriptsCfg() const; + /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; @@ -103,10 +120,9 @@ namespace MWWorld } /// Look up the given ID in 'all'. Returns 0 if not found. - /// \note id must be in lower case. int find(const std::string &id) const { - std::map::const_iterator it = mIds.find(id); + IDMap::const_iterator it = mIds.find(id); if (it == mIds.end()) { return 0; } @@ -114,7 +130,7 @@ namespace MWWorld } int findStatic(const std::string &id) const { - std::map::const_iterator it = mStaticIds.find(id); + IDMap::const_iterator it = mStaticIds.find(id); if (it == mStaticIds.end()) { return 0; } @@ -172,17 +188,19 @@ namespace MWWorld for (std::map::iterator it = mStores.begin(); it != mStores.end(); ++it) it->second->clearDynamic(); - mNpcs.insert(mPlayerTemplate); + movePlayerRecord(); } void movePlayerRecord () { - mPlayerTemplate = *mNpcs.find("player"); - mNpcs.eraseStatic(mPlayerTemplate.mId); - mNpcs.insert(mPlayerTemplate); + auto player = mNpcs.find("player"); + mNpcs.insert(*player); } - void load(ESM::ESMReader &esm, Loading::Listener* listener); + /// Validate entries in store after loading a save + void validateDynamic(); + + void load(ESM::ESMReader &esm, Loading::Listener* listener, ESM::Dialogue*& dialogue); template const Store &get() const { @@ -196,7 +214,7 @@ namespace MWWorld const std::string id = "$dynamic" + std::to_string(mDynamicCount++); Store &store = const_cast &>(get()); - if (store.search(id) != 0) + if (store.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); @@ -234,7 +252,7 @@ namespace MWWorld const std::string id = "$dynamic" + std::to_string(mDynamicCount++); Store &store = const_cast &>(get()); - if (store.search(id) != 0) + if (store.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); @@ -252,7 +270,8 @@ namespace MWWorld // This method must be called once, after loading all master/plugin files. This can only be done // from the outside, so it must be public. - void setUp(bool validateRecords = false); + void setUp(); + void validateRecords(ESM::ReadersCache& readers); int countSavedGameRecords() const; @@ -286,7 +305,7 @@ namespace MWWorld { return mNpcs.insert(npc); } - else if (mNpcs.search(id) != 0) + else if (mNpcs.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); diff --git a/apps/openmw/mwworld/failedaction.cpp b/apps/openmw/mwworld/failedaction.cpp index 45df75a322..ec8314712e 100644 --- a/apps/openmw/mwworld/failedaction.cpp +++ b/apps/openmw/mwworld/failedaction.cpp @@ -1,5 +1,4 @@ #include "failedaction.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" diff --git a/apps/openmw/mwworld/globals.cpp b/apps/openmw/mwworld/globals.cpp index 8a481334e8..5b236996b3 100644 --- a/apps/openmw/mwworld/globals.cpp +++ b/apps/openmw/mwworld/globals.cpp @@ -2,30 +2,30 @@ #include -#include -#include +#include +#include #include #include "esmstore.hpp" namespace MWWorld { - Globals::Collection::const_iterator Globals::find (const std::string& name) const + Globals::Collection::const_iterator Globals::find (std::string_view name) const { Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); if (iter==mVariables.end()) - throw std::runtime_error ("unknown global variable: " + name); + throw std::runtime_error ("unknown global variable: " + std::string{name}); return iter; } - Globals::Collection::iterator Globals::find (const std::string& name) + Globals::Collection::iterator Globals::find (std::string_view name) { Collection::iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); if (iter==mVariables.end()) - throw std::runtime_error ("unknown global variable: " + name); + throw std::runtime_error ("unknown global variable: " + std::string{name}); return iter; } @@ -42,17 +42,17 @@ namespace MWWorld } } - const ESM::Variant& Globals::operator[] (const std::string& name) const + const ESM::Variant& Globals::operator[] (std::string_view name) const { return find (Misc::StringUtils::lowerCase (name))->second.mValue; } - ESM::Variant& Globals::operator[] (const std::string& name) + ESM::Variant& Globals::operator[] (std::string_view name) { return find (Misc::StringUtils::lowerCase (name))->second.mValue; } - char Globals::getType (const std::string& name) const + char Globals::getType (std::string_view name) const { Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); diff --git a/apps/openmw/mwworld/globals.hpp b/apps/openmw/mwworld/globals.hpp index 3468c2e719..ff9caadd73 100644 --- a/apps/openmw/mwworld/globals.hpp +++ b/apps/openmw/mwworld/globals.hpp @@ -5,10 +5,9 @@ #include #include -#include +#include -#include -#include +#include namespace ESM { @@ -33,17 +32,17 @@ namespace MWWorld Collection mVariables; // type, value - Collection::const_iterator find (const std::string& name) const; + Collection::const_iterator find (std::string_view name) const; - Collection::iterator find (const std::string& name); + Collection::iterator find (std::string_view name); public: - const ESM::Variant& operator[] (const std::string& name) const; + const ESM::Variant& operator[] (std::string_view name) const; - ESM::Variant& operator[] (const std::string& name); + ESM::Variant& operator[] (std::string_view name); - char getType (const std::string& name) const; + char getType (std::string_view name) const; ///< If there is no global variable with this name, ' ' is returned. void fill (const MWWorld::ESMStore& store); diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp new file mode 100644 index 0000000000..0c3b016482 --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -0,0 +1,75 @@ +#include "groundcoverstore.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "store.hpp" + +namespace MWWorld +{ + void GroundcoverStore::init(const Store& statics, const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) + { + ::EsmLoader::Query query; + query.mLoadStatics = true; + query.mLoadCells = true; + + ESM::ReadersCache readers; + const ::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\\"; + for (const ESM::Static& stat : statics) + { + std::string id = Misc::StringUtils::lowerCase(stat.mId); + std::string model = Misc::StringUtils::lowerCase(stat.mModel); + std::replace(model.begin(), model.end(), '/', '\\'); + if (model.compare(0, prefix.size(), prefix) != 0) + continue; + mMeshCache[id] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + } + + for (const ESM::Static& stat : content.mStatics) + { + std::string id = Misc::StringUtils::lowerCase(stat.mId); + std::string model = Misc::StringUtils::lowerCase(stat.mModel); + std::replace(model.begin(), model.end(), '/', '\\'); + if (model.compare(0, prefix.size(), prefix) != 0) + continue; + mMeshCache[id] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + } + + for (const ESM::Cell& cell : content.mCells) + { + if (!cell.isExterior()) continue; + auto cellIndex = std::make_pair(cell.getCellId().mIndex.mX, cell.getCellId().mIndex.mY); + mCellContexts[cellIndex] = std::move(cell.mContextList); + } + } + + std::string GroundcoverStore::getGroundcoverModel(const std::string& id) const + { + std::string idLower = Misc::StringUtils::lowerCase(id); + auto search = mMeshCache.find(idLower); + if (search == mMeshCache.end()) return std::string(); + + return search->second; + } + + void GroundcoverStore::initCell(ESM::Cell& cell, int cellX, int cellY) const + { + cell.blank(); + + auto searchCell = mCellContexts.find(std::make_pair(cellX, cellY)); + if (searchCell != mCellContexts.end()) + cell.mContextList = searchCell->second; + } +} diff --git a/apps/openmw/mwworld/groundcoverstore.hpp b/apps/openmw/mwworld/groundcoverstore.hpp new file mode 100644 index 0000000000..d955f731fc --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.hpp @@ -0,0 +1,51 @@ +#ifndef GAME_MWWORLD_GROUNDCOVER_STORE_H +#define GAME_MWWORLD_GROUNDCOVER_STORE_H + +#include +#include +#include + +namespace ESM +{ + struct ESM_Context; + struct Static; + struct Cell; +} + +namespace Loading +{ + class Listener; +} + +namespace Files +{ + class Collections; +} + +namespace ToUTF8 +{ + class Utf8Encoder; +} + +namespace MWWorld +{ + template + class Store; + + class GroundcoverStore + { + private: + std::map mMeshCache; + std::map, std::vector> mCellContexts; + + public: + void init(const Store& statics, const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, + Loading::Listener* listener); + + std::string getGroundcoverModel(const std::string& id) const; + void initCell(ESM::Cell& cell, int cellX, int cellY) const; + }; +} + +#endif diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 386bbb30a4..27a21e3362 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -4,8 +4,8 @@ #include #include -#include -#include +#include +#include #include #include "../mwbase/environment.hpp" @@ -108,11 +108,9 @@ MWWorld::InventoryStore::InventoryStore() MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) : ContainerStore (store) - , mMagicEffects(store.mMagicEffects) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) - , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) , mSelectedEnchantItem(end()) { copySlots (store); @@ -120,11 +118,12 @@ MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStore& store) { + if (this == &store) + return *this; + mListener = store.mListener; mInventoryListener = store.mInventoryListener; - mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); @@ -140,8 +139,8 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) { - std::string type = itemPtr.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) + auto type = itemPtr.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(actorPtr); } @@ -183,8 +182,6 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) @@ -196,7 +193,6 @@ void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) mUpdatesEnabled = true; fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) @@ -435,7 +431,7 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& if (iter.getType() == ContainerStore::Type_Armor) { - if (old.getTypeName() == typeid(ESM::Armor).name()) + if (old.getType() == ESM::Armor::sRecordId) { if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) continue; @@ -469,7 +465,7 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& } } - if (old.getTypeName() == typeid(ESM::Clothing).name()) + if (old.getType() == ESM::Clothing::sRecordId) { // check value if (old.getClass().getValue (old) >= test.getClass().getValue (test)) @@ -551,132 +547,16 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { mSlots.swap (slots_); fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); flagAsModified(); } } -const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(const MWWorld::Ptr& actor) { - return mMagicEffects; -} - -void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) -{ - // To avoid excessive updates during auto-equip - if (!mUpdatesEnabled) - return; - - // Delay update until the listener is set up - if (!mInventoryListener) - return; - - mMagicEffects = MWMechanics::MagicEffects(); - - const auto& stats = actor.getClass().getCreatureStats(actor); - if (stats.isDead() && stats.isDeathAnimationFinished()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector params; - - bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); - if (!existed) - { - params.resize(enchantment.mEffects.mList.size()); - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - int delta = effect.mMagnMax - effect.mMagnMin; - // Roll some dice, one for each effect - if (delta) - params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - // Try resisting each effect - params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); - ++i; - } - - // Note that using the RefID as a key here is not entirely correct. - // Consider equipping the same item twice (e.g. a ring) - // However, permanent enchantments with a random magnitude are kind of an exploit anyway, - // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. - mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; - } - else - params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effect.mEffectID); - - // Fully resisted or can't be applied to target? - if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) - { - i++; - continue; - } - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (!existed) - { - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); - } - - if (magnitude) - mMagicEffects.add (effect, magnitude); - - i++; - } - } - } - - // Now drop expired effects - for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end();) - { - bool found = false; - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter == end()) - continue; - if ((**iter).getCellRef().getRefId() == it->first) - { - found = true; - } - } - if (!found) - mPermanentMagicEffectMagnitudes.erase(it++); - else - ++it; - } - - // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); - - mFirstAutoEquip = false; + TSlots slots; + initSlots (slots); + autoEquipArmor(actor, slots); + return slots[Slot_CarriedLeft]; } bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const @@ -737,8 +617,8 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { - std::string type = item.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) + auto type = item.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(actor); } @@ -754,7 +634,7 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor return retCount; } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, const MWWorld::Ptr& actor, bool fireEvent) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, const MWWorld::Ptr& actor, bool applyUpdates) { if (slot<0 || slot>=static_cast (mSlots.size())) throw std::runtime_error ("slot number out of range"); @@ -786,10 +666,10 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c } } - if (fireEvent) + if (applyUpdates) + { fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); + } return retval; } @@ -836,7 +716,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con return unstack(item, actor, item.getRefData().getCount() - count); } -MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() +MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } @@ -844,7 +724,6 @@ MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) { mInventoryListener = listener; - updateMagicEffects(actor); } void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) @@ -863,105 +742,6 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) */ } -void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) -{ - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - if (enchantmentId.empty()) - continue; - - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) - continue; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - i++; - // Don't get spell icon display information for enchantments that weren't actually applied - if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) - continue; - const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; - magnitude *= params.mMultiplier; - if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); - } - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) -{ - for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) - { - if (*it != end()) - purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) -{ - TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); - if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - if ((*iter)->getCellRef().getRefId() != sourceId) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector& params = effectMagnitudeIt->second; - - int i=0; - for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) - { - if (effectIt->mEffectID != effectId) - continue; - - if (effectIndex >= 0 && effectIndex != i) - continue; - - if (wholeSpell) - { - mPermanentMagicEffectMagnitudes.erase(sourceId); - return; - } - - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (magnitude) - mMagicEffects.add (*effectIt, -magnitude); - - params[i].mMultiplier = 0; - } - } - } -} - void MWWorld::InventoryStore::clear() { mSlots.clear(); @@ -979,38 +759,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) return false; } -void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const -{ - MWWorld::ContainerStore::writeState(state); - - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector > params; - for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - params.emplace_back(pIt->mRandom, pIt->mMultiplier); - } - - state.mPermanentMagicEffectMagnitudes[it->first] = params; - } -} - -void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) +bool MWWorld::InventoryStore::isFirstEquip() { - MWWorld::ContainerStore::readState(state); - - for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); - it != state.mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector params; - for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - EffectParams p; - p.mRandom = pIt->first; - p.mMultiplier = pIt->second; - params.push_back(p); - } - - mPermanentMagicEffectMagnitudes[it->first] = params; - } + bool first = mFirstAutoEquip; + mFirstAutoEquip = false; + return first; } diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index e70c214809..01c53d7028 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -25,15 +25,6 @@ namespace MWWorld */ virtual void equipmentChanged () {} - /** - * @param effect - * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) - * or was it loaded from a savegame / initial game state? \n - * If it isn't new, non-looping VFX should not be played. - * @param playSound Play effect sound? - */ - virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} - virtual ~InventoryStoreListener() = default; }; @@ -42,34 +33,32 @@ namespace MWWorld { public: - static const int Slot_Helmet = 0; - static const int Slot_Cuirass = 1; - static const int Slot_Greaves = 2; - static const int Slot_LeftPauldron = 3; - static const int Slot_RightPauldron = 4; - static const int Slot_LeftGauntlet = 5; - static const int Slot_RightGauntlet = 6; - static const int Slot_Boots = 7; - static const int Slot_Shirt = 8; - static const int Slot_Pants = 9; - static const int Slot_Skirt = 10; - static const int Slot_Robe = 11; - static const int Slot_LeftRing = 12; - static const int Slot_RightRing = 13; - static const int Slot_Amulet = 14; - static const int Slot_Belt = 15; - static const int Slot_CarriedRight = 16; - static const int Slot_CarriedLeft = 17; - static const int Slot_Ammunition = 18; - - static const int Slots = 19; - - static const int Slot_NoSlot = -1; + static constexpr int Slot_Helmet = 0; + static constexpr int Slot_Cuirass = 1; + static constexpr int Slot_Greaves = 2; + static constexpr int Slot_LeftPauldron = 3; + static constexpr int Slot_RightPauldron = 4; + static constexpr int Slot_LeftGauntlet = 5; + static constexpr int Slot_RightGauntlet = 6; + static constexpr int Slot_Boots = 7; + static constexpr int Slot_Shirt = 8; + static constexpr int Slot_Pants = 9; + static constexpr int Slot_Skirt = 10; + static constexpr int Slot_Robe = 11; + static constexpr int Slot_LeftRing = 12; + static constexpr int Slot_RightRing = 13; + static constexpr int Slot_Amulet = 14; + static constexpr int Slot_Belt = 15; + static constexpr int Slot_CarriedRight = 16; + static constexpr int Slot_CarriedLeft = 17; + static constexpr int Slot_Ammunition = 18; + + static constexpr int Slots = 19; + + static constexpr int Slot_NoSlot = -1; private: - MWMechanics::MagicEffects mMagicEffects; - InventoryStoreListener* mInventoryListener; // Enables updates of magic effects and actor model whenever items are equipped or unequipped. @@ -78,19 +67,6 @@ namespace MWWorld bool mFirstAutoEquip; - // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. - // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. - struct EffectParams - { - // Modifier to scale between min and max magnitude - float mRandom; - // Multiplier for when an effect was fully or partially resisted - float mMultiplier; - }; - - typedef std::map > TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; - typedef std::vector TSlots; TSlots mSlots; @@ -106,8 +82,6 @@ namespace MWWorld void initSlots (TSlots& slots_); - void updateMagicEffects(const Ptr& actor); - void fireEquipmentChangedEvent(const Ptr& actor); void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; @@ -123,7 +97,7 @@ namespace MWWorld InventoryStore& operator= (const InventoryStore& store); - InventoryStore* clone() override { return new InventoryStore(*this); } + std::unique_ptr clone() override { return std::make_unique(*this); } ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true, bool resolve = true) override; ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) @@ -153,15 +127,14 @@ namespace MWWorld ContainerStoreIterator getSlot (int slot); ConstContainerStoreIterator getSlot(int slot) const; + ContainerStoreIterator getPreferredShield(const MWWorld::Ptr& actor); + void unequipAll(const MWWorld::Ptr& actor); ///< Unequip all currently equipped items. void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. - const MWMechanics::MagicEffects& getMagicEffects() const; - ///< Return magic effects from worn items. - bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; ///< @return true if the two specified objects can stack with each other @@ -171,7 +144,7 @@ namespace MWWorld /// /// @return the number of items actually removed - ContainerStoreIterator unequipSlot(int slot, const Ptr& actor, bool fireEvent=true); + ContainerStoreIterator unequipSlot(int slot, const Ptr& actor, bool applyUpdates = true); ///< Unequip \a slot. /// /// @return an iterator to the item that was previously in the slot @@ -196,22 +169,12 @@ namespace MWWorld void setInvListener (InventoryStoreListener* listener, const Ptr& actor); ///< Set a listener for various events, see \a InventoryStoreListener - InventoryStoreListener* getInvListener(); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); - - void purgeEffect (short effectId, bool wholeSpell = false); - ///< Remove a magic effect - - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); - ///< Remove a magic effect + InventoryStoreListener* getInvListener() const; void clear() override; ///< Empty container. - void writeState (ESM::InventoryState& state) const override; - - void readState (const ESM::InventoryState& state) override; + bool isFirstEquip(); }; } diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 9cf8a0fe04..53c936564b 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -1,16 +1,19 @@ #include "livecellref.hpp" +#include + #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "ptr.hpp" #include "class.hpp" #include "esmstore.hpp" -MWWorld::LiveCellRefBase::LiveCellRefBase(const std::string& type, const ESM::CellRef &cref) +MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef &cref) : mClass(&Class::get(type)), mRef(cref), mData(cref) { } @@ -52,6 +55,8 @@ void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; mRef.setSoul(std::string()); } + + MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); } void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const @@ -61,6 +66,7 @@ void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const ConstPtr ptr (this); mData.write (state, mClass->getScript (ptr)); + MWBase::Environment::get().getLuaManager()->saveLocalScripts(Ptr(const_cast(this)), state.mLuaScripts); mClass->writeAdditionalState (ptr, state); } @@ -69,3 +75,25 @@ bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) { return true; } + +unsigned int MWWorld::LiveCellRefBase::getType() const +{ + return mClass->getType(); +} + +namespace MWWorld +{ + std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) + { + std::stringstream message; + + message << "Bad LiveCellRef cast to " << recordType << " from "; + + if (value != nullptr) + message << value->getTypeDescription(); + else + message << "an empty object"; + + return message.str(); + } +} diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 414fde42bd..bee6f2ba57 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -1,12 +1,12 @@ #ifndef GAME_MWWORLD_LIVECELLREF_H #define GAME_MWWORLD_LIVECELLREF_H -#include - #include "cellref.hpp" #include "refdata.hpp" +#include + namespace ESM { struct ObjectState; @@ -18,6 +18,9 @@ namespace MWWorld class ESMStore; class Class; + template + struct LiveCellRef; + /// Used to create pointers to hold any type of LiveCellRef<> object. struct LiveCellRefBase { @@ -31,7 +34,7 @@ namespace MWWorld /** runtime-data */ RefData mData; - LiveCellRefBase(const std::string& type, const ESM::CellRef &cref=ESM::CellRef()); + LiveCellRefBase(unsigned int type, const ESM::CellRef &cref=ESM::CellRef()); /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase() { } @@ -43,6 +46,17 @@ namespace MWWorld virtual void save (ESM::ObjectState& state) const = 0; ///< Save LiveCellRef state into \a state. + virtual std::string_view getTypeDescription() const = 0; + + unsigned int getType() const; + ///< @see MWWorld::Class::getType + + template + static const LiveCellRef* dynamicCast(const LiveCellRefBase* value); + + template + static LiveCellRef* dynamicCast(LiveCellRefBase* value); + protected: void loadImp (const ESM::ObjectState& state); @@ -67,6 +81,24 @@ namespace MWWorld return cellRef.mRef.getRefNum()==refNum; } + std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType); + + template + const LiveCellRef* LiveCellRefBase::dynamicCast(const LiveCellRefBase* value) + { + if (const LiveCellRef* ref = dynamic_cast*>(value)) + return ref; + throw std::runtime_error(makeDynamicCastErrorMessage(value, T::getRecordType())); + } + + template + LiveCellRef* LiveCellRefBase::dynamicCast(LiveCellRefBase* value) + { + if (LiveCellRef* ref = dynamic_cast*>(value)) + return ref; + throw std::runtime_error(makeDynamicCastErrorMessage(value, T::getRecordType())); + } + /// A reference to one object (of any type) in a cell. /// /// Constructing this with a CellRef instance in the constructor means that @@ -77,11 +109,11 @@ namespace MWWorld struct LiveCellRef : public LiveCellRefBase { LiveCellRef(const ESM::CellRef& cref, const X* b = nullptr) - : LiveCellRefBase(typeid(X).name(), cref), mBase(b) + : LiveCellRefBase(X::sRecordId, cref), mBase(b) {} LiveCellRef(const X* b = nullptr) - : LiveCellRefBase(typeid(X).name()), mBase(b) + : LiveCellRefBase(X::sRecordId), mBase(b) {} // The object that this instance is based on. @@ -95,6 +127,8 @@ namespace MWWorld void save (ESM::ObjectState& state) const override; ///< Save LiveCellRef state into \a state. + std::string_view getTypeDescription() const override { return X::getRecordType(); } + static bool checkState (const ESM::ObjectState& state); ///< Check if state is valid and report errors. /// @@ -120,7 +154,6 @@ namespace MWWorld { return checkStateImp (state); } - } #endif diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 42914d4ac0..0e7dd0771f 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -43,7 +43,7 @@ namespace bool operator()(const MWWorld::Ptr& containerPtr) { // Ignore containers without generated content - if (containerPtr.getTypeName() == typeid(ESM::Container).name() && + if (containerPtr.getType() == ESM::Container::sRecordId && containerPtr.getRefData().getCustomData() == nullptr) return true; @@ -76,7 +76,7 @@ void MWWorld::LocalScripts::startIteration() bool MWWorld::LocalScripts::getNext(std::pair& script) { - while (mIter!=mScripts.end()) + if (mIter!=mScripts.end()) { std::list >::iterator iter = mIter++; script = *iter; diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp new file mode 100644 index 0000000000..a99862d489 --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.cpp @@ -0,0 +1,225 @@ +#include "magiceffects.hpp" +#include "esmstore.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/magiceffects.hpp" + +namespace +{ + template + void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName) + { + const T* item = MWBase::Environment::get().getWorld()->getStore().get().search(id); + if(item) + { + enchantment = item->mEnchant; + itemName = item->mName; + } + } +} + +namespace MWWorld +{ + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) + { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + // Convert corprus to format 10 + for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell) + continue; + + ESM::CreatureStats::CorprusStats stats; + stats.mNextWorsening = oldStats.mNextWorsening; + for (int i=0; imEffects.mList) + { + if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + } + creatureStats.mCorprusSpells[id] = stats; + } + // Convert to format 17 + for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) + { + const ESM::Spell* spell = store.get().search(id); + 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.mDisplayName = spell->mName; + params.mItem.unset(); + params.mCasterActorId = creatureStats.mActorId; + if(spell->mData.mType == ESM::Spell::ST_Ability) + params.mType = ESM::ActiveSpells::Type_Ability; + else + params.mType = ESM::ActiveSpells::Type_Permanent; + 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()) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = effectIndex; + auto rand = oldParams.mEffectRands.find(effectIndex); + if(rand != oldParams.mEffectRands.end()) + { + float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + // 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; + } + else + { + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mFlags = ESM::ActiveEffect::Flag_None; + } + params.mEffects.emplace_back(effect); + } + effectIndex++; + } + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + std::multimap equippedItems; + for(std::size_t i = 0; i < inventory.mItems.size(); ++i) + { + const ESM::ObjectState& item = inventory.mItems[i]; + auto slot = inventory.mEquipmentSlots.find(i); + if(slot != inventory.mEquipmentSlots.end()) + equippedItems.emplace(item.mRef.mRefID, slot->second); + } + for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) + { + std::string eId; + std::string name; + switch(store.find(id)) + { + case ESM::REC_ARMO: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_CLOT: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_WEAP: + getEnchantedItem(id, eId, name); + break; + } + if(eId.empty()) + continue; + const ESM::Enchantment* enchantment = store.get().search(eId); + if(!enchantment) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = name; + params.mCasterActorId = creatureStats.mActorId; + params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mWorsenings = -1; + params.mNextWorsening = ESM::TimeStamp(); + for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + { + const auto& enam = enchantment->mEffects.mList[effectIndex]; + auto [random, multiplier] = oldMagnitudes[effectIndex]; + float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + magnitude *= multiplier; + if(magnitude <= 0) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = static_cast(effectIndex); + // 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; + params.mEffects.emplace_back(effect); + } + auto [begin, end] = equippedItems.equal_range(id); + for(auto it = begin; it != end; ++it) + { + params.mItem = { static_cast(it->second), 0 }; + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + } + for(const auto& spell : creatureStats.mCorprusSpells) + { + auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; }); + if(it != creatureStats.mActiveSpells.mSpells.end()) + { + it->mNextWorsening = spell.second.mNextWorsening; + int worsenings = 0; + for(int i = 0; i < ESM::Attribute::Length; ++i) + worsenings = std::max(spell.second.mWorsenings[i], worsenings); + it->mWorsenings = worsenings; + } + } + for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) + { + if(actorId == -1) + continue; + for(auto& params : creatureStats.mActiveSpells.mSpells) + { + if(params.mId == key.mSourceId) + { + bool found = false; + for(auto& effect : params.mEffects) + { + if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) + { + effect.mArg = actorId; + found = true; + break; + } + } + if(found) + break; + } + } + } + // Reset modifiers that were previously recalculated each frame + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + creatureStats.mAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < 3; ++i) + { + auto& dynamic = creatureStats.mDynamic[i]; + dynamic.mCurrent -= dynamic.mMod - dynamic.mBase; + dynamic.mMod = 0.f; + } + for(std::size_t i = 0; i < 4; ++i) + creatureStats.mAiSettings[i].mMod = 0.f; + if(npcStats) + { + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + npcStats->mSkills[i].mMod = 0.f; + } + } + + // Versions 17-19 wrote different modifiers to the savegame depending on whether the save had upgraded from a pre-17 version or not + void convertStats(ESM::CreatureStats& creatureStats) + { + for(std::size_t i = 0; i < 3; ++i) + creatureStats.mDynamic[i].mMod = 0.f; + for(std::size_t i = 0; i < 4; ++i) + creatureStats.mAiSettings[i].mMod = 0.f; + } +} diff --git a/apps/openmw/mwworld/magiceffects.hpp b/apps/openmw/mwworld/magiceffects.hpp new file mode 100644 index 0000000000..fdcc578e53 --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_MWWORLD_MAGICEFFECTS_H +#define OPENMW_MWWORLD_MAGICEFFECTS_H + +namespace ESM +{ + struct CreatureStats; + struct InventoryState; + struct NpcStats; +} + +namespace MWWorld +{ + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, + ESM::NpcStats* npcStats = nullptr); + + void convertStats(ESM::CreatureStats& creatureStats); +} + +#endif diff --git a/apps/openmw/mwworld/manualref.cpp b/apps/openmw/mwworld/manualref.cpp index 48270d224d..b809a81b3e 100644 --- a/apps/openmw/mwworld/manualref.cpp +++ b/apps/openmw/mwworld/manualref.cpp @@ -6,31 +6,22 @@ namespace { template - void create(const MWWorld::Store& list, const std::string& name, boost::any& refValue, MWWorld::Ptr& ptrValue) + void create(const MWWorld::Store& list, const std::string& name, std::any& refValue, MWWorld::Ptr& ptrValue) { const T* base = list.find(name); ESM::CellRef cellRef; - cellRef.mRefNum.unset(); + cellRef.blank(); cellRef.mRefID = name; - cellRef.mScale = 1; - cellRef.mFactionRank = 0; - cellRef.mChargeInt = -1; - cellRef.mChargeIntRemainder = 0.0f; - cellRef.mGoldValue = 1; - cellRef.mEnchantmentCharge = -1; - cellRef.mTeleport = false; - cellRef.mLockLevel = 0; - cellRef.mReferenceBlocked = 0; MWWorld::LiveCellRef ref(cellRef, base); refValue = ref; - ptrValue = MWWorld::Ptr(&boost::any_cast&>(refValue), 0); + ptrValue = MWWorld::Ptr(&std::any_cast&>(refValue), nullptr); } } -MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count) +MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, std::string_view name, const int count) { std::string lowerName = Misc::StringUtils::lowerCase(name); switch (store.find(lowerName)) diff --git a/apps/openmw/mwworld/manualref.hpp b/apps/openmw/mwworld/manualref.hpp index 2fc5994710..593df9f24f 100644 --- a/apps/openmw/mwworld/manualref.hpp +++ b/apps/openmw/mwworld/manualref.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWWORLD_MANUALREF_H #define GAME_MWWORLD_MANUALREF_H -#include +#include #include "ptr.hpp" @@ -10,14 +10,14 @@ namespace MWWorld /// \brief Manually constructed live cell ref class ManualRef { - boost::any mRef; + std::any mRef; Ptr mPtr; ManualRef (const ManualRef&); ManualRef& operator= (const ManualRef&); public: - ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count = 1); + ManualRef(const MWWorld::ESMStore& store, std::string_view name, const int count = 1); const Ptr& getPtr() const { diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 66ae4e3194..9c5f226bc6 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -4,14 +4,16 @@ #include -#include -#include -#include +#include +#include +#include #include -#include +#include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/magiceffects.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -22,14 +24,17 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" +#include "../mwrender/renderingmanager.hpp" +#include "../mwrender/camera.hpp" + +#include "cellstore.hpp" #include "class.hpp" #include "ptr.hpp" -#include "cellstore.hpp" namespace MWWorld { Player::Player (const ESM::NPC *player) - : mCellStore(0), + : mCellStore(nullptr), mLastKnownExteriorPosition(0,0,0), mMarkedPosition(ESM::Position()), mMarkedCell(nullptr), @@ -38,7 +43,6 @@ namespace MWWorld mTeleported(false), mCurrentCrimeId(-1), mPaidCrimeId(-1), - mAttackingOrSpell(false), mJumping(false) { ESM::CellRef cellRef; @@ -56,9 +60,9 @@ namespace MWWorld MWMechanics::NpcStats& stats = getPlayer().getClass().getNpcStats(getPlayer()); for (int i=0; i health = creatureStats.getDynamic(0); - creatureStats.setHealth(int(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat())); + creatureStats.setHealth(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat()); for (int i=0; i health = creatureStats.getDynamic(0); - creatureStats.setHealth(int(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat())); + creatureStats.setHealth(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat()); for(size_t i = 0;i < ESM::Attribute::Length;++i) { // Oh, Bethesda. It's "Intelligence". @@ -88,7 +101,7 @@ namespace MWWorld ESM::Attribute::sAttributeNames[i]); MWMechanics::AttributeValue value = npcStats.getAttribute(i); - value.setBase(int(gmst.find(name)->mValue.getFloat())); + value.setModifier(gmst.find(name)->mValue.getFloat() - value.getModified()); npcStats.setAttribute(i, value); } @@ -102,9 +115,8 @@ namespace MWWorld std::string name = "fWerewolf"+((i==ESM::Skill::Mercantile) ? std::string("Merchantile") : ESM::Skill::sSkillNames[i]); - MWMechanics::SkillValue value = npcStats.getSkill(i); - value.setBase(int(gmst.find(name)->mValue.getFloat())); - npcStats.setSkill(i, value); + MWMechanics::SkillValue& value = npcStats.getSkill(i); + value.setModifier(gmst.find(name)->mValue.getFloat() - value.getModified()); } } @@ -140,7 +152,7 @@ namespace MWWorld return mSign; } - void Player::setDrawState (MWMechanics::DrawState_ state) + void Player::setDrawState (MWMechanics::DrawState state) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getNpcStats(ptr).setDrawState (state); @@ -217,7 +229,7 @@ namespace MWWorld ptr.getClass().getMovementSettings(ptr).mRotation[1] += roll; } - MWMechanics::DrawState_ Player::getDrawState() + MWMechanics::DrawState Player::getDrawState() { MWWorld::Ptr ptr = getPlayer(); return ptr.getClass().getNpcStats(ptr).getDrawState(); @@ -257,12 +269,7 @@ namespace MWWorld void Player::setAttackingOrSpell(bool attackingOrSpell) { - mAttackingOrSpell = attackingOrSpell; - } - - bool Player::getAttackingOrSpell() const - { - return mAttackingOrSpell; + getPlayer().getClass().getCreatureStats(getPlayer()).setAttackingOrSpell(attackingOrSpell); } void Player::setJumping(bool jumping) @@ -299,13 +306,12 @@ namespace MWWorld void Player::clear() { - mCellStore = 0; + mCellStore = nullptr; mSign.clear(); - mMarkedCell = 0; + mMarkedCell = nullptr; mAutoMove = false; mForwardBackward = 0; mTeleported = false; - mAttackingOrSpell = false; mJumping = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; @@ -314,14 +320,12 @@ namespace MWWorld for (int i=0; iapplyWerewolfAcrobatics(getPlayer()); + } } getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear(); @@ -448,7 +465,7 @@ namespace MWWorld } else { - mMarkedCell = 0; + mMarkedCell = nullptr; } mForwardBackward = 0; @@ -501,4 +518,56 @@ namespace MWWorld MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, castChance); MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } + + void Player::update() + { + auto player = getPlayer(); + const auto world = MWBase::Environment::get().getWorld(); + const auto rendering = world->getRenderingManager(); + auto& store = world->getStore(); + auto& playerClass = player.getClass(); + const auto windowMgr = MWBase::Environment::get().getWindowManager(); + + if (player.getCell()->isExterior()) + { + ESM::Position pos = player.getRefData().getPosition(); + setLastKnownExteriorPosition(pos.asVec3()); + } + + bool isWerewolf = playerClass.getNpcStats(player).isWerewolf(); + bool isFirstPerson = world->isFirstPerson(); + if (isWerewolf && isFirstPerson) + { + float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV"); + if (werewolfFov != 0) + rendering->overrideFieldOfView(werewolfFov); + windowMgr->setWerewolfOverlay(true); + } + else + { + rendering->resetFieldOfView(); + windowMgr->setWerewolfOverlay(false); + } + + // Sink the camera while sneaking + bool sneaking = playerClass.getCreatureStats(player).getStance(MWMechanics::CreatureStats::Stance_Sneak); + bool swimming = world->isSwimming(player); + bool flying = world->isFlying(player); + + static const float i1stPersonSneakDelta = store.get().find("i1stPersonSneakDelta")->mValue.getFloat(); + if (sneaking && !swimming && !flying) + rendering->getCamera()->setSneakOffset(i1stPersonSneakDelta); + else + rendering->getCamera()->setSneakOffset(0.f); + + int blind = 0; + const auto& magicEffects = playerClass.getCreatureStats(player).getMagicEffects(); + if (!world->getGodModeState()) + blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); + windowMgr->setBlindness(std::clamp(blind, 0, 100)); + + int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); + rendering->setNightEyeFactor(std::min(1.f, (nightEye / 100.f))); + } + } diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index 1e4b0ffdf5..54d9c08da4 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -9,12 +9,12 @@ #include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/stat.hpp" -#include +#include #include +#include namespace ESM { - struct NPC; class ESMWriter; class ESMReader; } @@ -53,10 +53,9 @@ namespace MWWorld PreviousItems mPreviousItems; // Saved stats prior to becoming a werewolf - MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length]; - MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length]; + float mSaveSkills[ESM::Skill::Length]; + float mSaveAttributes[ESM::Attribute::Length]; - bool mAttackingOrSpell; bool mJumping; public: @@ -87,8 +86,8 @@ namespace MWWorld void setBirthSign(const std::string &sign); const std::string &getBirthSign() const; - void setDrawState (MWMechanics::DrawState_ state); - MWMechanics::DrawState_ getDrawState(); /// \todo constness + void setDrawState (MWMechanics::DrawState state); + MWMechanics::DrawState getDrawState(); /// \todo constness /// Activate the object under the crosshair, if any void activate(); @@ -112,7 +111,6 @@ namespace MWWorld void setTeleported(bool teleported); void setAttackingOrSpell(bool attackingOrSpell); - bool getAttackingOrSpell() const; void setJumping(bool jumping); bool getJumping() const; @@ -137,6 +135,8 @@ namespace MWWorld void erasePreviousItem(const std::string& boundItemId); void setSelectedSpell(const std::string& spellId); + + void update(); }; } #endif diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 39cdc3e722..3894569682 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -1,16 +1,20 @@ #include "projectilemanager.hpp" #include +#include +#include +#include #include #include -#include -#include +#include +#include #include #include +#include #include #include @@ -18,6 +22,9 @@ #include #include #include +#include + +#include #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" @@ -27,6 +34,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -43,6 +51,7 @@ #include "../mwsound/sound.hpp" #include "../mwphysics/physicssystem.hpp" +#include "../mwphysics/projectile.hpp" namespace { @@ -158,7 +167,7 @@ namespace MWWorld } /// Rotates an osg::PositionAttitudeTransform over time. - class RotateCallback : public osg::NodeCallback + class RotateCallback : public SceneUtil::NodeCallback { public: RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2) @@ -167,14 +176,12 @@ namespace MWWorld { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::PositionAttitudeTransform* node, osg::NodeVisitor* nv) { - osg::PositionAttitudeTransform* transform = static_cast(node); - double time = nv->getFrameStamp()->getSimulationTime(); osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis); - transform->setAttitude(orient); + node->setAttitude(orient); traverse(node, nv); } @@ -206,6 +213,9 @@ namespace MWWorld osg::ref_ptr projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo); 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; @@ -214,8 +224,10 @@ namespace MWWorld SceneUtil::FindByNameVisitor findVisitor(nodeName.str()); attachTo->accept(findVisitor); if (findVisitor.mFoundNode) - mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode); + mResourceSystem->getSceneManager()->getInstance( + Misc::ResourceHelpers::correctMeshPath(weapon->mModel, vfs), findVisitor.mFoundNode); } + } if (createLight) { @@ -235,15 +247,12 @@ namespace MWWorld state.mNode->addChild(projectileLightSource); projectileLightSource->setLight(projectileLight); } - - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - state.mNode->accept(disableFreezeOnCullVisitor); state.mNode->addCullCallback(new SceneUtil::LightListCallback); mParent->addChild(state.mNode); - state.mEffectAnimationTime.reset(new MWRender::EffectAnimationTime); + state.mEffectAnimationTime = std::make_shared(); SceneUtil::AssignControllerSourcesVisitor assignVisitor (state.mEffectAnimationTime); state.mNode->accept(assignVisitor); @@ -256,14 +265,13 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection, int slot) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) { - // Spawn at 0.75 * ActorHeight // Note: we ignore the collision box offset, this is required to make some flying creatures work as intended. - pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * 0.75; + pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight; } if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible @@ -279,12 +287,13 @@ namespace MWWorld MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; + state.mSlot = slot; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else state.mActorId = -1; - std::string texture = ""; + std::string texture; state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); @@ -302,7 +311,9 @@ namespace MWWorld MWWorld::Ptr ptr = ref.getPtr(); osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); - createModel(state, ptr.getClass().getModel(ptr), pos, orient, true, true, lightDiffuseColor, texture); + + auto model = ptr.getClass().getModel(ptr); + createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) @@ -312,11 +323,20 @@ namespace MWWorld if (sound) state.mSounds.push_back(sound); } - + + // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape + if (state.mIdMagic.size() > 1) + { + model = Misc::ResourceHelpers::correctMeshPath( + MWBase::Environment::get().getWorld()->getStore().get().find(state.mIdMagic[1])->mModel, + MWBase::Environment::get().getResourceSystem()->getVFS()); + } + state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); + state.mToDelete = false; mMagicBolts.push_back(state); } - void ProjectileManager::launchProjectile(Ptr actor, ConstPtr projectile, const osg::Vec3f &pos, const osg::Quat &orient, Ptr bow, float speed, float attackStrength) + void ProjectileManager::launchProjectile(const Ptr& actor, const ConstPtr& projectile, const osg::Vec3f &pos, const osg::Quat &orient, const Ptr& bow, float speed, float attackStrength) { ProjectileState state; state.mActorId = actor.getClass().getCreatureStats(actor).getActorId(); @@ -325,20 +345,45 @@ namespace MWWorld state.mIdArrow = projectile.getCellRef().getRefId(); state.mCasterHandle = actor; state.mAttackStrength = attackStrength; - int type = projectile.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown; MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId()); MWWorld::Ptr ptr = ref.getPtr(); - createModel(state, ptr.getClass().getModel(ptr), pos, orient, false, false, osg::Vec4(0,0,0,0)); + const auto model = ptr.getClass().getModel(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)); + state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false); + state.mToDelete = false; mProjectiles.push_back(state); } + void ProjectileManager::updateCasters() + { + for (auto& state : mProjectiles) + mPhysics->setCaster(state.mProjectileId, state.getCaster()); + + for (auto& state : mMagicBolts) + { + // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back. + // TODO: should object-type caster be restored from savegame? + if (state.mActorId == -1) + continue; + + auto caster = state.getCaster(); + if (caster.isEmpty()) + { + Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId; + cleanupMagicBolt(state); + continue; + } + mPhysics->setCaster(state.mProjectileId, caster); + } + } + void ProjectileManager::update(float dt) { periodicCleanup(dt); @@ -360,192 +405,199 @@ namespace MWWorld return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold; }; - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end();) + for (auto& projectileState : mProjectiles) { - if (isCleanable(*it)) - { - cleanupProjectile(*it); - it = mProjectiles.erase(it); - } - else - ++it; + if (isCleanable(projectileState)) + cleanupProjectile(projectileState); } - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end();) + for (auto& magicBoltState : mMagicBolts) { - if (isCleanable(*it)) - { - cleanupMagicBolt(*it); - it = mMagicBolts.erase(it); - } - else - ++it; + if (isCleanable(magicBoltState)) + cleanupMagicBolt(magicBoltState); } } } void ProjectileManager::moveMagicBolts(float duration) { - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end();) + static const bool normaliseRaceSpeed = Settings::Manager::getBool("normalise race speed", "Game"); + for (auto& magicBoltState : mMagicBolts) { + if (magicBoltState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); + if (!projectile->isActive()) + continue; // If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame. - MWWorld::Ptr caster = it->getCaster(); + MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { - cleanupMagicBolt(*it); - it = mMagicBolts.erase(it); + cleanupMagicBolt(magicBoltState); continue; } } - osg::Quat orient = it->mNode->getAttitude(); - static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get() - .find("fTargetSpellMaxSpeed")->mValue.getFloat(); - float speed = fTargetSpellMaxSpeed * it->mSpeed; - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); - direction.normalize(); - osg::Vec3f pos(it->mNode->getPosition()); - osg::Vec3f newPos = pos + direction * duration * speed; - - for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++) + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + osg::Quat orient = magicBoltState.mNode->getAttitude(); + static float fTargetSpellMaxSpeed = store.get().find("fTargetSpellMaxSpeed")->mValue.getFloat(); + float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; + if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc()) { - it->mSounds.at(soundIter)->setPosition(newPos); + 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; } + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + direction.normalize(); + projectile->setVelocity(direction * speed); - it->mNode->setPosition(newPos); - - update(*it, duration); + update(magicBoltState, duration); // 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 (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); - - // Check for impact - // TODO: use a proper btRigidBody / btGhostObject? - MWPhysics::RayCastingResult result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile); - - bool hit = false; - if (result.mHit) - { - hit = true; - if (result.mHitObject.isEmpty()) - { - // terrain - } - else - { - MWMechanics::CastSpell cast(caster, result.mHitObject); - cast.mHitPosition = pos; - cast.mId = it->mSpellId; - cast.mSourceName = it->mSourceName; - cast.mStack = false; - cast.inflict(result.mHitObject, caster, it->mEffects, ESM::RT_Target, false, true); - mPhysics->reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); - } - } - - // Explodes when hitting water - if (MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), newPos)) - hit = true; - - if (hit) - { - MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, result.mHitObject, - ESM::RT_Target, it->mSpellId, it->mSourceName); - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++) - sndMgr->stopSound(it->mSounds.at(soundIter)); - - mParent->removeChild(it->mNode); - - it = mMagicBolts.erase(it); - continue; - } - else - ++it; + projectile->setValidTargets(targetActors); } } void ProjectileManager::moveProjectiles(float duration) { - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end();) + for (auto& projectileState : mProjectiles) { + if (projectileState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); + if (!projectile->isActive()) + continue; // gravity constant - must be way lower than the gravity affecting actors, since we're not // simulating aerodynamics at all - it->mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; + projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f pos(it->mNode->getPosition()); - osg::Vec3f newPos = pos + it->mVelocity * duration; + projectile->setVelocity(projectileState.mVelocity); // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. - if (!it->mThrown) + if (!projectileState.mThrown) { osg::Quat orient; - orient.makeRotate(osg::Vec3f(0,1,0), it->mVelocity); - it->mNode->setAttitude(orient); + orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity); + projectileState.mNode->setAttitude(orient); } - it->mNode->setPosition(newPos); - - update(*it, duration); + update(projectileState, duration); - MWWorld::Ptr caster = it->getCaster(); + MWWorld::Ptr caster = projectileState.getCaster(); // 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 (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); + projectile->setValidTargets(targetActors); + } + } - // Check for impact - // TODO: use a proper btRigidBody / btGhostObject? - MWPhysics::RayCastingResult result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile); + void ProjectileManager::processHits() + { + for (auto& projectileState : mProjectiles) + { + if (projectileState.mToDelete) + continue; - bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), newPos); + auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - if (result.mHit || underwater) - { - MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mIdArrow); + const auto pos = projectile->getSimulationPosition(); + projectileState.mNode->setPosition(pos); - // Try to get a Ptr to the bow that was used. It might no longer exist. - MWWorld::Ptr bow = projectileRef.getPtr(); - if (!caster.isEmpty() && it->mIdArrow != it->mBowId) - { - MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); - MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) - bow = *invIt; - } + if (projectile->isActive()) + continue; + + const auto target = projectile->getTarget(); + auto caster = projectileState.getCaster(); + assert(target != caster); + + if (caster.isEmpty()) + caster = target; - if (caster.isEmpty()) - caster = result.mHitObject; + // Try to get a Ptr to the bow that was used. It might no longer exist. + MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow); + MWWorld::Ptr bow = projectileRef.getPtr(); + if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId) + { + MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); + MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) + bow = *invIt; + } + if (projectile->getHitWater()) + mRendering->emitWaterRipple(pos); + + MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); + projectileState.mToDelete = true; + } + for (auto& magicBoltState : mMagicBolts) + { + if (magicBoltState.mToDelete) + continue; - MWMechanics::projectileHit(caster, result.mHitObject, bow, projectileRef.getPtr(), result.mHit ? result.mHitPos : newPos, it->mAttackStrength); - mPhysics->reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); + auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); - if (underwater) - mRendering->emitWaterRipple(newPos); + const auto pos = projectile->getSimulationPosition(); + magicBoltState.mNode->setPosition(pos); + for (const auto& sound : magicBoltState.mSounds) + sound->setPosition(pos); - mParent->removeChild(it->mNode); - it = mProjectiles.erase(it); + if (projectile->isActive()) continue; - } - ++it; + const auto target = projectile->getTarget(); + const auto caster = magicBoltState.getCaster(); + assert(target != caster); + + MWMechanics::CastSpell cast(caster, target); + cast.mHitPosition = pos; + cast.mId = magicBoltState.mSpellId; + cast.mSourceName = magicBoltState.mSourceName; + cast.mSlot = magicBoltState.mSlot; + cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, true); + + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); + magicBoltState.mToDelete = true; + } + + for (auto& projectileState : mProjectiles) + { + if (projectileState.mToDelete) + cleanupProjectile(projectileState); } + + for (auto& magicBoltState : mMagicBolts) + { + if (magicBoltState.mToDelete) + cleanupMagicBolt(magicBoltState); + } + mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }), + mProjectiles.end()); + mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }), + mMagicBolts.end()); } void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state) { mParent->removeChild(state.mNode); + mPhysics->removeProjectile(state.mProjectileId); + state.mToDelete = true; } void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state) { mParent->removeChild(state.mNode); + mPhysics->removeProjectile(state.mProjectileId); + state.mToDelete = true; for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++) { MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter)); @@ -554,15 +606,12 @@ namespace MWWorld void ProjectileManager::clear() { - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it) - { - cleanupProjectile(*it); - } + for (auto& mProjectile : mProjectiles) + cleanupProjectile(mProjectile); mProjectiles.clear(); - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) - { - cleanupMagicBolt(*it); - } + + for (auto& mMagicBolt : mMagicBolts) + cleanupMagicBolt(mMagicBolt); mMagicBolts.clear(); } @@ -596,7 +645,7 @@ namespace MWWorld state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; - + state.mSlot = it->mSlot; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; @@ -619,6 +668,7 @@ namespace MWWorld state.mVelocity = esm.mVelocity; state.mIdArrow = esm.mId; state.mAttackStrength = esm.mAttackStrength; + state.mToDelete = false; std::string model; try @@ -626,9 +676,10 @@ namespace MWWorld MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getModel(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(...) { @@ -640,7 +691,7 @@ namespace MWWorld mProjectiles.push_back(state); return true; } - else if (type == ESM::REC_MPRJ) + if (type == ESM::REC_MPRJ) { ESM::MagicBoltState esm; esm.load(reader); @@ -649,7 +700,9 @@ namespace MWWorld state.mIdMagic.push_back(esm.mId); state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; - std::string texture = ""; + state.mToDelete = false; + state.mSlot = esm.mSlot; + std::string texture; try { @@ -680,6 +733,7 @@ namespace MWWorld osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index c7b1056b72..63a0dacc71 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include "../mwbase/soundmanager.hpp" @@ -49,13 +49,17 @@ namespace MWWorld MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); + void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot); - void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, - const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); + void launchProjectile (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile, + const osg::Vec3f& pos, const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength); + + void updateCasters(); void update(float dt); + void processHits(); + /// Removes all current projectiles. Should be called when switching to a new worldspace. void clear(); @@ -76,6 +80,7 @@ namespace MWWorld std::shared_ptr mEffectAnimationTime; int mActorId; + int mProjectileId; // TODO: this will break when the game is saved and reloaded, since there is currently // no way to write identifiers for non-actors to a savegame. @@ -88,6 +93,8 @@ namespace MWWorld // MW-id of an arrow projectile std::string mIdArrow; + + bool mToDelete; }; struct MagicBoltState : public State @@ -100,6 +107,7 @@ namespace MWWorld ESM::EffectList mEffects; float mSpeed; + int mSlot; std::vector mSounds; std::set mSoundIds; diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp deleted file mode 100644 index 12c44b0b31..0000000000 --- a/apps/openmw/mwworld/ptr.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "ptr.hpp" - -#include - -#include "containerstore.hpp" -#include "class.hpp" -#include "livecellref.hpp" - -const std::string& MWWorld::Ptr::getTypeName() const -{ - if(mRef != 0) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -MWWorld::CellRef& MWWorld::Ptr::getCellRef() const -{ - assert(mRef); - - return mRef->mRef; -} - -MWWorld::RefData& MWWorld::Ptr::getRefData() const -{ - assert(mRef); - - return mRef->mData; -} - -void MWWorld::Ptr::setContainerStore (ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::Ptr::operator const void *() -{ - return mRef; -} - -// ------------------------------------------------------------------------------- - -const std::string &MWWorld::ConstPtr::getTypeName() const -{ - if(mRef != 0) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::ConstPtr::operator const void *() -{ - return mRef; -} diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index d7c170e456..0b8db5ac75 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -2,9 +2,9 @@ #define GAME_MWWORLD_PTR_H #include - +#include #include -#include +#include #include "livecellref.hpp" @@ -15,56 +15,72 @@ namespace MWWorld struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef - - class Ptr + /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr + template class TypeTransform> + class PtrBase { public: - MWWorld::LiveCellRefBase *mRef; - CellStore *mCell; - ContainerStore *mContainerStore; + typedef TypeTransform LiveCellRefBaseType; + typedef TypeTransform CellStoreType; + typedef TypeTransform ContainerStoreType; - public: - Ptr(MWWorld::LiveCellRefBase *liveCellRef=0, CellStore *cell=0) - : mRef(liveCellRef), mCell(cell), mContainerStore(0) + LiveCellRefBaseType *mRef; + CellStoreType *mCell; + ContainerStoreType *mContainerStore; + + bool isEmpty() const { + return mRef == nullptr; } - bool isEmpty() const + // Returns a 32-bit id of the ESM record this object is based on. + // Specific values of ids are defined in ESM::RecNameInts. + // Note 1: ids are not sequential. E.g. for a creature `getType` returns 0x41455243. + // Note 2: Life is not easy and full of surprises. For example + // prison marker reuses ESM::Door record. Player is ESM::NPC. + unsigned int getType() const { - return mRef == 0; + if(mRef != nullptr) + return mRef->getType(); + throw std::runtime_error("Can't get type name from an empty object."); } - const std::string& getTypeName() const; + std::string_view getTypeDescription() const + { + return mRef ? mRef->getTypeDescription() : "nullptr"; + } const Class& getClass() const { - if(mRef != 0) + if(mRef != nullptr) return *(mRef->mClass); throw std::runtime_error("Cannot get class of an empty object"); } - template - MWWorld::LiveCellRef *get() const - { - MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); - if(ref) return ref; - - std::stringstream str; - str<< "Bad LiveCellRef cast to "< + auto* get() const { return LiveCellRefBase::dynamicCast(mRef); } - throw std::runtime_error(str.str()); + LiveCellRefBaseType *getBase() const + { + if (!mRef) + throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); + return mRef; } - MWWorld::LiveCellRefBase *getBase() const; - - MWWorld::CellRef& getCellRef() const; + TypeTransform& getCellRef() const + { + assert(mRef); + return mRef->mRef; + } - RefData& getRefData() const; + TypeTransform& getRefData() const + { + assert(mRef); + return mRef->mData; + } - CellStore *getCell() const + CellStoreType *getCell() const { assert(mCell); return mCell; @@ -72,162 +88,50 @@ namespace MWWorld bool isInCell() const { - return (mContainerStore == 0) && (mCell != 0); + return (mContainerStore == nullptr) && (mCell != nullptr); } - void setContainerStore (ContainerStore *store); + void setContainerStore (ContainerStoreType *store) ///< Must not be called on references that are in a cell. + { + assert (store); + assert (!mCell); + mContainerStore = store; + } - ContainerStore *getContainerStore() const; + ContainerStoreType *getContainerStore() const ///< May return a 0-pointer, if reference is not in a container. + { + return mContainerStore; + } - operator const void *(); + operator const void *() const ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + { + return mRef; + } + + protected: + PtrBase(LiveCellRefBaseType *liveCellRef, CellStoreType *cell, ContainerStoreType* containerStore) : mRef(liveCellRef), mCell(cell), mContainerStore(containerStore) {} }; - /// \brief Pointer to a const LiveCellRef - /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. - class ConstPtr + /// @note It is possible to get mutable values from const Ptr. So if a function accepts const Ptr&, the object is still mutable. + /// To make it really const the argument should be const ConstPtr&. + class Ptr : public PtrBase { public: - - const MWWorld::LiveCellRefBase *mRef; - const CellStore *mCell; - const ContainerStore *mContainerStore; - - public: - ConstPtr(const MWWorld::LiveCellRefBase *liveCellRef=0, const CellStore *cell=0) - : mRef(liveCellRef), mCell(cell), mContainerStore(0) - { - } - - ConstPtr(const MWWorld::Ptr& ptr) - : mRef(ptr.mRef), mCell(ptr.mCell), mContainerStore(ptr.mContainerStore) - { - } - - bool isEmpty() const - { - return mRef == 0; - } - - const std::string& getTypeName() const; - - const Class& getClass() const - { - if(mRef != 0) - return *(mRef->mClass); - throw std::runtime_error("Cannot get class of an empty object"); - } - - template - const MWWorld::LiveCellRef *get() const - { - const MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); - if(ref) return ref; - - std::stringstream str; - str<< "Bad LiveCellRef cast to "<mRef; - } - - const RefData& getRefData() const - { - assert(mRef); - return mRef->mData; - } - - const CellStore *getCell() const - { - assert(mCell); - return mCell; - } - - bool isInCell() const - { - return (mContainerStore == 0) && (mCell != 0); - } - - void setContainerStore (const ContainerStore *store); - ///< Must not be called on references that are in a cell. - - const ContainerStore *getContainerStore() const; - ///< May return a 0-pointer, if reference is not in a container. - - operator const void *(); - ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + Ptr(LiveCellRefBase *liveCellRef=nullptr, CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} }; - inline bool operator== (const Ptr& left, const Ptr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const Ptr& left, const Ptr& right) - { - return !(left==right); - } - - inline bool operator< (const Ptr& left, const Ptr& right) - { - return left.mRef= (const Ptr& left, const Ptr& right) - { - return !(left (const Ptr& left, const Ptr& right) - { - return rightright); - } - - inline bool operator== (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const ConstPtr& left, const ConstPtr& right) - { - return !(left==right); - } - - inline bool operator< (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef= (const ConstPtr& left, const ConstPtr& right) - { - return !(left (const ConstPtr& left, const ConstPtr& right) + /// @note The difference between Ptr and ConstPtr is that the second one adds const to the underlying pointers. + /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. + class ConstPtr : public PtrBase { - return rightright); - } } #endif diff --git a/apps/openmw/mwworld/recordcmp.hpp b/apps/openmw/mwworld/recordcmp.hpp deleted file mode 100644 index f749351cea..0000000000 --- a/apps/openmw/mwworld/recordcmp.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWWORLD_RECORDCMP_H -#define OPENMW_MWWORLD_RECORDCMP_H - -#include - -#include - -namespace MWWorld -{ - struct RecordCmp - { - template - bool operator()(const T &x, const T& y) const { - return x.mId < y.mId; - } - }; - - template <> - inline bool RecordCmp::operator()(const ESM::Dialogue &x, const ESM::Dialogue &y) const { - return Misc::StringUtils::ciLess(x.mId, y.mId); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Cell &x, const ESM::Cell &y) const { - return Misc::StringUtils::ciLess(x.mName, y.mName); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - return Misc::StringUtils::ciLess(x.mCell, y.mCell); - } - -} // end namespace -#endif diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index a5f8ef4b14..10c65bd17c 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -1,6 +1,6 @@ #include "refdata.hpp" -#include +#include #include "customdata.hpp" #include "cellstore.hpp" @@ -8,6 +8,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + namespace { enum RefDataFlags @@ -21,6 +23,12 @@ enum RefDataFlags namespace MWWorld { + void RefData::setLuaScripts(std::shared_ptr&& scripts) + { + mChanged = true; + mLuaScripts = std::move(scripts); + } + void RefData::copy (const RefData& refData) { mBaseNode = refData.mBaseNode; @@ -31,22 +39,23 @@ namespace MWWorld mChanged = refData.mChanged; mDeletedByContentFile = refData.mDeletedByContentFile; mFlags = refData.mFlags; + mPhysicsPostponed = refData.mPhysicsPostponed; mAnimationState = refData.mAnimationState; - mCustomData = refData.mCustomData ? refData.mCustomData->clone() : 0; + mCustomData = refData.mCustomData ? refData.mCustomData->clone() : nullptr; + mLuaScripts = refData.mLuaScripts; } void RefData::cleanup() { - mBaseNode = 0; - - delete mCustomData; - mCustomData = 0; + mBaseNode = nullptr; + mCustomData = nullptr; + mLuaScripts = nullptr; } RefData::RefData() - : mBaseNode(0), mDeletedByContentFile(false), mEnabled (true), mCount (1), mCustomData (0), mChanged(false), mFlags(0) + : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mPhysicsPostponed(false), mCount (1), mCustomData (nullptr), mChanged(false), mFlags(0) { for (int i=0; i<3; ++i) { @@ -56,20 +65,20 @@ namespace MWWorld } RefData::RefData (const ESM::CellRef& cellRef) - : mBaseNode(0), mDeletedByContentFile(false), mEnabled (true), + : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mPhysicsPostponed(false), mCount (1), mPosition (cellRef.mPos), - mCustomData (0), + mCustomData (nullptr), mChanged(false), mFlags(0) // Loading from ESM/ESP files -> assume unchanged { } RefData::RefData (const ESM::ObjectState& objectState, bool deletedByContentFile) - : mBaseNode(0), mDeletedByContentFile(deletedByContentFile), - mEnabled (objectState.mEnabled != 0), + : mBaseNode(nullptr), mDeletedByContentFile(deletedByContentFile), + mEnabled (objectState.mEnabled != 0), mPhysicsPostponed(false), mCount (objectState.mCount), mPosition (objectState.mPosition), mAnimationState(objectState.mAnimationState), - mCustomData (0), + mCustomData (nullptr), mChanged(true), mFlags(objectState.mFlags) // Loading from a savegame -> assume changed { // "Note that the ActivationFlag_UseEnabled is saved to the reference, @@ -79,7 +88,7 @@ namespace MWWorld } RefData::RefData (const RefData& refData) - : mBaseNode(0), mCustomData (0) + : mBaseNode(nullptr), mCustomData (nullptr) { try { @@ -131,6 +140,9 @@ namespace MWWorld {} } + RefData::RefData(RefData&& other) noexcept = default; + RefData& RefData::operator=(RefData&& other) noexcept = default; + void RefData::setBaseNode(SceneUtil::PositionAttitudeTransform *base) { mBaseNode = base; @@ -223,21 +235,20 @@ namespace MWWorld return mPosition; } - void RefData::setCustomData (CustomData *data) + void RefData::setCustomData(std::unique_ptr&& value) noexcept { mChanged = true; // We do not currently track CustomData, so assume anything with a CustomData is changed - delete mCustomData; - mCustomData = data; + mCustomData = std::move(value); } CustomData *RefData::getCustomData() { - return mCustomData; + return mCustomData.get(); } const CustomData *RefData::getCustomData() const { - return mCustomData; + return mCustomData.get(); } bool RefData::hasChanged() const diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index 738a6d53a8..0bc6df52a5 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -2,11 +2,13 @@ #define GAME_MWWORLD_REFDATA_H #include -#include +#include #include "../mwscript/locals.hpp" +#include "../mwworld/customdata.hpp" #include +#include namespace SceneUtil { @@ -20,6 +22,11 @@ namespace ESM struct ObjectState; } +namespace MWLua +{ + class LocalScripts; +} + namespace MWWorld { @@ -30,12 +37,16 @@ namespace MWWorld SceneUtil::PositionAttitudeTransform* mBaseNode; MWScript::Locals mLocals; + std::shared_ptr mLuaScripts; /// separate delete flag used for deletion by a content file /// @note not stored in the save game file. - bool mDeletedByContentFile; + bool mDeletedByContentFile:1; - bool mEnabled; + bool mEnabled:1; + public: + bool mPhysicsPostponed:1; + private: /// 0: deleted int mCount; @@ -44,7 +55,7 @@ namespace MWWorld ESM::AnimationState mAnimationState; - CustomData *mCustomData; + std::unique_ptr mCustomData; void copy (const RefData& refData); @@ -55,7 +66,6 @@ namespace MWWorld unsigned int mFlags; public: - RefData(); /// @param cellRef Used to copy constant data such as position into this class where it can @@ -68,6 +78,7 @@ namespace MWWorld /// perform these operations). RefData (const RefData& refData); + RefData (RefData&& other) noexcept; ~RefData(); @@ -76,6 +87,7 @@ namespace MWWorld /// perform this operations). RefData& operator= (const RefData& refData); + RefData& operator= (RefData&& other) noexcept; /// Return base node (can be a null pointer). SceneUtil::PositionAttitudeTransform* getBaseNode(); @@ -90,6 +102,9 @@ namespace MWWorld void setLocals (const ESM::Script& script); + MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } + void setLuaScripts(std::shared_ptr&&); + void setCount (int count); ///< Set object count (an object pile is a simple object with a count >1). /// @@ -117,7 +132,7 @@ namespace MWWorld void setPosition (const ESM::Position& pos); const ESM::Position& getPosition() const; - void setCustomData (CustomData *data); + void setCustomData(std::unique_ptr&& value) noexcept; ///< Set custom data (potentially replacing old custom data). The ownership of \a data is /// transferred to this. diff --git a/apps/openmw/mwworld/registeredclass.hpp b/apps/openmw/mwworld/registeredclass.hpp new file mode 100644 index 0000000000..7ed9632129 --- /dev/null +++ b/apps/openmw/mwworld/registeredclass.hpp @@ -0,0 +1,23 @@ +#ifndef GAME_MWWORLD_REGISTEREDCLASS_H +#define GAME_MWWORLD_REGISTEREDCLASS_H + +#include "class.hpp" + +namespace MWWorld +{ + template + class RegisteredClass : public Base + { + public: + static void registerSelf() + { + static Derived instance; + Base::registerClass(instance); + } + + protected: + explicit RegisteredClass(unsigned type) : Base(type) {} + }; +} + +#endif diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index d01cc741cb..f23c086827 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -2,10 +2,9 @@ #include #include -#include +#include #include -#include #include #include @@ -13,23 +12,21 @@ #include #include #include -#include -#include #include #include -#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" - -#include "../mwmechanics/actorutil.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/landmanager.hpp" +#include "../mwrender/postprocessor.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/actor.hpp" @@ -43,6 +40,8 @@ #include "cellvisitors.hpp" #include "cellstore.hpp" #include "cellpreloader.hpp" +#include "worldimp.hpp" +#include "cellutils.hpp" namespace { @@ -64,45 +63,43 @@ namespace * osg::Quat(zr, osg::Vec3(0, 0, -1)); } - osg::Quat makeObjectOsgQuat(const ESM::Position& position) + osg::Quat makeInverseNodeRotation(const MWWorld::Ptr& ptr) { - const float xr = position.rot[0]; - const float yr = position.rot[1]; - const float zr = position.rot[2]; + const auto pos = ptr.getRefData().getPosition(); + return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : makeInversedOrderObjectOsgQuat(pos); + } - return osg::Quat(zr, osg::Vec3(0, 0, -1)) - * osg::Quat(yr, osg::Vec3(0, -1, 0)) - * osg::Quat(xr, osg::Vec3(-1, 0, 0)); + osg::Quat makeDirectNodeRotation(const MWWorld::Ptr& ptr) + { + const auto pos = ptr.getRefData().getPosition(); + return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : Misc::Convert::makeOsgQuat(pos); } - void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, RotationOrder order) + osg::Quat makeNodeRotation(const MWWorld::Ptr& ptr, RotationOrder order) { - if (!ptr.getRefData().getBaseNode()) - return; + if (order == RotationOrder::inverse) + return makeInverseNodeRotation(ptr); + return makeDirectNodeRotation(ptr); + } - rendering.rotateObject(ptr, - ptr.getClass().isActor() - ? makeActorOsgQuat(ptr.getRefData().getPosition()) - : (order == RotationOrder::inverse - ? makeInversedOrderObjectOsgQuat(ptr.getRefData().getPosition()) - : makeObjectOsgQuat(ptr.getRefData().getPosition())) - ); + void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, const osg::Quat &rotation) + { + if (ptr.getRefData().getBaseNode()) + rendering.rotateObject(ptr, rotation); } std::string getModel(const MWWorld::Ptr &ptr, const VFS::Manager *vfs) { + if (Misc::ResourceHelpers::isHiddenMarker(ptr.getCellRef().getRefId())) + return {}; bool useAnim = ptr.getClass().useAnim(); std::string model = ptr.getClass().getModel(ptr); if (useAnim) model = Misc::ResourceHelpers::correctActorModelPath(model, vfs); - - const std::string &id = ptr.getCellRef().getRefId(); - if (id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker") - model = ""; // marker objects that have a hardcoded function in the game logic, should be hidden from the player return model; } - void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics, + void addObject(const MWWorld::Ptr& ptr, const MWWorld::World& world, MWPhysics::PhysicsSystem& physics, MWRender::RenderingManager& rendering, std::set& pagedRefs) { if (ptr.getRefData().getBaseNode() || physics.getActor(ptr)) @@ -111,71 +108,70 @@ namespace return; } - bool useAnim = ptr.getClass().useAnim(); std::string model = getModel(ptr, rendering.getResourceSystem()->getVFS()); + const auto rotation = makeDirectNodeRotation(ptr); const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile() || pagedRefs.find(refnum) == pagedRefs.end()) ptr.getClass().insertObjectRendering(ptr, model, rendering); else ptr.getRefData().setBaseNode(new SceneUtil::PositionAttitudeTransform); // FIXME remove this when physics code is fixed not to depend on basenode - setNodeRotation(ptr, rendering, RotationOrder::direct); + setNodeRotation(ptr, rendering, rotation); - ptr.getClass().insertObject (ptr, model, physics); - - if (useAnim) + if (ptr.getClass().useAnim()) MWBase::Environment::get().getMechanicsManager()->add(ptr); if (ptr.getClass().isActor()) rendering.addWaterRippleEmitter(ptr); // Restore effect particles - MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); + world.applyLoopingParticles(ptr); + + if (!model.empty()) + ptr.getClass().insertObject(ptr, model, rotation, physics); + + MWBase::Environment::get().getLuaManager()->objectAddedToScene(ptr); } - void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator) + void addObject(const MWWorld::Ptr& ptr, const MWWorld::World& world, const MWPhysics::PhysicsSystem& physics, + DetourNavigator::Navigator& navigator) { if (const auto object = physics.getObject(ptr)) { + const DetourNavigator::ObjectTransform objectTransform {ptr.getRefData().getPosition(), ptr.getCellRef().getScale()}; + if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport()) { - const auto shape = object->getShapeInstance()->getCollisionShape(); - btVector3 aabbMin; btVector3 aabbMax; - shape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; - const auto distanceFromDoor = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 0.5f; + const auto distanceFromDoor = world.getMaxActivationDistance() * 0.5f; const auto toPoint = aabbMax.x() - aabbMin.x() < aabbMax.y() - aabbMin.y() ? btVector3(distanceFromDoor, 0, 0) : btVector3(0, distanceFromDoor, 0); const auto transform = object->getTransform(); const btTransform closedDoorTransform( - Misc::Convert::toBullet(makeObjectOsgQuat(ptr.getCellRef().getPosition())), + Misc::Convert::makeBulletQuaternion(ptr.getCellRef().getPosition()), transform.getOrigin() ); - const auto start = Misc::Convert::makeOsgVec3f(closedDoorTransform(center + toPoint)); + const auto start = Misc::Convert::toOsg(closedDoorTransform(center + toPoint)); const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start; - const auto end = Misc::Convert::makeOsgVec3f(closedDoorTransform(center - toPoint)); + const auto end = Misc::Convert::toOsg(closedDoorTransform(center - toPoint)); const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end; navigator.addObject( DetourNavigator::ObjectId(object), - DetourNavigator::DoorShapes( - *shape, - object->getShapeInstance()->getAvoidCollisionShape(), - connectionStart, - connectionEnd - ), + DetourNavigator::DoorShapes(object->getShapeInstance(), objectTransform, connectionStart, connectionEnd), transform ); } @@ -183,29 +179,25 @@ namespace { navigator.addObject( DetourNavigator::ObjectId(object), - DetourNavigator::ObjectShapes { - *object->getShapeInstance()->getCollisionShape(), - object->getShapeInstance()->getAvoidCollisionShape() - }, + DetourNavigator::ObjectShapes(object->getShapeInstance(), objectTransform), object->getTransform() ); } } else if (physics.getActor(ptr)) { - navigator.addAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); + navigator.addAgent(world.getPathfindingAgentBounds(ptr)); } } struct InsertVisitor { MWWorld::CellStore& mCell; - Loading::Listener& mLoadingListener; - bool mTest; + Loading::Listener* mLoadingListener; std::vector mToInsert; - InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test); + InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener); bool operator() (const MWWorld::Ptr& ptr); @@ -213,8 +205,8 @@ namespace void insert(AddObject&& addObject); }; - InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test) - : mCell (cell), mLoadingListener (loadingListener), mTest(test) + InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener) + : mCell(cell), mLoadingListener(loadingListener) {} bool InsertVisitor::operator() (const MWWorld::Ptr& ptr) @@ -243,26 +235,26 @@ namespace } } - if (!mTest) - mLoadingListener.increaseProgress (1); + if (mLoadingListener != nullptr) + mLoadingListener->increaseProgress(1); } } - struct PositionVisitor - { - bool operator() (const MWWorld::Ptr& ptr) - { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) - ptr.getClass().adjustPosition (ptr, false); - return true; - } - }; - int getCellPositionDistanceToOrigin(const std::pair& cellPosition) { return std::abs(cellPosition.first) + std::abs(cellPosition.second); } + bool isCellInCollection(int x, int y, MWWorld::Scene::CellStoreCollection& collection) + { + for (auto *cell : collection) + { + assert(cell->getCell()->isExterior()); + if (x == cell->getCell()->getGridX() && y == cell->getCell()->getGridY()) + return true; + } + return false; + } } @@ -276,7 +268,7 @@ namespace MWWorld { if (!ptr.getRefData().getBaseNode()) return; ptr.getClass().insertObjectRendering(ptr, getModel(ptr, mRendering.getResourceSystem()->getVFS()), mRendering); - setNodeRotation(ptr, mRendering, RotationOrder::direct); + setNodeRotation(ptr, mRendering, makeNodeRotation(ptr, RotationOrder::direct)); reloadTerrain(); } } @@ -292,8 +284,9 @@ namespace MWWorld void Scene::updateObjectRotation(const Ptr &ptr, RotationOrder order) { - setNodeRotation(ptr, mRendering, order); - mPhysics->updateRotation(ptr); + const auto rot = makeNodeRotation(ptr, order); + setNodeRotation(ptr, mRendering, rot); + mPhysics->updateRotation(ptr, rot); } void Scene::updateObjectScale(const Ptr &ptr) @@ -305,174 +298,190 @@ namespace MWWorld mPhysics->updateScale(ptr); } - void Scene::update (float duration, bool paused) + void Scene::update(float duration) { + if (mChangeCellGridRequest.has_value()) + { + changeCellGrid(mChangeCellGridRequest->mPosition, mChangeCellGridRequest->mCell.x(), + mChangeCellGridRequest->mCell.y(), mChangeCellGridRequest->mChangeEvent); + mChangeCellGridRequest.reset(); + } + mPreloader->updateCache(mRendering.getReferenceTime()); preloadCells(duration); - - mRendering.update (duration, paused); } - void Scene::unloadCell (CellStoreCollection::iterator iter, bool test) + void Scene::unloadCell(CellStore* cell) { - if (!test) - Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription(); + if (mActiveCells.find(cell) == mActiveCells.end()) + return; + + Log(Debug::Info) << "Unloading cell " << cell->getCell()->getDescription(); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); ListAndResetObjectsVisitor visitor; - (*iter)->forEach(visitor); - const auto world = MWBase::Environment::get().getWorld(); + cell->forEach(visitor); for (const auto& ptr : visitor.mObjects) { if (const auto object = mPhysics->getObject(ptr)) - navigator->removeObject(DetourNavigator::ObjectId(object)); + { + mNavigator.removeObject(DetourNavigator::ObjectId(object)); + mPhysics->remove(ptr); + ptr.mRef->mData.mPhysicsPostponed = false; + } else if (mPhysics->getActor(ptr)) { - navigator->removeAgent(world->getPathfindingHalfExtents(ptr)); + mNavigator.removeAgent(mWorld.getPathfindingAgentBounds(ptr)); mRendering.removeActorPath(ptr); + mPhysics->remove(ptr); } - mPhysics->remove(ptr); + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); } - const auto cellX = (*iter)->getCell()->getGridX(); - const auto cellY = (*iter)->getCell()->getGridY(); + const auto cellX = cell->getCell()->getGridX(); + const auto cellY = cell->getCell()->getGridY(); - if ((*iter)->getCell()->isExterior()) + if (cell->getCell()->isExterior()) { - const ESM::Land* land = - MWBase::Environment::get().getWorld()->getStore().get().search( - (*iter)->getCell()->getGridX(), - (*iter)->getCell()->getGridY() - ); - if (land && land->mDataTypes&ESM::Land::DATA_VHGT) - { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->removeObject(DetourNavigator::ObjectId(heightField)); - mPhysics->removeHeightField(cellX, cellY); - } + if (mPhysics->getHeightField(cellX, cellY) != nullptr) + mNavigator.removeHeightfield(osg::Vec2i(cellX, cellY)); + + mPhysics->removeHeightField(cellX, cellY); } - if ((*iter)->getCell()->hasWater()) - navigator->removeWater(osg::Vec2i(cellX, cellY)); + if (cell->getCell()->hasWater()) + mNavigator.removeWater(osg::Vec2i(cellX, cellY)); - if (const auto pathgrid = world->getStore().get().search(*(*iter)->getCell())) - navigator->removePathgrid(*pathgrid); + if (const auto pathgrid = mWorld.getStore().get().search(*cell->getCell())) + mNavigator.removePathgrid(*pathgrid); - const auto player = world->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + mNavigator.update(mWorld.getPlayerPtr().getRefData().getPosition().asVec3()); - MWBase::Environment::get().getMechanicsManager()->drop (*iter); + MWBase::Environment::get().getMechanicsManager()->drop (cell); - mRendering.removeCell(*iter); - MWBase::Environment::get().getWindowManager()->removeCell(*iter); + mRendering.removeCell(cell); + MWBase::Environment::get().getWindowManager()->removeCell(cell); - MWBase::Environment::get().getWorld()->getLocalScripts().clearCell (*iter); + mWorld.getLocalScripts().clearCell (cell); - MWBase::Environment::get().getSoundManager()->stopSound (*iter); - mActiveCells.erase(*iter); + MWBase::Environment::get().getSoundManager()->stopSound (cell); + mActiveCells.erase(cell); } - void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test) + void Scene::loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn, const osg::Vec3f& position) { - std::pair result = mActiveCells.insert(cell); + using DetourNavigator::HeightfieldShape; - if(result.second) - { - if (test) - Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription(); - else - Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); + assert(mActiveCells.find(cell) == mActiveCells.end()); + mActiveCells.insert(cell); - float verts = ESM::Land::LAND_SIZE; - float worldsize = ESM::Land::REAL_SIZE; + Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); - const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); + const int cellX = cell->getCell()->getGridX(); + const int cellY = cell->getCell()->getGridY(); - const int cellX = cell->getCell()->getGridX(); - const int cellY = cell->getCell()->getGridY(); + mNavigator.setWorldspace(cell->getCell()->mCellId.mWorldspace); - // Load terrain physics first... - if (!test && cell->getCell()->isExterior()) + if (cell->getCell()->isExterior()) + { + osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; + const int verts = ESM::Land::LAND_SIZE; + const int worldsize = ESM::Land::REAL_SIZE; + if (data) { - osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0; - if (data) - { - mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); - } - else - { - static std::vector defaultHeight; - defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); - mPhysics->addHeightField (&defaultHeight[0], cell->getCell()->getGridX(), cell->getCell()->getGridY(), worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); - } - - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->addObject(DetourNavigator::ObjectId(heightField), *heightField->getShape(), - heightField->getCollisionObject()->getWorldTransform()); + mPhysics->addHeightField(data->mHeights, cellX, cellY, worldsize, verts, data->mMinHeight, data->mMaxHeight, land.get()); } - - if (const auto pathgrid = world->getStore().get().search(*cell->getCell())) - navigator->addPathgrid(*cell->getCell(), *pathgrid); - - // register local scripts - // do this before insertCell, to make sure we don't add scripts from levelled creature spawning twice - MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); - - if (respawn) - cell->respawn(); - - // ... then references. This is important for adjustPosition to work correctly. - insertCell (*cell, loadingListener, test); - - mRendering.addCell(cell); - if (!test) + else { - MWBase::Environment::get().getWindowManager()->addCell(cell); - bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); - float waterLevel = cell->getWaterLevel(); - mRendering.setWaterEnabled(waterEnabled); - if (waterEnabled) + static std::vector defaultHeight; + defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); + mPhysics->addHeightField(defaultHeight.data(), cellX, cellY, worldsize, verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); + } + if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + { + const osg::Vec2i cellPosition(cellX, cellY); + const btVector3& origin = heightField->getCollisionObject()->getWorldTransform().getOrigin(); + const osg::Vec3f shift(origin.x(), origin.y(), origin.z()); + const HeightfieldShape shape = [&] () -> HeightfieldShape { - mPhysics->enableWater(waterLevel); - mRendering.setWaterHeight(waterLevel); - - if (cell->getCell()->isExterior()) + if (data == nullptr) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, - cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform()); + return DetourNavigator::HeightfieldPlane {static_cast(ESM::Land::DEFAULT_HEIGHT)}; } else { - navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), - cell->getWaterLevel(), btTransform::getIdentity()); + DetourNavigator::HeightfieldSurface heights; + heights.mHeights = data->mHeights; + heights.mSize = static_cast(ESM::Land::LAND_SIZE); + heights.mMinHeight = data->mMinHeight; + heights.mMaxHeight = data->mMaxHeight; + return heights; } - } - else - mPhysics->disableWater(); + } (); + mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shape); + } + } - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + if (const auto pathgrid = mWorld.getStore().get().search(*cell->getCell())) + mNavigator.addPathgrid(*cell->getCell(), *pathgrid); - if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) - { + // register local scripts + // do this before insertCell, to make sure we don't add scripts from levelled creature spawning twice + mWorld.getLocalScripts().addCell (cell); - mRendering.configureAmbient(cell->getCell()); - } + if (respawn) + cell->respawn(); + + insertCell(*cell, loadingListener); + + mRendering.addCell(cell); + + MWBase::Environment::get().getWindowManager()->addCell(cell); + bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); + float waterLevel = cell->getWaterLevel(); + mRendering.setWaterEnabled(waterEnabled); + if (waterEnabled) + { + mPhysics->enableWater(waterLevel); + mRendering.setWaterHeight(waterLevel); + + if (cell->getCell()->isExterior()) + { + if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, waterLevel); + } + else + { + mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), waterLevel); } } + else + mPhysics->disableWater(); + + const auto player = mWorld.getPlayerPtr(); + + // The player is loaded before the scene and by default it is grounded, with the scene fully loaded, we validate and correct this. + if (player.mCell == cell) // Only run once, during initial cell load. + { + mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f); + } + + mNavigator.update(position); + + if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) + mRendering.configureAmbient(cell->getCell()); mPreloader->notifyLoaded(cell); } void Scene::clear() { - CellStoreCollection::iterator active = mActiveCells.begin(); - while (active!=mActiveCells.end()) - unloadCell (active++); + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) + { + auto* cell = *iter++; + unloadCell (cell); + } assert(mActiveCells.empty()); mCurrentCell = nullptr; @@ -489,90 +498,90 @@ namespace MWWorld if (currentGridCenter) { float centerX, centerY; - MWBase::Environment::get().getWorld()->indexToPosition(currentGridCenter->x(), currentGridCenter->y(), centerX, centerY, true); + mWorld.indexToPosition(currentGridCenter->x(), currentGridCenter->y(), centerX, centerY, true); float distance = std::max(std::abs(centerX-pos.x()), std::abs(centerY-pos.y())); const float maxDistance = Constants::CellSizeInUnits / 2 + mCellLoadingThreshold; // 1/2 cell size + threshold if (distance <= maxDistance) return *currentGridCenter; } - osg::Vec2i newCenter; - MWBase::Environment::get().getWorld()->positionToIndex(pos.x(), pos.y(), newCenter.x(), newCenter.y()); - return newCenter; + return positionToCellIndex(pos.x(), pos.y()); } void Scene::playerMoved(const osg::Vec3f &pos) { - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + if (mCurrentCell == nullptr) + return; - if (!mCurrentCell || !mCurrentCell->isExterior()) + mNavigator.updatePlayerPosition(pos); + + if (!mCurrentCell->isExterior()) return; osg::Vec2i newCell = getNewGridCenter(pos, &mCurrentGridCenter); if (newCell != mCurrentGridCenter) - changeCellGrid(pos, newCell.x(), newCell.y()); + requestChangeCellGrid(pos, newCell); + } + + void Scene::requestChangeCellGrid(const osg::Vec3f &position, const osg::Vec2i& cell, bool changeEvent) + { + mChangeCellGridRequest = ChangeCellGridRequest {position, cell, changeEvent}; } void Scene::changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent) { - CellStoreCollection::iterator active = mActiveCells.begin(); - while (active!=mActiveCells.end()) + for (auto iter = mActiveCells.begin(); iter != mActiveCells.end(); ) { - if ((*active)->getCell()->isExterior()) + auto* cell = *iter++; + if (cell->getCell()->isExterior()) { - if (std::abs (playerCellX-(*active)->getCell()->getGridX())<=mHalfGridSize && - std::abs (playerCellY-(*active)->getCell()->getGridY())<=mHalfGridSize) - { - // keep cells within the new grid - ++active; - continue; - } + const auto dx = std::abs(playerCellX - cell->getCell()->getGridX()); + const auto dy = std::abs(playerCellY - cell->getCell()->getGridY()); + if (dx > mHalfGridSize || dy > mHalfGridSize) + unloadCell(cell); } - unloadCell (active++); + else + unloadCell (cell); } + mNavigator.updateBounds(pos); + mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); mRendering.setActiveGrid(newGrid); - preloadTerrain(pos, true); + if (mRendering.pagingUnlockCache()) + mPreloader->abortTerrainPreloadExcept(nullptr); + if (!mPreloader->isTerrainLoaded(std::make_pair(pos, newGrid), mRendering.getReferenceTime())) + preloadTerrain(pos, true); mPagedRefs.clear(); mRendering.getPagedRefnums(newGrid, mPagedRefs); std::size_t refsToLoad = 0; - std::vector> cellsPositionsToLoad; - // get the number of refs to load - for (int x = playerCellX - mHalfGridSize; x <= playerCellX + mHalfGridSize; ++x) + const auto cellsToLoad = [&] (CellStoreCollection& collection, int range) -> std::vector> { - for (int y = playerCellY - mHalfGridSize; y <= playerCellY + mHalfGridSize; ++y) + std::vector> cellsPositionsToLoad; + for (int x = playerCellX - range; x <= playerCellX + range; ++x) { - CellStoreCollection::iterator iter = mActiveCells.begin(); - - while (iter!=mActiveCells.end()) + for (int y = playerCellY - range; y <= playerCellY + range; ++y) { - assert ((*iter)->getCell()->isExterior()); - - if (x==(*iter)->getCell()->getGridX() && - y==(*iter)->getCell()->getGridY()) - break; - - ++iter; - } - - if (iter==mActiveCells.end()) - { - refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count(); - cellsPositionsToLoad.emplace_back(x, y); + if (!isCellInCollection(x, y, collection)) + { + refsToLoad += mWorld.getExterior(x, y)->count(); + cellsPositionsToLoad.emplace_back(x, y); + } } } - } + return cellsPositionsToLoad; + }; + + addPostponedPhysicsObjects(); + + auto cellsPositionsToLoad = cellsToLoad(mActiveCells,mHalfGridSize); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); - int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount(); std::string loadingExteriorText = "#{sLoadingMessage3}"; - loadingListener->setLabel(loadingExteriorText, false, messagesCount > 0); + loadingListener->setLabel(loadingExteriorText); loadingListener->setProgressRange(refsToLoad); const auto getDistanceToPlayerCell = [&] (const std::pair& cellPosition) @@ -590,38 +599,46 @@ namespace MWWorld return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); }); - // Load cells - for (const auto& cellPosition : cellsPositionsToLoad) + for (const auto& [x,y] : cellsPositionsToLoad) { - const auto x = cellPosition.first; - const auto y = cellPosition.second; - - CellStoreCollection::iterator iter = mActiveCells.begin(); - - while (iter != mActiveCells.end()) + if (!isCellInCollection(x, y, mActiveCells)) { - assert ((*iter)->getCell()->isExterior()); - - if (x == (*iter)->getCell()->getGridX() && - y == (*iter)->getCell()->getGridY()) - break; - - ++iter; - } - - if (iter == mActiveCells.end()) - { - CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - - loadCell (cell, loadingListener, changeEvent); + CellStore *cell = mWorld.getExterior(x, y); + loadCell(cell, loadingListener, changeEvent, pos); } } - CellStore* current = MWBase::Environment::get().getWorld()->getExterior(playerCellX, playerCellY); + CellStore* current = mWorld.getExterior(playerCellX, playerCellY); MWBase::Environment::get().getWindowManager()->changeCell(current); if (changeEvent) mCellChanged = true; + + mNavigator.wait(*loadingListener, DetourNavigator::WaitConditionType::requiredTilesPresent); + } + + void Scene::addPostponedPhysicsObjects() + { + for(const auto& cell : mActiveCells) + { + cell->forEach([&](const MWWorld::Ptr& ptr) + { + if(ptr.mRef->mData.mPhysicsPostponed) + { + ptr.mRef->mData.mPhysicsPostponed = false; + if (ptr.mRef->mData.isEnabled() && ptr.mRef->mData.getCount() > 0) + { + std::string model = getModel(ptr, MWBase::Environment::get().getResourceSystem()->getVFS()); + if (!model.empty()) + { + const auto rotation = makeNodeRotation(ptr, RotationOrder::direct); + ptr.getClass().insertObjectPhysics(ptr, model, rotation, *mPhysics); + } + } + } + return true; + }); + } } void Scene::testExteriorCells() @@ -631,7 +648,7 @@ namespace MWWorld mRendering.getResourceSystem()->setExpiryDelay(1.f); - const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store &cells = mWorld.getStore().get(); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -643,18 +660,16 @@ namespace MWWorld { loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")..."); - CellStoreCollection::iterator iter = mActiveCells.begin(); - - CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY); - loadCell (cell, loadingListener, false, true); + CellStore *cell = mWorld.getExterior(it->mData.mX, it->mData.mY); + loadCell(cell, nullptr, false, osg::Vec3f(it->mData.mX + 0.5f, it->mData.mY + 0.5f, 0) * Constants::CellSizeInUnits); - iter = mActiveCells.begin(); + auto iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() && it->mData.mY == (*iter)->getCell()->getGridY()) { - unloadCell(iter, true); + unloadCell(*iter); break; } @@ -662,7 +677,6 @@ namespace MWWorld } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); - mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; @@ -679,7 +693,7 @@ namespace MWWorld mRendering.getResourceSystem()->setExpiryDelay(1.f); - const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store &cells = mWorld.getStore().get(); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -691,17 +705,19 @@ namespace MWWorld { loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")..."); - CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName); - loadCell (cell, loadingListener, false, true); + CellStore *cell = mWorld.getInterior(it->mName); + ESM::Position position; + mWorld.findInteriorPosition(it->mName, position); + loadCell(cell, nullptr, false, position.asVec3()); - CellStoreCollection::iterator iter = mActiveCells.begin(); + auto iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { assert (!(*iter)->getCell()->isExterior()); if (it->mName == (*iter)->getCell()->mName) { - unloadCell(iter, true); + unloadCell(*iter); break; } @@ -709,7 +725,6 @@ namespace MWWorld } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); - mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; @@ -725,20 +740,16 @@ namespace MWWorld mRendering.enableTerrain(cell->isExterior()); - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr old = world->getPlayerPtr(); - world->getPlayer().setCell(cell); + MWWorld::Ptr old = mWorld.getPlayerPtr(); + mWorld.getPlayer().setCell(cell); - MWWorld::Ptr player = world->getPlayerPtr(); + MWWorld::Ptr player = mWorld.getPlayerPtr(); mRendering.updatePlayerPtr(player); - if (adjustPlayerPos) { - world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - - float x = pos.rot[0]; - float y = pos.rot[1]; - float z = pos.rot[2]; - world->rotateObject(player, x, y, z); + if (adjustPlayerPos) + { + mWorld.moveObject(player, pos.asVec3()); + mWorld.rotateObject(player, pos.asRotationVec3()); player.getClass().adjustPosition(player, true); } @@ -748,14 +759,15 @@ namespace MWWorld mPhysics->updatePtr(old, player); - world->adjustSky(); + mWorld.adjustSky(); mLastPlayerPos = player.getRefData().getPosition().asVec3(); } - Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, + Scene::Scene(MWWorld::World& world, MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, DetourNavigator::Navigator& navigator) - : mCurrentCell (0), mCellChanged (false), mPhysics(physics), mRendering(rendering), mNavigator(navigator) + : mCurrentCell (nullptr), mCellChanged (false) + , mWorld(world), mPhysics(physics), mRendering(rendering), mNavigator(navigator) , mCellLoadingThreshold(1024.f) , mPreloadDistance(Settings::Manager::getInt("preload distance", "Cells")) , mPreloadEnabled(Settings::Manager::getBool("preload enabled", "Cells")) @@ -764,12 +776,9 @@ namespace MWWorld , mPreloadFastTravel(Settings::Manager::getBool("preload fast travel", "Cells")) , mPredictionTime(Settings::Manager::getFloat("prediction time", "Cells")) { - mPreloader.reset(new CellPreloader(rendering.getResourceSystem(), physics->getShapeManager(), rendering.getTerrain(), rendering.getLandManager())); + mPreloader = std::make_unique(rendering.getResourceSystem(), physics->getShapeManager(), rendering.getTerrain(), rendering.getLandManager()); mPreloader->setWorkQueue(mRendering.getWorkQueue()); - mPreloader->setUnrefQueue(rendering.getUnrefQueue()); - mPhysics->setUnrefQueue(rendering.getUnrefQueue()); - rendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells")); mPreloader->setExpiryDelay(Settings::Manager::getFloat("preload cell expiry delay", "Cells")); @@ -780,6 +789,11 @@ namespace MWWorld Scene::~Scene() { + for (const osg::ref_ptr& v : mWorkItems) + v->abort(); + + for (const osg::ref_ptr& v : mWorkItems) + v->waitTillDone(); } bool Scene::hasCellChanged() const @@ -794,29 +808,23 @@ namespace MWWorld void Scene::changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { - CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); + CellStore *cell = mWorld.getInterior(cellName); bool useFading = (mCurrentCell != nullptr); if (useFading) MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount(); std::string loadingInteriorText = "#{sLoadingMessage2}"; - loadingListener->setLabel(loadingInteriorText, false, messagesCount > 0); + loadingListener->setLabel(loadingInteriorText); Loading::ScopedLoad load(loadingListener); if(mCurrentCell != nullptr && *mCurrentCell == *cell) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - world->moveObject(world->getPlayerPtr(), position.pos[0], position.pos[1], position.pos[2]); - - float x = position.rot[0]; - float y = position.rot[1]; - float z = position.rot[2]; - world->rotateObject(world->getPlayerPtr(), x, y, z); + mWorld.moveObject(mWorld.getPlayerPtr(), position.asVec3()); + mWorld.rotateObject(mWorld.getPlayerPtr(), position.asRotationVec3()); if (adjustPlayerPos) - world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr(), true); + mWorld.getPlayerPtr().getClass().adjustPosition(mWorld.getPlayerPtr(), true); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); return; } @@ -824,15 +832,20 @@ namespace MWWorld Log(Debug::Info) << "Changing to interior"; // unload - CellStoreCollection::iterator active = mActiveCells.begin(); - while (active!=mActiveCells.end()) - unloadCell (active++); + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) + { + auto* cellToUnload = *iter++; + unloadCell(cellToUnload); + } + assert(mActiveCells.empty()); loadingListener->setProgressRange(cell->count()); + mNavigator.updateBounds(position.asVec3()); + // Load cell. mPagedRefs.clear(); - loadCell (cell, loadingListener, changeEvent); + loadCell(cell, loadingListener, changeEvent, position.asVec3()); changePlayerCell(cell, position, adjustPlayerPos); @@ -840,7 +853,7 @@ namespace MWWorld mRendering.configureFog(mCurrentCell->getCell()); // Sky system - MWBase::Environment::get().getWorld()->adjustSky(); + mWorld.adjustSky(); if (changeEvent) mCellChanged = true; @@ -849,25 +862,28 @@ namespace MWWorld MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); + + mNavigator.wait(*loadingListener, DetourNavigator::WaitConditionType::requiredTilesPresent); + + MWBase::Environment::get().getWorld()->getPostProcessor()->setExteriorFlag(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx); } void Scene::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { - int x = 0; - int y = 0; - - MWBase::Environment::get().getWorld()->positionToIndex (position.pos[0], position.pos[1], x, y); + const osg::Vec2i cellIndex = positionToCellIndex(position.pos[0], position.pos[1]); if (changeEvent) MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); - changeCellGrid(position.asVec3(), x, y, changeEvent); + changeCellGrid(position.asVec3(), cellIndex.x(), cellIndex.y(), changeEvent); - CellStore* current = MWBase::Environment::get().getWorld()->getExterior(x, y); + CellStore* current = mWorld.getExterior(cellIndex.x(), cellIndex.y()); changePlayerCell(current, position, adjustPlayerPos); if (changeEvent) MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); + + MWBase::Environment::get().getWorld()->getPostProcessor()->setExteriorFlag(true); } CellStore* Scene::getCurrentCell () @@ -880,28 +896,26 @@ namespace MWWorld mCellChanged = false; } - void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test) + void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener) { - InsertVisitor insertVisitor (cell, *loadingListener, test); + InsertVisitor insertVisitor(cell, loadingListener); cell.forEach (insertVisitor); - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs); }); - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); - - // do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order - PositionVisitor posVisitor; - cell.forEach (posVisitor); + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, mWorld, *mPhysics, mRendering, mPagedRefs); }); + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, mWorld, *mPhysics, mNavigator); }); } void Scene::addObjectToScene (const Ptr& ptr) { try { - addObject(ptr, *mPhysics, mRendering, mPagedRefs); - addObject(ptr, *mPhysics, mNavigator); - MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + addObject(ptr, mWorld, *mPhysics, mRendering, mPagedRefs); + addObject(ptr, mWorld, *mPhysics, mNavigator); + mWorld.scaleObject(ptr, ptr.getCellRef().getScale()); + if (mCurrentCell != nullptr) + { + const auto player = mWorld.getPlayerPtr(); + mNavigator.update(player.getRefData().getPosition().asVec3()); + } } catch (std::exception& e) { @@ -909,20 +923,23 @@ namespace MWWorld } } - void Scene::removeObjectFromScene (const Ptr& ptr) + void Scene::removeObjectFromScene (const Ptr& ptr, bool keepActive) { - MWBase::Environment::get().getMechanicsManager()->remove (ptr); + MWBase::Environment::get().getMechanicsManager()->remove (ptr, keepActive); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); if (const auto object = mPhysics->getObject(ptr)) { - navigator->removeObject(DetourNavigator::ObjectId(object)); - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + mNavigator.removeObject(DetourNavigator::ObjectId(object)); + if (mCurrentCell != nullptr) + { + const auto player = mWorld.getPlayerPtr(); + mNavigator.update(player.getRefData().getPosition().asVec3()); + } } else if (mPhysics->getActor(ptr)) { - navigator->removeAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); + mNavigator.removeAgent(mWorld.getPathfindingAgentBounds(ptr)); } mPhysics->remove(ptr); mRendering.removeObject (ptr); @@ -963,17 +980,27 @@ namespace MWWorld void doWork() override { + if (mAborted) + return; + try { mSceneManager->getTemplate(mMesh); } - catch (std::exception& e) + catch (std::exception&) { } } + + void abort() override + { + mAborted = true; + } + private: std::string mMesh; Resource::SceneManager* mSceneManager; + std::atomic_bool mAborted {false}; }; void Scene::preload(const std::string &mesh, bool useAnim) @@ -983,7 +1010,13 @@ namespace MWWorld mesh_ = Misc::ResourceHelpers::correctActorModelPath(mesh_, mRendering.getResourceSystem()->getVFS()); if (!mRendering.getResourceSystem()->getSceneManager()->checkLoaded(mesh_, mRendering.getReferenceTime())) - mRendering.getWorkQueue()->addWorkItem(new PreloadMeshItem(mesh_, mRendering.getResourceSystem()->getSceneManager())); + { + osg::ref_ptr item(new PreloadMeshItem(mesh_, mRendering.getResourceSystem()->getSceneManager())); + mRendering.getWorkQueue()->addWorkItem(item); + const auto isDone = [] (const osg::ref_ptr& v) { return v->isDone(); }; + mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end()); + mWorkItems.emplace_back(std::move(item)); + } } void Scene::preloadCells(float dt) @@ -991,7 +1024,7 @@ namespace MWWorld if (dt<=1e-06) return; std::vector exteriorPositions; - const MWWorld::ConstPtr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + const MWWorld::ConstPtr player = mWorld.getPlayerPtr(); osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); osg::Vec3f moved = playerPos - mLastPlayerPos; osg::Vec3f predictedPos = playerPos + moved / dt * mPredictionTime; @@ -1041,17 +1074,16 @@ namespace MWWorld try { if (!door.getCellRef().getDestCell().empty()) - preloadCell(MWBase::Environment::get().getWorld()->getInterior(door.getCellRef().getDestCell())); + preloadCell(mWorld.getInterior(door.getCellRef().getDestCell())); else { osg::Vec3f pos = door.getCellRef().getDoorDest().asVec3(); - int x,y; - MWBase::Environment::get().getWorld()->positionToIndex (pos.x(), pos.y(), x, y); - preloadCell(MWBase::Environment::get().getWorld()->getExterior(x,y), true); + const osg::Vec2i cellIndex = positionToCellIndex(pos.x(), pos.y()); + preloadCell(mWorld.getExterior(cellIndex.x(), cellIndex.y()), true); exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } - catch (std::exception& e) + catch (std::exception&) { // ignore error for now, would spam the log too much } @@ -1061,7 +1093,7 @@ namespace MWWorld void Scene::preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos) { - if (!MWBase::Environment::get().getWorld()->isCellExterior()) + if (!mWorld.isCellExterior()) return; int halfGridSizePlusOne = mHalfGridSize + 1; @@ -1071,7 +1103,7 @@ namespace MWWorld cellX = mCurrentGridCenter.x(); cellY = mCurrentGridCenter.y(); float centerX, centerY; - MWBase::Environment::get().getWorld()->indexToPosition(cellX, cellY, centerX, centerY, true); + mWorld.indexToPosition(cellX, cellY, centerX, centerY, true); for (int dx = -halfGridSizePlusOne; dx <= halfGridSizePlusOne; ++dx) { @@ -1081,14 +1113,14 @@ namespace MWWorld continue; // only care about the outer (not yet loaded) part of the grid float thisCellCenterX, thisCellCenterY; - MWBase::Environment::get().getWorld()->indexToPosition(cellX+dx, cellY+dy, thisCellCenterX, thisCellCenterY, true); + mWorld.indexToPosition(cellX+dx, cellY+dy, thisCellCenterX, thisCellCenterY, true); float dist = std::max(std::abs(thisCellCenterX - playerPos.x()), std::abs(thisCellCenterY - playerPos.y())); dist = std::min(dist,std::max(std::abs(thisCellCenterX - predictedPos.x()), std::abs(thisCellCenterY - predictedPos.y()))); float loadDist = Constants::CellSizeInUnits / 2 + Constants::CellSizeInUnits - mCellLoadingThreshold + mPreloadDistance; if (dist < loadDist) - preloadCell(MWBase::Environment::get().getWorld()->getExterior(cellX+dx, cellY+dy)); + preloadCell(mWorld.getExterior(cellX+dx, cellY+dy)); } } } @@ -1104,7 +1136,7 @@ namespace MWWorld { for (int dy = -mHalfGridSize; dy <= mHalfGridSize; ++dy) { - mPreloader->preload(MWBase::Environment::get().getWorld()->getExterior(x+dx, y+dy), mRendering.getReferenceTime()); + mPreloader->preload(mWorld.getExterior(x+dx, y+dy), mRendering.getReferenceTime()); if (++numpreloaded >= mPreloader->getMaxCacheSize()) break; } @@ -1118,32 +1150,16 @@ namespace MWWorld { std::vector vec; vec.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); - if (sync && mRendering.pagingUnlockCache()) - mPreloader->abortTerrainPreloadExcept(nullptr); - else - mPreloader->abortTerrainPreloadExcept(&vec[0]); + mPreloader->abortTerrainPreloadExcept(&vec[0]); mPreloader->setTerrainPreloadPositions(vec); if (!sync) return; Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); - int progress = 0, initialProgress = -1, progressRange = 0; - while (!mPreloader->syncTerrainLoad(vec, progress, progressRange, mRendering.getReferenceTime())) - { - if (initialProgress == -1) - { - loadingListener->setLabel("#{sLoadingMessage4}"); - initialProgress = progress; - } - if (progress) - { - loadingListener->setProgressRange(std::max(0, progressRange-initialProgress)); - loadingListener->setProgress(progress-initialProgress); - } - else - loadingListener->setProgress(0); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } + + loadingListener->setLabel("#{sLoadingMessage4}"); + + while (!mPreloader->syncTerrainLoad(vec, mRendering.getReferenceTime(), *loadingListener)) {} } void Scene::reloadTerrain() @@ -1183,7 +1199,7 @@ namespace MWWorld void Scene::preloadFastTravelDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& /*predictedPos*/, std::vector& exteriorPositions) // ignore predictedPos here since opening dialogue with travel service takes extra time { - const MWWorld::ConstPtr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + const MWWorld::ConstPtr player = mWorld.getPlayerPtr(); ListFastTravelDestinationsVisitor listVisitor(mPreloadDistance, player.getRefData().getPosition().asVec3()); for (MWWorld::CellStore* cellStore : mActiveCells) @@ -1195,13 +1211,12 @@ namespace MWWorld for (ESM::Transport::Dest& dest : listVisitor.mList) { if (!dest.mCellName.empty()) - preloadCell(MWBase::Environment::get().getWorld()->getInterior(dest.mCellName)); + preloadCell(mWorld.getInterior(dest.mCellName)); else { osg::Vec3f pos = dest.mPos.asVec3(); - int x,y; - MWBase::Environment::get().getWorld()->positionToIndex( pos.x(), pos.y(), x, y); - preloadCell(MWBase::Environment::get().getWorld()->getExterior(x,y), true); + const osg::Vec2i cellIndex = positionToCellIndex(pos.x(), pos.y()); + preloadCell(mWorld.getExterior(cellIndex.x(), cellIndex.y()), true); exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index a70d3ccdd5..c504a2cf73 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "ptr.hpp" #include "globals.hpp" @@ -10,6 +11,8 @@ #include #include #include +#include +#include #include @@ -36,7 +39,6 @@ namespace Loading namespace DetourNavigator { struct Navigator; - class Water; } namespace MWRender @@ -50,11 +52,17 @@ namespace MWPhysics class PhysicsSystem; } +namespace SceneUtil +{ + class WorkItem; +} + namespace MWWorld { class Player; class CellStore; class CellPreloader; + class World; enum class RotationOrder { @@ -65,14 +73,20 @@ namespace MWWorld class Scene { public: - - typedef std::set CellStoreCollection; + using CellStoreCollection = std::set; private: + struct ChangeCellGridRequest + { + osg::Vec3f mPosition; + osg::Vec2i mCell; + bool mChangeEvent; + }; CellStore* mCurrentCell; // the cell the player is in CellStoreCollection mActiveCells; bool mCellChanged; + MWWorld::World& mWorld; MWPhysics::PhysicsSystem *mPhysics; MWRender::RenderingManager& mRendering; DetourNavigator::Navigator& mNavigator; @@ -92,12 +106,18 @@ namespace MWWorld std::set mPagedRefs; - void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false); + std::vector> mWorkItems; + + std::optional mChangeCellGridRequest; + + void insertCell(CellStore &cell, Loading::Listener* loadingListener); osg::Vec2i mCurrentGridCenter; // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center void changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent = true); + void requestChangeCellGrid(const osg::Vec3f &position, const osg::Vec2i& cell, bool changeEvent = true); + typedef std::pair PositionCellGrid; void preloadCells(float dt); @@ -108,9 +128,12 @@ namespace MWWorld osg::Vec4i gridCenterToBounds(const osg::Vec2i ¢erCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f &pos, const osg::Vec2i *currentGridCenter = nullptr) const; + void unloadCell(CellStore* cell); + void loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn, const osg::Vec3f& position); + public: - Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, + Scene(MWWorld::World& world, MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, DetourNavigator::Navigator& navigator); ~Scene(); @@ -119,13 +142,9 @@ namespace MWWorld void preloadTerrain(const osg::Vec3f& pos, bool sync=false); void reloadTerrain(); - void unloadCell (CellStoreCollection::iterator iter, bool test = false); - - void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false); - void playerMoved (const osg::Vec3f& pos); - void changePlayerCell (CellStore* newCell, const ESM::Position& position, bool adjustPlayerPos); + void changePlayerCell(CellStore* newCell, const ESM::Position& position, bool adjustPlayerPos); CellStore *getCurrentCell(); @@ -147,14 +166,16 @@ namespace MWWorld void markCellAsUnchanged(); - void update (float duration, bool paused); + void update(float duration); void addObjectToScene (const Ptr& ptr); ///< Add an object that already exists in the world model to the scene. - void removeObjectFromScene (const Ptr& ptr); + void removeObjectFromScene (const Ptr& ptr, bool keepActive = false); ///< Remove an object from the scene, but not from the world model. + void addPostponedPhysicsObjects(); + void removeFromPagedRefs(const Ptr &ptr); void updateObjectRotation(const Ptr& ptr, RotationOrder order); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index f8ec8c7c27..794b87bdfd 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -2,50 +2,15 @@ #include -#include -#include +#include +#include #include #include +#include #include - -namespace -{ - template - class GetRecords - { - const std::string mFind; - std::vector *mRecords; - - public: - GetRecords(const std::string &str, std::vector *records) - : mFind(Misc::StringUtils::lowerCase(str)), mRecords(records) - { } - - void operator()(const T *item) - { - if(Misc::StringUtils::ciCompareLen(mFind, item->mId, mFind.size()) == 0) - mRecords->push_back(item); - } - }; - - struct Compare - { - bool operator()(const ESM::Land *x, const ESM::Land *y) { - if (x->mX == y->mX) { - return x->mY < y->mY; - } - return x->mX < y->mX; - } - bool operator()(const ESM::Land *x, const std::pair& y) { - if (x->mX == y.first) { - return x->mY < y.second; - } - return x->mX < y.first; - } - }; -} +#include namespace MWWorld { @@ -53,16 +18,16 @@ namespace MWWorld : mId(id), mIsDeleted(isDeleted) {} - template + template IndexedStore::IndexedStore() { } - template + template typename IndexedStore::iterator IndexedStore::begin() const { return mStatic.begin(); } - template + template typename IndexedStore::iterator IndexedStore::end() const { return mStatic.end(); @@ -74,11 +39,8 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - - // Try to overwrite existing record - std::pair ret = mStatic.insert(std::make_pair(record.mIndex, record)); - if (!ret.second) - ret.first->second = record; + auto idx = record.mIndex; + mStatic.insert_or_assign(idx, std::move(record)); } template int IndexedStore::getSize() const @@ -101,10 +63,11 @@ namespace MWWorld const T *IndexedStore::find(int index) const { const T *ptr = search(index); - if (ptr == 0) + if (ptr == nullptr) { - const std::string msg = T::getRecordType() + " with index " + std::to_string(index) + " not found"; - throw std::runtime_error(msg); + std::stringstream msg; + msg << T::getRecordType() << " with index " << index << " not found"; + throw std::runtime_error(msg.str()); } return ptr; } @@ -136,27 +99,24 @@ namespace MWWorld template const T *Store::search(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename Dynamic::const_iterator dit = mDynamic.find(idLower); + typename Dynamic::const_iterator dit = mDynamic.find(id); if (dit != mDynamic.end()) return &dit->second; - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); - return 0; + return nullptr; } template const T *Store::searchStatic(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); - return 0; + return nullptr; } template @@ -166,33 +126,27 @@ namespace MWWorld return (dit != mDynamic.end()); } template - const T *Store::searchRandom(const std::string &id) const + const T *Store::searchRandom(const std::string &id, Misc::Rng::Generator& prng) const { std::vector results; - std::for_each(mShared.begin(), mShared.end(), GetRecords(id, &results)); + std::copy_if(mShared.begin(), mShared.end(), std::back_inserter(results), + [&id](const T* item) + { + return Misc::StringUtils::ciCompareLen(id, item->mId, id.size()) == 0; + }); if(!results.empty()) - return results[Misc::Rng::rollDice(results.size())]; + return results[Misc::Rng::rollDice(results.size(), prng)]; return nullptr; } template const T *Store::find(const std::string &id) const { const T *ptr = search(id); - if (ptr == 0) - { - const std::string msg = T::getRecordType() + " '" + id + "' not found"; - throw std::runtime_error(msg); - } - return ptr; - } - template - const T *Store::findRandom(const std::string &id) const - { - const T *ptr = searchRandom(id); - if(ptr == 0) + if (ptr == nullptr) { - const std::string msg = T::getRecordType() + " starting with '" + id + "' not found"; - throw std::runtime_error(msg); + std::stringstream msg; + msg << T::getRecordType() << " '" << id << "' not found"; + throw std::runtime_error(msg.str()); } return ptr; } @@ -203,13 +157,11 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - Misc::StringUtils::lowerCaseInPlace(record.mId); + Misc::StringUtils::lowerCaseInPlace(record.mId); // TODO: remove this line once we have ported our remaining code base to lowercase on lookup - std::pair inserted = mStatic.insert(std::make_pair(record.mId, record)); + std::pair inserted = mStatic.insert_or_assign(record.mId, record); if (inserted.second) mShared.push_back(&inserted.first->second); - else - inserted.first->second = record; return RecordId(record.mId, isDeleted); } @@ -221,7 +173,7 @@ namespace MWWorld template typename Store::iterator Store::begin() const { - return mShared.begin(); + return mShared.begin(); } template typename Store::iterator Store::end() const @@ -250,39 +202,33 @@ namespace MWWorld } } template - T *Store::insert(const T &item) + T *Store::insert(const T &item, bool overrideOnly) { - std::string id = Misc::StringUtils::lowerCase(item.mId); - std::pair result = - mDynamic.insert(std::pair(id, item)); + if(overrideOnly) + { + auto it = mStatic.find(item.mId); + if(it == mStatic.end()) + return nullptr; + } + std::pair result = mDynamic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; - if (result.second) { + if (result.second) mShared.push_back(ptr); - } else { - *ptr = item; - } return ptr; } template T *Store::insertStatic(const T &item) { - std::string id = Misc::StringUtils::lowerCase(item.mId); - std::pair result = - mStatic.insert(std::pair(id, item)); + std::pair result = mStatic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; - if (result.second) { + if (result.second) mShared.push_back(ptr); - } else { - *ptr = item; - } return ptr; } template bool Store::eraseStatic(const std::string &id) { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename std::map::iterator it = mStatic.find(idLower); + typename Static::iterator it = mStatic.find(id); if (it != mStatic.end()) { // delete from the static part of mShared @@ -290,7 +236,7 @@ namespace MWWorld typename std::vector::iterator end = sharedIter + mStatic.size(); while (sharedIter != mShared.end() && sharedIter != end) { - if((*sharedIter)->mId == idLower) { + if(Misc::StringUtils::ciEqual((*sharedIter)->mId, id)) { mShared.erase(sharedIter); break; } @@ -305,17 +251,13 @@ namespace MWWorld template bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - typename Dynamic::iterator it = mDynamic.find(key); - if (it == mDynamic.end()) { + if (!mDynamic.erase(id)) return false; - } - mDynamic.erase(it); // have to reinit the whole shared part assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); - for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { + for (auto it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); } return true; @@ -337,13 +279,13 @@ namespace MWWorld } } template - RecordId Store::read(ESM::ESMReader& reader) + RecordId Store::read(ESM::ESMReader& reader, bool overrideOnly) { T record; bool isDeleted = false; record.load (reader, isDeleted); - insert (record); + insert (record, overrideOnly); return RecordId(record.mId, isDeleted); } @@ -352,11 +294,6 @@ namespace MWWorld //========================================================================= Store::Store() { - mStatic.emplace_back(); - LandTextureList <exl = mStatic[0]; - // More than enough to hold Morrowind.esm. Extra lists for plugins will we - // added on-the-fly in a different method. - ltexl.reserve(128); } const ESM::LandTexture *Store::search(size_t index, size_t plugin) const { @@ -370,7 +307,7 @@ namespace MWWorld const ESM::LandTexture *Store::find(size_t index, size_t plugin) const { const ESM::LandTexture *ptr = search(index, plugin); - if (ptr == 0) + if (ptr == nullptr) { const std::string msg = "Land texture with index " + std::to_string(index) + " not found"; throw std::runtime_error(msg); @@ -386,42 +323,33 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].size(); } - RecordId Store::load(ESM::ESMReader &esm, size_t plugin) + RecordId Store::load(ESM::ESMReader &esm) { ESM::LandTexture lt; bool isDeleted = false; lt.load(esm, isDeleted); - assert(plugin < mStatic.size()); - // Replace texture for records with given ID and index from all plugins. for (unsigned int i=0; i(search(lt.mIndex, i)); if (tex) { - const std::string texId = Misc::StringUtils::lowerCase(tex->mId); - const std::string ltId = Misc::StringUtils::lowerCase(lt.mId); - if (texId == ltId) - { + if (Misc::StringUtils::ciEqual(tex->mId, lt.mId)) tex->mTexture = lt.mTexture; - } } } - LandTextureList <exl = mStatic[plugin]; + LandTextureList <exl = mStatic.back(); if(lt.mIndex + 1 > (int)ltexl.size()) ltexl.resize(lt.mIndex+1); // Store it - ltexl[lt.mIndex] = lt; + auto idx = lt.mIndex; + ltexl[idx] = std::move(lt); - return RecordId(lt.mId, isDeleted); - } - RecordId Store::load(ESM::ESMReader &esm) - { - return load(esm, esm.getIndex()); + return RecordId(ltexl[idx].mId, isDeleted); } Store::iterator Store::begin(size_t plugin) const { @@ -433,21 +361,11 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].end(); } - void Store::resize(size_t num) - { - if (mStatic.size() < num) - mStatic.resize(num); - } - + // Land //========================================================================= Store::~Store() { - for (const ESM::Land* staticLand : mStatic) - { - delete staticLand; - } - } size_t Store::getSize() const { @@ -464,19 +382,14 @@ namespace MWWorld const ESM::Land *Store::search(int x, int y) const { std::pair comp(x,y); - - std::vector::const_iterator it = - std::lower_bound(mStatic.begin(), mStatic.end(), comp, Compare()); - - if (it != mStatic.end() && (*it)->mX == x && (*it)->mY == y) { - return *it; - } - return 0; + if (auto it = mStatic.find(comp); it != mStatic.end() && it->mX == x && it->mY == y) + return &*it; + return nullptr; } const ESM::Land *Store::find(int x, int y) const { const ESM::Land *ptr = search(x, y); - if (ptr == 0) + if (ptr == nullptr) { const std::string msg = "Land at (" + std::to_string(x) + ", " + std::to_string(y) + ") not found"; throw std::runtime_error(msg); @@ -485,24 +398,21 @@ namespace MWWorld } RecordId Store::load(ESM::ESMReader &esm) { - ESM::Land *ptr = new ESM::Land(); + ESM::Land land; bool isDeleted = false; - ptr->load(esm, isDeleted); + land.load(esm, isDeleted); // Same area defined in multiple plugins? -> last plugin wins - // Can't use search() because we aren't sorted yet - is there any other way to speed this up? - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + auto it = mStatic.lower_bound(land); + if (it != mStatic.end() && (std::tie(it->mX, it->mY) == std::tie(land.mX, land.mY))) { - if ((*it)->mX == ptr->mX && (*it)->mY == ptr->mY) - { - delete *it; - mStatic.erase(it); - break; - } + auto nh = mStatic.extract(it); + nh.value() = std::move(land); + mStatic.insert(std::move(nh)); } - - mStatic.push_back(ptr); + else + mStatic.insert(it, std::move(land)); return RecordId("", isDeleted); } @@ -512,7 +422,6 @@ namespace MWWorld if (mBuilt) return; - std::sort(mStatic.begin(), mStatic.end(), Compare()); mBuilt = true; } @@ -527,20 +436,28 @@ namespace MWWorld } return search(cell.mName); } + + // this method *must* be called right after esm3.loadCell() void Store::handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell) { - //Handling MovedCellRefs, there is no way to do it inside loadcell - while (esm.isNextSub("MVRF")) { - ESM::CellRef ref; - ESM::MovedCellRef cMRef; - cell->getNextMVRF(esm, cMRef); + ESM::CellRef ref; + ESM::MovedCellRef cMRef; + bool deleted = false; + bool moved = false; - ESM::Cell *cellAlt = const_cast(searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); + ESM::ESM_Context ctx = esm.getContext(); - // Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following - // implementation when the oher implementation works as well. - bool deleted = false; - cell->getNextRef(esm, ref, deleted); + // Handling MovedCellRefs, there is no way to do it inside loadcell + // TODO: verify above comment + // + // Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following + // implementation when the oher implementation works as well. + while (ESM::Cell::getNextRef(esm, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyMoved)) + { + if (!moved) + continue; + + ESM::Cell *cellAlt = const_cast(searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); // Add data required to make reference appear in the correct cell. // We should not need to test for duplicates, as this part of the code is pre-cell merge. @@ -549,71 +466,59 @@ namespace MWWorld // But there may be duplicates here! ESM::CellRefTracker::iterator iter = std::find_if(cellAlt->mLeasedRefs.begin(), cellAlt->mLeasedRefs.end(), ESM::CellRefTrackerPredicate(ref.mRefNum)); if (iter == cellAlt->mLeasedRefs.end()) - cellAlt->mLeasedRefs.push_back(std::make_pair(ref, deleted)); + cellAlt->mLeasedRefs.emplace_back(std::move(ref), deleted); else - *iter = std::make_pair(ref, deleted); + *iter = std::make_pair(std::move(ref), deleted); + + cMRef.mRefNum.mIndex = 0; } + + esm.restoreContext(ctx); } const ESM::Cell *Store::search(const std::string &id) const { - ESM::Cell cell; - cell.mName = Misc::StringUtils::lowerCase(id); - - std::map::const_iterator it = mInt.find(cell.mName); - + DynamicInt::const_iterator it = mInt.find(id); if (it != mInt.end()) { return &(it->second); } - DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); + DynamicInt::const_iterator dit = mDynamicInt.find(id); if (dit != mDynamicInt.end()) { return &dit->second; } - return 0; + return nullptr; } const ESM::Cell *Store::search(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x, cell.mData.mY = y; - std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } - return 0; + return nullptr; } const ESM::Cell *Store::searchStatic(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x, cell.mData.mY = y; - - std::pair key(x, y); - DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + DynamicExt::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) return &(it->second); - } - return 0; + return nullptr; } const ESM::Cell *Store::searchOrCreate(int x, int y) { std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } ESM::Cell newCell; newCell.mData.mX = x; @@ -623,12 +528,16 @@ namespace MWWorld newCell.mAmbi.mSunlight = 0; newCell.mAmbi.mFog = 0; newCell.mAmbi.mFogDensity = 0; + newCell.mCellId.mPaged = true; + newCell.mCellId.mIndex.mX = x; + newCell.mCellId.mIndex.mY = y; + return &mExt.insert(std::make_pair(key, newCell)).first->second; } const ESM::Cell *Store::find(const std::string &id) const { const ESM::Cell *ptr = search(id); - if (ptr == 0) + if (ptr == nullptr) { const std::string msg = "Cell '" + id + "' not found"; throw std::runtime_error(msg); @@ -638,7 +547,7 @@ namespace MWWorld const ESM::Cell *Store::find(int x, int y) const { const ESM::Cell *ptr = search(x, y); - if (ptr == 0) + if (ptr == nullptr) { const std::string msg = "Exterior at (" + std::to_string(x) + ", " + std::to_string(y) + ") not found"; throw std::runtime_error(msg); @@ -652,20 +561,15 @@ namespace MWWorld void Store::setUp() { - typedef DynamicExt::iterator ExtIterator; - typedef std::map::iterator IntIterator; - mSharedInt.clear(); mSharedInt.reserve(mInt.size()); - for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) { - mSharedInt.push_back(&(it->second)); - } + for (auto & [_, cell] : mInt) + mSharedInt.push_back(&cell); mSharedExt.clear(); mSharedExt.reserve(mExt.size()); - for (ExtIterator it = mExt.begin(); it != mExt.end(); ++it) { - mSharedExt.push_back(&(it->second)); - } + for (auto & [_, cell] : mExt) + mSharedExt.push_back(&cell); } RecordId Store::load(ESM::ESMReader &esm) { @@ -678,15 +582,14 @@ namespace MWWorld ESM::Cell cell; bool isDeleted = false; - // Load the (x,y) coordinates of the cell, if it is an exterior cell, + // Load the (x,y) coordinates of the cell, if it is an exterior cell, // so we can find the cell we need to merge with cell.loadNameAndData(esm, isDeleted); - std::string idLower = Misc::StringUtils::lowerCase(cell.mName); if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. - ESM::Cell *oldcell = const_cast(search(idLower)); + ESM::Cell *oldcell = const_cast(search(cell.mName)); if (oldcell) { // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) @@ -698,7 +601,7 @@ namespace MWWorld // spawn a new cell cell.loadCell(esm, true); - mInt[idLower] = cell; + mInt[cell.mName] = cell; } } else @@ -781,7 +684,7 @@ namespace MWWorld { if (Misc::StringUtils::ciEqual(sharedCell->mName, id)) { - if (cell == 0 || + if (cell == nullptr || (sharedCell->mData.mX > cell->mData.mX) || (sharedCell->mData.mX == cell->mData.mX && sharedCell->mData.mY > cell->mData.mY)) { @@ -831,32 +734,24 @@ namespace MWWorld } ESM::Cell *Store::insert(const ESM::Cell &cell) { - if (search(cell) != 0) + if (search(cell) != nullptr) { const std::string cellType = (cell.isExterior()) ? "exterior" : "interior"; throw std::runtime_error("Failed to create " + cellType + " cell"); } - ESM::Cell *ptr; if (cell.isExterior()) { std::pair key(cell.getGridX(), cell.getGridY()); // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicExt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedExt.push_back(ptr); + DynamicExt::iterator result = mDynamicExt.emplace(key, cell).first; + mSharedExt.push_back(&result->second); + return &result->second; } else { - std::string key = Misc::StringUtils::lowerCase(cell.mName); - // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicInt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedInt.push_back(ptr); + DynamicInt::iterator result = mDynamicInt.emplace(cell.mName, cell).first; + mSharedInt.push_back(&result->second); + return &result->second; } - return ptr; } bool Store::erase(const ESM::Cell &cell) { @@ -867,8 +762,7 @@ namespace MWWorld } bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - DynamicInt::iterator it = mDynamicInt.find(key); + DynamicInt::iterator it = mDynamicInt.find(id); if (it == mDynamicInt.end()) { return false; @@ -906,7 +800,7 @@ namespace MWWorld return true; } - + // Pathgrid //========================================================================= @@ -931,7 +825,27 @@ namespace MWWorld // mX and mY will be (0,0) for interior cells, but there is also an exterior cell with the coordinates of (0,0), so that doesn't help. // Check whether mCell is an interior cell. This isn't perfect, will break if a Region with the same name as an interior cell is created. // A proper fix should be made for future versions of the file format. - bool interior = mCells->search(pathgrid.mCell) != nullptr; + bool interior = pathgrid.mData.mX == 0 && pathgrid.mData.mY == 0 && mCells->search(pathgrid.mCell) != nullptr; + + // deal with mods that have empty pathgrid records (Issue #6209) + // we assume that these records are empty on purpose (i.e. to remove old pathgrid on an updated cell) + if (isDeleted || pathgrid.mPoints.empty() || pathgrid.mEdges.empty()) + { + if (interior) + { + Interior::iterator it = mInt.find(pathgrid.mCell); + if (it != mInt.end()) + mInt.erase(it); + } + else + { + Exterior::iterator it = mExt.find(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY)); + if (it != mExt.end()) + mExt.erase(it); + } + + return RecordId("", isDeleted); + } // Try to overwrite existing record if (interior) @@ -1008,7 +922,7 @@ namespace MWWorld // Skill //========================================================================= - + Store::Store() { } @@ -1032,15 +946,15 @@ namespace MWWorld const ESM::Attribute *Store::search(size_t index) const { if (index >= mStatic.size()) { - return 0; + return nullptr; } - return &mStatic.at(index); + return &mStatic[index]; } const ESM::Attribute *Store::find(size_t index) const { const ESM::Attribute *ptr = search(index); - if (ptr == 0) + if (ptr == nullptr) { const std::string msg = "Attribute with index " + std::to_string(index) + " not found"; throw std::runtime_error(msg); @@ -1049,7 +963,7 @@ namespace MWWorld } void Store::setUp() { - for (int i = 0; i < ESM::Attribute::Length; ++i) + for (int i = 0; i < ESM::Attribute::Length; ++i) { ESM::Attribute newAttribute; newAttribute.mId = ESM::Attribute::sAttributeIds[i]; @@ -1071,31 +985,69 @@ namespace MWWorld return mStatic.end(); } - + // Dialogue //========================================================================= + Store::Store() + : mKeywordSearchModFlag(true) + { + } - template<> void Store::setUp() { // DialInfos marked as deleted are kept during the loading phase, so that the linked list // structure is kept intact for inserting further INFOs. Delete them now that loading is done. - for (Static::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - ESM::Dialogue& dial = it->second; + for (auto & [_, dial] : mStatic) dial.clearDeletedInfos(); - } mShared.clear(); mShared.reserve(mStatic.size()); - std::map::iterator it = mStatic.begin(); - for (; it != mStatic.end(); ++it) { - mShared.push_back(&(it->second)); + for (auto & [_, dial] : mStatic) + mShared.push_back(&dial); + // TODO: verify and document this inconsistent behaviour + // TODO: if we require this behaviour, maybe we should move it to the place that requires it + std::sort(mShared.begin(), mShared.end(), [](const ESM::Dialogue* l, const ESM::Dialogue* r) -> bool { return l->mId < r->mId; }); + + mKeywordSearchModFlag = true; + } + + const ESM::Dialogue *Store::search(const std::string &id) const + { + typename Static::const_iterator it = mStatic.find(id); + if (it != mStatic.end()) + return &(it->second); + + return nullptr; + } + + const ESM::Dialogue *Store::find(const std::string &id) const + { + const ESM::Dialogue *ptr = search(id); + if (ptr == nullptr) + { + std::stringstream msg; + msg << ESM::Dialogue::getRecordType() << " '" << id << "' not found"; + throw std::runtime_error(msg.str()); } + return ptr; + } + + typename Store::iterator Store::begin() const + { + return mShared.begin(); + } + + typename Store::iterator Store::end() const + { + return mShared.end(); + } + + size_t Store::getSize() const + { + return mShared.size(); } - template <> inline RecordId Store::load(ESM::ESMReader &esm) { // The original letter case of a dialogue ID is saved, because it's printed ESM::Dialogue dialogue; @@ -1103,33 +1055,58 @@ namespace MWWorld dialogue.loadId(esm); - std::string idLower = Misc::StringUtils::lowerCase(dialogue.mId); - std::map::iterator found = mStatic.find(idLower); + Static::iterator found = mStatic.find(dialogue.mId); if (found == mStatic.end()) { dialogue.loadData(esm, isDeleted); - mStatic.insert(std::make_pair(idLower, dialogue)); + mStatic.emplace(dialogue.mId, dialogue); } else { found->second.loadData(esm, isDeleted); - dialogue = found->second; + dialogue.mId = found->second.mId; } + + mKeywordSearchModFlag = true; return RecordId(dialogue.mId, isDeleted); } - template<> bool Store::eraseStatic(const std::string &id) { - auto it = mStatic.find(Misc::StringUtils::lowerCase(id)); - - if (it != mStatic.end()) - mStatic.erase(it); + if (mStatic.erase(id)) + mKeywordSearchModFlag = true; return true; } + void Store::listIdentifier(std::vector& list) const + { + list.reserve(list.size() + getSize()); + for (const auto& dialogue : mShared) + list.push_back(dialogue->mId); + } + + const MWDialogue::KeywordSearch& Store::getDialogIdKeywordSearch() const + { + if (mKeywordSearchModFlag) + { + mKeywordSearch.clear(); + + std::vector keywordList; + keywordList.reserve(getSize()); + for (const auto& it : *this) + keywordList.push_back(Misc::StringUtils::lowerCase(it.mId)); + sort(keywordList.begin(), keywordList.end()); + + for (const auto& it : keywordList) + mKeywordSearch.seed(it, 0 /*unused*/); + + mKeywordSearchModFlag = false; + } + + return mKeywordSearch; + } } template class MWWorld::Store; @@ -1145,7 +1122,7 @@ template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; -template class MWWorld::Store; +//template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index d2406b602b..29e1b53a72 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -3,9 +3,16 @@ #include #include +#include #include +#include +#include -#include "recordcmp.hpp" +#include +#include +#include + +#include "../mwdialogue/keywordsearch.hpp" namespace ESM { @@ -46,7 +53,7 @@ namespace MWWorld virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const {} - virtual RecordId read (ESM::ESMReader& reader) { return RecordId(); } + virtual RecordId read (ESM::ESMReader& reader, bool overrideOnly = false) { return RecordId(); } ///< Read into dynamic storage }; @@ -71,13 +78,15 @@ namespace MWWorld void setUp(); const T *search(int index) const; + + // calls `search` and throws an exception if not found const T *find(int index) const; }; - template + template > class SharedIterator { - typedef typename std::vector::const_iterator Iter; + typedef typename Container::const_iterator Iter; Iter mIter; @@ -145,14 +154,14 @@ namespace MWWorld template class Store : public StoreBase { - std::map mStatic; - std::vector mShared; // Preserves the record order as it came from the content files (this - // is relevant for the spell autocalc code and selection order - // for heads/hairs in the character creation) - std::map mDynamic; - - typedef std::map Dynamic; - typedef std::map Static; + typedef std::unordered_map Static; + Static mStatic; + /// @par mShared usually preserves the record order as it came from the content files (this + /// is relevant for the spell autocalc code and selection order + /// for heads/hairs in the character creation) + std::vector mShared; + typedef std::unordered_map Dynamic; + Dynamic mDynamic; friend class ESMStore; @@ -175,14 +184,11 @@ namespace MWWorld bool isDynamic(const std::string &id) const; /** Returns a random record that starts with the named ID, or nullptr if not found. */ - const T *searchRandom(const std::string &id) const; + const T *searchRandom(const std::string &id, Misc::Rng::Generator& prng) const; + // calls `search` and throws an exception if not found const T *find(const std::string &id) const; - /** Returns a random record that starts with the named ID. An exception is thrown if none - * are found. */ - const T *findRandom(const std::string &id) const; - iterator begin() const; iterator end() const; @@ -192,7 +198,7 @@ namespace MWWorld /// @note The record identifiers are listed in the order that the records were defined by the content files. void listIdentifier(std::vector &list) const override; - T *insert(const T &item); + T *insert(const T &item, bool overrideOnly = false); T *insertStatic(const T &item); bool eraseStatic(const std::string &id) override; @@ -201,7 +207,7 @@ namespace MWWorld RecordId load(ESM::ESMReader &esm) override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; - RecordId read(ESM::ESMReader& reader) override; + RecordId read(ESM::ESMReader& reader, bool overrideOnly = false) override; }; template <> @@ -221,13 +227,11 @@ namespace MWWorld const ESM::LandTexture *search(size_t index, size_t plugin) const; const ESM::LandTexture *find(size_t index, size_t plugin) const; - /// Resize the internal store to hold at least \a num plugins. - void resize(size_t num); + void resize(size_t num) { mStatic.resize(num); } size_t getSize() const override; size_t getSize(size_t plugin) const; - RecordId load(ESM::ESMReader &esm, size_t plugin); RecordId load(ESM::ESMReader &esm) override; iterator begin(size_t plugin) const; @@ -237,10 +241,28 @@ namespace MWWorld template <> class Store : public StoreBase { - std::vector mStatic; + struct SpatialComparator + { + using is_transparent = void; + + bool operator()(const ESM::Land& x, const ESM::Land& y) const + { + return std::tie(x.mX, x.mY) < std::tie(y.mX, y.mY); + } + bool operator()(const ESM::Land& x, const std::pair& y) const + { + return std::tie(x.mX, x.mY) < std::tie(y.first, y.second); + } + bool operator()(const std::pair& x, const ESM::Land& y) const + { + return std::tie(x.first, x.second) < std::tie(y.mX, y.mY); + } + }; + using Statics = std::set; + Statics mStatic; public: - typedef SharedIterator iterator; + typedef typename Statics::iterator iterator; virtual ~Store(); @@ -278,7 +300,7 @@ namespace MWWorld } }; - typedef std::map DynamicInt; + typedef std::unordered_map DynamicInt; typedef std::map, ESM::Cell, DynamicExtCmp> DynamicExt; DynamicInt mInt; @@ -338,7 +360,7 @@ namespace MWWorld class Store : public StoreBase { private: - typedef std::map Interior; + typedef std::unordered_map Interior; typedef std::map, ESM::Pathgrid> Exterior; Interior mInt; @@ -390,6 +412,8 @@ namespace MWWorld Store(); const ESM::Attribute *search(size_t index) const; + + // calls `search` and throws an exception if not found const ESM::Attribute *find(size_t index) const; void setUp(); @@ -410,9 +434,11 @@ namespace MWWorld Store(); const ESM::WeaponType *search(const int id) const; + + // calls `search` and throws an exception if not found const ESM::WeaponType *find(const int id) const; - RecordId load(ESM::ESMReader &esm) override { return RecordId(0, false); } + RecordId load(ESM::ESMReader &esm) override { return RecordId({}, false); } ESM::WeaponType* insert(const ESM::WeaponType &weaponType); @@ -423,6 +449,43 @@ namespace MWWorld iterator end() const; }; + template <> + class Store : public StoreBase + { + typedef std::unordered_map Static; + Static mStatic; + /// @par mShared usually preserves the record order as it came from the content files (this + /// is relevant for the spell autocalc code and selection order + /// for heads/hairs in the character creation) + /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons. + std::vector mShared; + + mutable bool mKeywordSearchModFlag; + mutable MWDialogue::KeywordSearch mKeywordSearch; + + public: + Store(); + + typedef SharedIterator iterator; + + void setUp() override; + + const ESM::Dialogue *search(const std::string &id) const; + const ESM::Dialogue *find(const std::string &id) const; + + iterator begin() const; + iterator end() const; + + size_t getSize() const override; + + bool eraseStatic(const std::string &id) override; + + RecordId load(ESM::ESMReader &esm) override; + + void listIdentifier(std::vector &list) const override; + + const MWDialogue::KeywordSearch& getDialogIdKeywordSearch() const; + }; } //end namespace diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 6a4a227a49..95b1c69ae1 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -2,12 +2,13 @@ #include -#include -#include -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" @@ -22,8 +23,8 @@ #include -using namespace MWWorld; - +namespace MWWorld +{ namespace { static const int invalidWeatherID = -1; @@ -38,1224 +39,1237 @@ namespace { return x * (1-factor) + y * factor; } -} -template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const -{ - WeatherSetting setting = timeSettings.getSetting(prefix); - float preSunriseTime = setting.mPreSunriseTime; - float postSunriseTime = setting.mPostSunriseTime; - float preSunsetTime = setting.mPreSunsetTime; - float postSunsetTime = setting.mPostSunsetTime; - - // night - if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) - return mNightValue; - // sunrise - else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) + osg::Vec3f calculateStormDirection(const std::string& particleEffect) { - float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; - float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - if (gameHour <= middle) - { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mNightValue, factor); - } - else + osg::Vec3f stormDirection = MWWorld::Weather::defaultDirection(); + if (particleEffect == "meshes\\ashcloud.nif" || particleEffect == "meshes\\blightcloud.nif") { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mDayValue, factor); + osg::Vec3f playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); + playerPos.z() = 0; + osg::Vec3f redMountainPos = osg::Vec3f(25000.f, 70000.f, 0.f); + stormDirection = (playerPos - redMountainPos); + stormDirection.normalize(); } + return stormDirection; } - // day - else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) - return mDayValue; - // sunset - else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) - { - float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; - float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; +} - if (gameHour <= middle) + template + T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const + { + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; + + // night + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) + return mNightValue; + // sunrise + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mDayValue, factor); + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mNightValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mDayValue, factor); + } } - else + // day + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) + return mDayValue; + // sunset + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mNightValue, factor); + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mDayValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mNightValue, factor); + } } + // shut up compiler + return T(); } - // shut up compiler - return T(); -} - + template class MWWorld::TimeOfDayInterpolator; + template class MWWorld::TimeOfDayInterpolator; -template class MWWorld::TimeOfDayInterpolator; -template class MWWorld::TimeOfDayInterpolator; - -Weather::Weather(const std::string& name, - float stormWindSpeed, - float rainSpeed, - float dlFactor, - float dlOffset, - const std::string& particleEffect) - : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) - , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) - , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) - , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) - , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) - , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) - , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) - , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) - , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) - , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) - , mIsStorm(mWindSpeed > stormWindSpeed) - , mRainSpeed(rainSpeed) - , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) - , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) - , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) - , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) - , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) - , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) - , mParticleEffect(particleEffect) - , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") - , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) - , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) - , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) - , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() - , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) - , mFlashBrightness(0.0f) -{ - mDL.FogFactor = dlFactor; - mDL.FogOffset = dlOffset; - mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); - mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); - mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); - mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); - - // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. - - if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect + osg::Vec3f Weather::defaultDirection() { - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); - if (mAmbientLoopSoundID.empty()) // default to "rain" if not set - mAmbientLoopSoundID = "rain"; + static const osg::Vec3f direction = osg::Vec3f(0.f, 1.f, 0.f); + return direction; } - else - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); - - if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) - mAmbientLoopSoundID.clear(); -} -float Weather::transitionDelta() const -{ - // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the - // measurement is in real time, not in-game time. - return mTransitionDelta; -} + Weather::Weather(const std::string& name, + float stormWindSpeed, + float rainSpeed, + float dlFactor, + float dlOffset, + const std::string& particleEffect) + : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) + , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) + , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) + , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) + , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) + , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) + , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) + , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) + , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) + , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) + , mIsStorm(mWindSpeed > stormWindSpeed) + , mRainSpeed(rainSpeed) + , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) + , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) + , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) + , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) + , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) + , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) + , mParticleEffect(particleEffect) + , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") + , mStormDirection(Weather::defaultDirection()) + , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) + , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) + , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) + , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) + , mThunderSoundID() + , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) + , mFlashBrightness(0.0f) + { + mDL.FogFactor = dlFactor; + mDL.FogOffset = dlOffset; + mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); + mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); + mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); + mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); -float Weather::cloudBlendFactor(const float transitionRatio) const -{ - // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. - return transitionRatio / mCloudsMaximumPercent; -} + // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. -float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) -{ - // When paused, the flash brightness remains the same and no new strikes can occur. - if(!isPaused) - { - // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. - if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect { - flashDecrement(elapsedSeconds); - - if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) - { - lightningAndThunder(); - } + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); + if (mAmbientLoopSoundID.empty()) // default to "rain" if not set + mAmbientLoopSoundID = "rain"; } else - { - mFlashBrightness = 0.0f; - } - } - - return mFlashBrightness; -} + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); -inline void Weather::flashDecrement(const float elapsedSeconds) -{ - // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was - // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). - float decrement = mFlashDecrement * elapsedSeconds; - mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; -} - -inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const -{ - // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes - // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of - // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to - // scaled based on how far past it is past the Thunder Threshold. - float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); - return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; -} - -inline void Weather::lightningAndThunder(void) -{ - // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. - // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance - // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. - // TODO: Determine the distribution of each distance to see if it's evenly weighted. - unsigned int distance = Misc::Rng::rollDice(4); - // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. - mFlashBrightness += 1 - (distance * 0.25f); - MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); -} + if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) + mAmbientLoopSoundID.clear(); + } -RegionWeather::RegionWeather(const ESM::Region& region) - : mWeather(invalidWeatherID) - , mChances() -{ - mChances.reserve(10); - mChances.push_back(region.mData.mClear); - mChances.push_back(region.mData.mCloudy); - mChances.push_back(region.mData.mFoggy); - mChances.push_back(region.mData.mOvercast); - mChances.push_back(region.mData.mRain); - mChances.push_back(region.mData.mThunder); - mChances.push_back(region.mData.mAsh); - mChances.push_back(region.mData.mBlight); - mChances.push_back(region.mData.mA); - mChances.push_back(region.mData.mB); -} + float Weather::transitionDelta() const + { + // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the + // measurement is in real time, not in-game time. + return mTransitionDelta; + } -RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) - : mWeather(state.mWeather) - , mChances(state.mChances) -{ -} + float Weather::cloudBlendFactor(const float transitionRatio) const + { + // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. + return transitionRatio / mCloudsMaximumPercent; + } -RegionWeather::operator ESM::RegionWeatherState() const -{ - ESM::RegionWeatherState state = + float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) + { + // When paused, the flash brightness remains the same and no new strikes can occur. + if(!isPaused) { - mWeather, - mChances - }; + // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. + if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + { + flashDecrement(elapsedSeconds); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if(Misc::Rng::rollProbability(prng) <= thunderChance(transitionRatio, elapsedSeconds)) + { + lightningAndThunder(); + } + } + else + { + mFlashBrightness = 0.0f; + } + } - return state; -} + return mFlashBrightness; + } -void RegionWeather::setChances(const std::vector& chances) -{ - if(mChances.size() < chances.size()) + inline void Weather::flashDecrement(const float elapsedSeconds) { - mChances.reserve(chances.size()); + // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was + // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). + float decrement = mFlashDecrement * elapsedSeconds; + mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; } - int i = 0; - for(char chance : chances) + inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const { - mChances[i] = chance; - i++; + // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes + // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of + // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to + // scaled based on how far past it is past the Thunder Threshold. + float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); + return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; } - // Regional weather no longer supports the current type, select a new weather pattern. - if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) + inline void Weather::lightningAndThunder(void) { - chooseNewWeather(); + // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. + // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance + // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. + // TODO: Determine the distribution of each distance to see if it's evenly weighted. + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + unsigned int distance = Misc::Rng::rollDice(4, prng); + // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. + mFlashBrightness += 1 - (distance * 0.25f); + MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); } -} -void RegionWeather::setWeather(int weatherID) -{ - mWeather = weatherID; -} + RegionWeather::RegionWeather(const ESM::Region& region) + : mWeather(invalidWeatherID) + , mChances() + { + mChances.reserve(10); + mChances.push_back(region.mData.mClear); + mChances.push_back(region.mData.mCloudy); + mChances.push_back(region.mData.mFoggy); + mChances.push_back(region.mData.mOvercast); + mChances.push_back(region.mData.mRain); + mChances.push_back(region.mData.mThunder); + mChances.push_back(region.mData.mAsh); + mChances.push_back(region.mData.mBlight); + mChances.push_back(region.mData.mSnow); + mChances.push_back(region.mData.mBlizzard); + } -int RegionWeather::getWeather() -{ - // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. - // Note that the region weather will be expired periodically when the weather update timer expires. - if(mWeather == invalidWeatherID) + RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) + : mWeather(state.mWeather) + , mChances(state.mChances) { - chooseNewWeather(); } - return mWeather; -} + RegionWeather::operator ESM::RegionWeatherState() const + { + ESM::RegionWeatherState state = + { + mWeather, + mChances + }; -void RegionWeather::chooseNewWeather() -{ - // All probabilities must add to 100 (responsibility of the user). - // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 - // and 70% will be greater than 30 (in theory). - int chance = Misc::Rng::rollDice(100) + 1; // 1..100 - int sum = 0; - int i = 0; - for(; static_cast(i) < mChances.size(); ++i) + return state; + } + + void RegionWeather::setChances(const std::vector& chances) { - sum += mChances[i]; - if(chance <= sum) + if(mChances.size() < chances.size()) { - mWeather = i; - return; + mChances.reserve(chances.size()); } - } - - // if we hit this path then the chances don't add to 100, choose a default weather instead - mWeather = 0; -} -MoonModel::MoonModel(const std::string& name) - : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) - , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) - , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) - , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) - , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) - , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) - , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) - , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) - , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) - , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) -{ - // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably - // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. - mSpeed = std::min(mSpeed, 180.0f / 23.0f); -} + int i = 0; + for(char chance : chances) + { + mChances[i] = chance; + i++; + } -MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const -{ - float rotationFromHorizon = angle(gameTime); - MWRender::MoonState state = + // Regional weather no longer supports the current type, select a new weather pattern. + if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) { - rotationFromHorizon, - mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. - phase(gameTime), - shadowBlend(rotationFromHorizon), - earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) - }; + chooseNewWeather(); + } + } - return state; -} + void RegionWeather::setWeather(int weatherID) + { + mWeather = weatherID; + } -inline float MoonModel::angle(const TimeStamp& gameTime) const -{ - // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the - // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. + int RegionWeather::getWeather() + { + // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. + // Note that the region weather will be expired periodically when the weather update timer expires. + if(mWeather == invalidWeatherID) + { + chooseNewWeather(); + } - // When calculating the angle of the moon, several cases have to be taken into account: - // 1. Moon rises and then sets in one day. - // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). - // 3. Moon sets and then rises in one day. - float moonRiseHourToday = moonRiseHour(gameTime.getDay()); - float moonRiseAngleToday = 0; + return mWeather; + } - if(gameTime.getHour() < moonRiseHourToday) + void RegionWeather::chooseNewWeather() { - float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); - if(moonRiseHourYesterday < 24) + // All probabilities must add to 100 (responsibility of the user). + // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 + // and 70% will be greater than 30 (in theory). + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int chance = Misc::Rng::rollDice(100, prng) + 1; // 1..100 + int sum = 0; + int i = 0; + for(; static_cast(i) < mChances.size(); ++i) { - float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); - if(moonRiseAngleYesterday < 180) + sum += mChances[i]; + if(chance <= sum) { - // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. - moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + mWeather = i; + return; } } + + // if we hit this path then the chances don't add to 100, choose a default weather instead + mWeather = 0; } - else + + MoonModel::MoonModel(const std::string& name) + : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) + , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) + , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) + , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) + , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) + , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) + , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) + , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) + , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) + , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { - moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); + // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably + // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. + mSpeed = std::min(mSpeed, 180.0f / 23.0f); } - if(moonRiseAngleToday >= 180) + MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { - // The moon set today, reset the angle to the horizon. - moonRiseAngleToday = 0; + float rotationFromHorizon = angle(gameTime); + MWRender::MoonState state = + { + rotationFromHorizon, + mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. + phase(gameTime), + shadowBlend(rotationFromHorizon), + earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) + }; + + return state; } - return moonRiseAngleToday; -} + inline float MoonModel::angle(const TimeStamp& gameTime) const + { + // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the + // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. -inline float MoonModel::moonRiseHour(unsigned int daysPassed) const -{ - // This arises from the start date of 16 Last Seed, 427 - // TODO: Find an alternate formula that doesn't rely on this day being fixed. - static const unsigned int startDay = 16; - - // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning - // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. - // Note that we don't modulo after adding the latest daily increment because other calculations need to - // know if doing so would cause the moon rise to be postponed until the next day (which happens when - // the moon rise hour is >= 24 in Morrowind). - return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); -} + // When calculating the angle of the moon, several cases have to be taken into account: + // 1. Moon rises and then sets in one day. + // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). + // 3. Moon sets and then rises in one day. + float moonRiseHourToday = moonRiseHour(gameTime.getDay()); + float moonRiseAngleToday = 0; -inline float MoonModel::rotation(float hours) const -{ - // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. - // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure - // of whole rotations that could be completed in a day. - return 15.0f * mSpeed * hours; -} - -MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const -{ - // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. - - // If the moon didn't rise yet today, use yesterday's moon phase. - if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) - return static_cast((gameTime.getDay() / 3) % 8); - else - return static_cast(((gameTime.getDay() + 1) / 3) % 8); -} + if(gameTime.getHour() < moonRiseHourToday) + { + float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); + if(moonRiseHourYesterday < 24) + { + float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); + if(moonRiseAngleYesterday < 180) + { + // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. + moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + } + } + } + else + { + moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); + } -inline float MoonModel::shadowBlend(float angle) const -{ - // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk - // that is roughly the color of the sky, to a textured surface. - // Depending on the current angle, the following values describe the ratio between the textured moon - // and the solid disk: - // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 - // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) - // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 - // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) - float fadeAngle = mFadeStartAngle - mFadeEndAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float fadeStartAngle2 = 180.0f - mFadeStartAngle; - if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) - return (angle - mFadeEndAngle) / fadeAngle; - else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) - return 1.0f; - else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) - return (fadeEndAngle2 - angle) / fadeAngle; - else - return 0.0f; -} + if(moonRiseAngleToday >= 180) + { + // The moon set today, reset the angle to the horizon. + moonRiseAngleToday = 0; + } -inline float MoonModel::hourlyAlpha(float gameHour) const -{ - // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon - // appears and disappears. - // Depending on the current hour, the following values describe how transparent the moon is. - // 1. From Fade Out Start to Fade Out Finish: 1..0 - // 2. From Fade Out Finish to Fade In Start: 0 (transparent) - // 3. From Fade In Start to Fade In Finish: 0..1 - // 4. From Fade In Finish to Fade Out Start: 1 (solid) - if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) - return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); - else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) - return 0.0f; - else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) - return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); - else - return 1.0f; -} + return moonRiseAngleToday; + } -inline float MoonModel::earlyMoonShadowAlpha(float angle) const -{ - // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. - // Depending on the current angle, the following values describe how transparent the moon is. - // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 - // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) - // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 - // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) - float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; - if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) - return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; - else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) - return 1.0f; - else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) - return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; - else - return 0.0f; -} + inline float MoonModel::moonRiseHour(unsigned int daysPassed) const + { + // This arises from the start date of 16 Last Seed, 427 + // TODO: Find an alternate formula that doesn't rely on this day being fixed. + static const unsigned int startDay = 16; + + // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning + // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. + // Note that we don't modulo after adding the latest daily increment because other calculations need to + // know if doing so would cause the moon rise to be postponed until the next day (which happens when + // the moon rise hour is >= 24 in Morrowind). + return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); + } -WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) - : mStore(store) - , mRendering(rendering) - , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) - , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) - , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) - , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) - , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) - , mNightFade(0, 0, 0, 1) - , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) - , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) - , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), - Fallback::Map::getFloat("Water_UnderwaterDayFog"), - Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), - Fallback::Map::getFloat("Water_UnderwaterNightFog")) - , mWeatherSettings() - , mMasser("Masser") - , mSecunda("Secunda") - , mWindSpeed(0.f) - , mCurrentWindSpeed(0.f) - , mNextWindSpeed(0.f) - , mIsStorm(false) - , mPrecipitation(false) - , mStormDirection(0,1,0) - , mCurrentRegion() - , mTimePassed(0) - , mFastForward(false) - , mWeatherUpdateTime(mHoursBetweenWeatherChanges) - , mTransitionFactor(0) - , mNightDayMode(Default) - , mCurrentWeather(0) - , mNextWeather(0) - , mQueuedWeather(0) - , mRegions() - , mResult() - , mAmbientSound(nullptr) - , mPlayingSoundID() -{ - mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime; - mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; - mTimeSettings.mDayEnd = mSunsetTime; - - mTimeSettings.addSetting("Sky"); - mTimeSettings.addSetting("Ambient"); - mTimeSettings.addSetting("Fog"); - mTimeSettings.addSetting("Sun"); - - // Morrowind handles stars settings differently for other ones - mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); - mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); - mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); - - WeatherSetting starSetting = { - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsPostSunsetStart, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart - }; - - mTimeSettings.mSunriseTransitions["Stars"] = starSetting; - - mWeatherSettings.reserve(10); - // These distant land fog factor and offset values are the defaults MGE XE provides. Should be - // provided by settings somewhere? - addWeather("Clear", 1.0f, 0.0f); // 0 - addWeather("Cloudy", 0.9f, 0.0f); // 1 - addWeather("Foggy", 0.2f, 30.0f); // 2 - addWeather("Overcast", 0.7f, 0.0f); // 3 - addWeather("Rain", 0.5f, 10.0f); // 4 - addWeather("Thunderstorm", 0.5f, 20.0f); // 5 - addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 - addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 - addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 - addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - - Store::iterator it = store.get().begin(); - for(; it != store.get().end(); ++it) + inline float MoonModel::rotation(float hours) const { - std::string regionID = Misc::StringUtils::lowerCase(it->mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); + // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. + // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure + // of whole rotations that could be completed in a day. + return 15.0f * mSpeed * hours; } - forceWeather(0); -} + MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const + { + // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. -WeatherManager::~WeatherManager() -{ - stopSounds(); -} + // If the moon didn't rise yet today, use yesterday's moon phase. + if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) + return static_cast((gameTime.getDay() / 3) % 8); + else + return static_cast(((gameTime.getDay() + 1) / 3) % 8); + } -void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) -{ - // In Morrowind, this seems to have the following behavior, when applied to the current region: - // - When there is no transition in progress, start transitioning to the new weather. - // - If there is a transition in progress, queue up the transition and process it when the current one completes. - // - If there is a transition in progress, and a queued transition, overwrite the queued transition. - // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, - // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. - // If the region isn't current, Morrowind will store the new weather for the region in question. - - if(weatherID < mWeatherSettings.size()) + inline float MoonModel::shadowBlend(float angle) const { - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) - { - it->second.setWeather(weatherID); - regionalWeatherChanged(it->first, it->second); - } + // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk + // that is roughly the color of the sky, to a textured surface. + // Depending on the current angle, the following values describe the ratio between the textured moon + // and the solid disk: + // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 + // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) + // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 + // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) + float fadeAngle = mFadeStartAngle - mFadeEndAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float fadeStartAngle2 = 180.0f - mFadeStartAngle; + if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) + return (angle - mFadeEndAngle) / fadeAngle; + else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) + return 1.0f; + else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) + return (fadeEndAngle2 - angle) / fadeAngle; + else + return 0.0f; } -} -void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) -{ - // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. - // In Morrowind, this seems to have the following behavior when applied to the current region: - // - If the region supports the current weather, no change in current weather occurs. - // - If the region no longer supports the current weather, and there is no transition in progress, begin to - // transition to a new supported weather type. - // - If the region no longer supports the current weather, and there is a transition in progress, queue a - // transition to a new supported weather type. - - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) + inline float MoonModel::hourlyAlpha(float gameHour) const { - it->second.setChances(chances); - regionalWeatherChanged(it->first, it->second); + // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon + // appears and disappears. + // Depending on the current hour, the following values describe how transparent the moon is. + // 1. From Fade Out Start to Fade Out Finish: 1..0 + // 2. From Fade Out Finish to Fade In Start: 0 (transparent) + // 3. From Fade In Start to Fade In Finish: 0..1 + // 4. From Fade In Finish to Fade Out Start: 1 (solid) + if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) + return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); + else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) + return 0.0f; + else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) + return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); + else + return 1.0f; } -} -void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) -{ - // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to - // be changed immediately, and any transitions for the previous region discarded. + inline float MoonModel::earlyMoonShadowAlpha(float angle) const { - std::map::iterator it = mRegions.find(playerRegion); - if(it != mRegions.end() && playerRegion != mCurrentRegion) - { - mCurrentRegion = playerRegion; - forceWeather(it->second.getWeather()); - } + // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. + // Depending on the current angle, the following values describe how transparent the moon is. + // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 + // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) + // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 + // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) + float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; + if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) + return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; + else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) + return 1.0f; + else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) + return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; + else + return 0.0f; } -} - -float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) -{ - float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); - if (currentSpeed == 0.f) - currentSpeed = targetSpeed; - float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; - float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; + WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) + : mStore(store) + , mRendering(rendering) + , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) + , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) + , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) + , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) + , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) + , mNightFade(0, 0, 0, 1) + , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) + , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) + , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), + Fallback::Map::getFloat("Water_UnderwaterDayFog"), + Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), + Fallback::Map::getFloat("Water_UnderwaterNightFog")) + , mWeatherSettings() + , mMasser("Masser") + , mSecunda("Secunda") + , mWindSpeed(0.f) + , mCurrentWindSpeed(0.f) + , mNextWindSpeed(0.f) + , mIsStorm(false) + , mPrecipitation(false) + , mStormDirection(Weather::defaultDirection()) + , mCurrentRegion() + , mTimePassed(0) + , mFastForward(false) + , mWeatherUpdateTime(mHoursBetweenWeatherChanges) + , mTransitionFactor(0) + , mNightDayMode(Default) + , mCurrentWeather(0) + , mNextWeather(0) + , mQueuedWeather(0) + , mRegions() + , mResult() + , mAmbientSound(nullptr) + , mPlayingSoundID() + { + mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; + mTimeSettings.mNightEnd = mSunriseTime; + mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; + mTimeSettings.mDayEnd = mSunsetTime; + + mTimeSettings.addSetting("Sky"); + mTimeSettings.addSetting("Ambient"); + mTimeSettings.addSetting("Fog"); + mTimeSettings.addSetting("Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart + }; - if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) - currentSpeed = updatedSpeed; + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; + + mWeatherSettings.reserve(10); + // These distant land fog factor and offset values are the defaults MGE XE provides. Should be + // provided by settings somewhere? + addWeather("Clear", 1.0f, 0.0f); // 0 + addWeather("Cloudy", 0.9f, 0.0f); // 1 + addWeather("Foggy", 0.2f, 30.0f); // 2 + addWeather("Overcast", 0.7f, 0.0f); // 3 + addWeather("Rain", 0.5f, 10.0f); // 4 + addWeather("Thunderstorm", 0.5f, 20.0f); // 5 + addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 + addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 + addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 + addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 + + Store::iterator it = store.get().begin(); + for(; it != store.get().end(); ++it) + { + std::string regionID = Misc::StringUtils::lowerCase(it->mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); + } - return currentSpeed; -} + forceWeather(0); + } -void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) -{ - MWWorld::ConstPtr player = MWMechanics::getPlayer(); + WeatherManager::~WeatherManager() + { + stopSounds(); + } - if(!paused || mFastForward) + void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) { - // Add new transitions when either the player's current external region changes. - std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); - if(updateWeatherTime() || updateWeatherRegion(playerRegion)) + // In Morrowind, this seems to have the following behavior, when applied to the current region: + // - When there is no transition in progress, start transitioning to the new weather. + // - If there is a transition in progress, queue up the transition and process it when the current one completes. + // - If there is a transition in progress, and a queued transition, overwrite the queued transition. + // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, + // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. + // If the region isn't current, Morrowind will store the new weather for the region in question. + + if(weatherID < mWeatherSettings.size()) { - std::map::iterator it = mRegions.find(mCurrentRegion); + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { - addWeatherTransition(it->second.getWeather()); + it->second.setWeather(weatherID); + regionalWeatherChanged(it->first, it->second); } } - - updateWeatherTransitions(duration); } - bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; - if (isExterior && !isDay) - mNightDayMode = ExteriorNight; - else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) - mNightDayMode = InteriorDay; - else - mNightDayMode = Default; - - if(!isExterior) + void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { - mRendering.setSkyEnabled(false); - stopSounds(); - mWindSpeed = 0.f; - mCurrentWindSpeed = 0.f; - mNextWindSpeed = 0.f; - return; - } + // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. + // In Morrowind, this seems to have the following behavior when applied to the current region: + // - If the region supports the current weather, no change in current weather occurs. + // - If the region no longer supports the current weather, and there is no transition in progress, begin to + // transition to a new supported weather type. + // - If the region no longer supports the current weather, and there is a transition in progress, queue a + // transition to a new supported weather type. - calculateWeatherResult(time.getHour(), duration, paused); + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); + if(it != mRegions.end()) + { + it->second.setChances(chances); + regionalWeatherChanged(it->first, it->second); + } + } - if (!paused) + void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { - mWindSpeed = mResult.mWindSpeed; - mCurrentWindSpeed = mResult.mCurrentWindSpeed; - mNextWindSpeed = mResult.mNextWindSpeed; + // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to + // be changed immediately, and any transitions for the previous region discarded. + { + std::map::iterator it = mRegions.find(playerRegion); + if(it != mRegions.end() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; + forceWeather(it->second.getWeather()); + } + } } - mIsStorm = mResult.mIsStorm; + float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) + { + float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); + if (currentSpeed == 0.f) + currentSpeed = targetSpeed; - // For some reason Ash Storm is not considered as a precipitation weather in game - mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) - && mResult.mParticleEffect != "meshes\\ashcloud.nif"; + float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + float updatedSpeed = (Misc::Rng::rollClosedProbability(prng) - 0.5f) * multiplier * targetSpeed + currentSpeed; + + if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) + currentSpeed = updatedSpeed; + + return currentSpeed; + } - if (mIsStorm) + void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { - osg::Vec3f stormDirection(0, 1, 0); - if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + + if(!paused || mFastForward) { - osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); - playerPos.z() = 0; - osg::Vec3f redMountainPos (25000, 70000, 0); - stormDirection = (playerPos - redMountainPos); - stormDirection.normalize(); + // Add new transitions when either the player's current external region changes. + std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); + if(updateWeatherTime() || updateWeatherRegion(playerRegion)) + { + std::map::iterator it = mRegions.find(mCurrentRegion); + if(it != mRegions.end()) + { + addWeatherTransition(it->second.getWeather()); + } + } + + updateWeatherTransitions(duration); } - mStormDirection = stormDirection; - mRendering.getSkyManager()->setStormDirection(mStormDirection); - } - // disable sun during night - if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) - mRendering.getSkyManager()->sunDisable(); - else - mRendering.getSkyManager()->sunEnable(); + bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; + if (isExterior && !isDay) + mNightDayMode = ExteriorNight; + else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) + mNightDayMode = InteriorDay; + else + mNightDayMode = Default; - // Update the sun direction. Run it east to west at a fixed angle from overhead. - // The sun's speed at day and night may differ, since mSunriseTime and mNightStart - // mark when the sun is level with the horizon. - { - // Shift times into a 24-hour window beginning at mSunriseTime... - float adjustedHour = time.getHour(); - float adjustedNightStart = mTimeSettings.mNightStart; - if ( time.getHour() < mSunriseTime ) - adjustedHour += 24.f; - if ( mTimeSettings.mNightStart < mSunriseTime ) - adjustedNightStart += 24.f; - - const bool is_night = adjustedHour >= adjustedNightStart; - const float dayDuration = adjustedNightStart - mSunriseTime; - const float nightDuration = 24.f - dayDuration; - - double theta; - if ( !is_night ) + if(!isExterior) { - theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; + mRendering.setSkyEnabled(false); + stopSounds(); + mWindSpeed = 0.f; + mCurrentWindSpeed = 0.f; + mNextWindSpeed = 0.f; + return; } - else + + calculateWeatherResult(time.getHour(), duration, paused); + + if (!paused) { - theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + mWindSpeed = mResult.mWindSpeed; + mCurrentWindSpeed = mResult.mCurrentWindSpeed; + mNextWindSpeed = mResult.mNextWindSpeed; } - osg::Vec3f final( - static_cast(cos(theta)), - -0.268f, // approx tan( -15 degrees ) - static_cast(sin(theta))); - mRendering.setSunDirection( final * -1 ); - } + mIsStorm = mResult.mIsStorm; - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); + // For some reason Ash Storm is not considered as a precipitation weather in game + mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) + && mResult.mParticleEffect != "meshes\\ashcloud.nif"; - float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; - float glareFade = 1.f; - if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) - glareFade = 0.f; - else if (time.getHour() < peakHour) - glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); - else - glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); + mStormDirection = calculateStormDirection(mResult.mParticleEffect); + mRendering.getSkyManager()->setStormParticleDirection(mStormDirection); - mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); + // disable sun during night + if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) + mRendering.getSkyManager()->sunDisable(); + else + mRendering.getSkyManager()->sunEnable(); - mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); - mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); + // Update the sun direction. Run it east to west at a fixed angle from overhead. + // The sun's speed at day and night may differ, since mSunriseTime and mNightStart + // mark when the sun is level with the horizon. + { + // Shift times into a 24-hour window beginning at mSunriseTime... + float adjustedHour = time.getHour(); + float adjustedNightStart = mTimeSettings.mNightStart; + if ( time.getHour() < mSunriseTime ) + adjustedHour += 24.f; + if ( mTimeSettings.mNightStart < mSunriseTime ) + adjustedNightStart += 24.f; + + const bool is_night = adjustedHour >= adjustedNightStart; + const float dayDuration = adjustedNightStart - mSunriseTime; + const float nightDuration = 24.f - dayDuration; + + double theta; + if ( !is_night ) + { + theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; + } + else + { + theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + } - mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, - mResult.mDLFogOffset/100.0f, mResult.mFogColor); - mRendering.setAmbientColour(mResult.mAmbientColor); - mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); + osg::Vec3f final( + static_cast(cos(theta)), + -0.268f, // approx tan( -15 degrees ) + static_cast(sin(theta))); + mRendering.setSunDirection( final * -1 ); + mRendering.setNight(is_night); + } - mRendering.getSkyManager()->setWeather(mResult); + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); - // Play sounds - if (mPlayingSoundID != mResult.mAmbientLoopSoundID) - { - stopSounds(); - if (!mResult.mAmbientLoopSoundID.empty()) - mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( - mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, - MWSound::Type::Sfx, MWSound::PlayMode::Loop - ); - mPlayingSoundID = mResult.mAmbientLoopSoundID; - } - else if (mAmbientSound) - mAmbientSound->setVolume(mResult.mAmbientSoundVolume); -} + float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; + float glareFade = 1.f; + if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) + glareFade = 0.f; + else if (time.getHour() < peakHour) + glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); + else + glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); -void WeatherManager::stopSounds() -{ - if (mAmbientSound) - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); - mAmbientSound = nullptr; - mPlayingSoundID.clear(); -} + mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); -float WeatherManager::getWindSpeed() const -{ - return mWindSpeed; -} + mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); + mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); -bool WeatherManager::isInStorm() const -{ - return mIsStorm; -} + mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, + mResult.mDLFogOffset/100.0f, mResult.mFogColor); + mRendering.setAmbientColour(mResult.mAmbientColor); + mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade, mResult.mGlareView * glareFade); -osg::Vec3f WeatherManager::getStormDirection() const -{ - return mStormDirection; -} + mRendering.getSkyManager()->setWeather(mResult); -void WeatherManager::advanceTime(double hours, bool incremental) -{ - // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are - // immediately applied, regardless of whatever transition time might have been remaining. - mTimePassed += hours; - mFastForward = !incremental ? true : mFastForward; -} + // Play sounds + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) + { + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( + mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, + MWSound::Type::Sfx, MWSound::PlayMode::Loop + ); + mPlayingSoundID = mResult.mAmbientLoopSoundID; + } + else if (mAmbientSound) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); + } -unsigned int WeatherManager::getWeatherID() const -{ - return mCurrentWeather; -} + void WeatherManager::stopSounds() + { + if (mAmbientSound) + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound = nullptr; + mPlayingSoundID.clear(); + } -NightDayMode WeatherManager::getNightDayMode() const -{ - return mNightDayMode; -} + float WeatherManager::getWindSpeed() const + { + return mWindSpeed; + } -bool WeatherManager::useTorches(float hour) const -{ - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + bool WeatherManager::isInStorm() const + { + return mIsStorm; + } - return isDark && !mPrecipitation; -} + osg::Vec3f WeatherManager::getStormDirection() const + { + return mStormDirection; + } -void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) -{ - ESM::WeatherState state; - state.mCurrentRegion = mCurrentRegion; - state.mTimePassed = mTimePassed; - state.mFastForward = mFastForward; - state.mWeatherUpdateTime = mWeatherUpdateTime; - state.mTransitionFactor = mTransitionFactor; - state.mCurrentWeather = mCurrentWeather; - state.mNextWeather = mNextWeather; - state.mQueuedWeather = mQueuedWeather; - - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) + void WeatherManager::advanceTime(double hours, bool incremental) { - state.mRegions.insert(std::make_pair(it->first, it->second)); + // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are + // immediately applied, regardless of whatever transition time might have been remaining. + mTimePassed += hours; + mFastForward = !incremental ? true : mFastForward; } - writer.startRecord(ESM::REC_WTHR); - state.save(writer); - writer.endRecord(ESM::REC_WTHR); -} + NightDayMode WeatherManager::getNightDayMode() const + { + return mNightDayMode; + } -bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) -{ - if(ESM::REC_WTHR == type) + bool WeatherManager::useTorches(float hour) const + { + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + + return isDark && !mPrecipitation; + } + + void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { - static const int oldestCompatibleSaveFormat = 2; - if(reader.getFormat() < oldestCompatibleSaveFormat) + ESM::WeatherState state; + state.mCurrentRegion = mCurrentRegion; + state.mTimePassed = mTimePassed; + state.mFastForward = mFastForward; + state.mWeatherUpdateTime = mWeatherUpdateTime; + state.mTransitionFactor = mTransitionFactor; + state.mCurrentWeather = mCurrentWeather; + state.mNextWeather = mNextWeather; + state.mQueuedWeather = mQueuedWeather; + + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) { - // Weather state isn't really all that important, so to preserve older save games, we'll just discard the - // older weather records, rather than fail to handle the record. - reader.skipRecord(); + state.mRegions.insert(std::make_pair(it->first, it->second)); } - else + + writer.startRecord(ESM::REC_WTHR); + state.save(writer); + writer.endRecord(ESM::REC_WTHR); + } + + bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if(ESM::REC_WTHR == type) { - ESM::WeatherState state; - state.load(reader); - - mCurrentRegion.swap(state.mCurrentRegion); - mTimePassed = state.mTimePassed; - mFastForward = state.mFastForward; - mWeatherUpdateTime = state.mWeatherUpdateTime; - mTransitionFactor = state.mTransitionFactor; - mCurrentWeather = state.mCurrentWeather; - mNextWeather = state.mNextWeather; - mQueuedWeather = state.mQueuedWeather; - - mRegions.clear(); - importRegions(); - - for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + static const int oldestCompatibleSaveFormat = 2; + if(reader.getFormat() < oldestCompatibleSaveFormat) + { + // Weather state isn't really all that important, so to preserve older save games, we'll just discard the + // older weather records, rather than fail to handle the record. + reader.skipRecord(); + } + else { - std::map::iterator found = mRegions.find(it->first); - if (found != mRegions.end()) + ESM::WeatherState state; + state.load(reader); + + mCurrentRegion.swap(state.mCurrentRegion); + mTimePassed = state.mTimePassed; + mFastForward = state.mFastForward; + mWeatherUpdateTime = state.mWeatherUpdateTime; + mTransitionFactor = state.mTransitionFactor; + mCurrentWeather = state.mCurrentWeather; + mNextWeather = state.mNextWeather; + mQueuedWeather = state.mQueuedWeather; + + mRegions.clear(); + importRegions(); + + for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) { - found->second = RegionWeather(it->second); + std::map::iterator found = mRegions.find(it->first); + if (found != mRegions.end()) + { + found->second = RegionWeather(it->second); + } } } + + return true; } - return true; + return false; } - return false; -} + void WeatherManager::clear() + { + stopSounds(); -void WeatherManager::clear() -{ - stopSounds(); - - mCurrentRegion = ""; - mTimePassed = 0.0f; - mWeatherUpdateTime = 0.0f; - forceWeather(0); - mRegions.clear(); - importRegions(); -} + mCurrentRegion.clear(); + mTimePassed = 0.0f; + mWeatherUpdateTime = 0.0f; + forceWeather(0); + mRegions.clear(); + importRegions(); + } -inline void WeatherManager::addWeather(const std::string& name, - float dlFactor, float dlOffset, - const std::string& particleEffect) -{ - static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); + inline void WeatherManager::addWeather(const std::string& name, + float dlFactor, float dlOffset, + const std::string& particleEffect) + { + static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); - Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); + Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); - mWeatherSettings.push_back(weather); -} + mWeatherSettings.push_back(weather); + } -inline void WeatherManager::importRegions() -{ - for(const ESM::Region& region : mStore.get()) + inline void WeatherManager::importRegions() { - std::string regionID = Misc::StringUtils::lowerCase(region.mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + for(const ESM::Region& region : mStore.get()) + { + std::string regionID = Misc::StringUtils::lowerCase(region.mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + } } -} -inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) -{ - // If the region is current, then add a weather transition for it. - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - if(player.isInCell()) + inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) { - if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + // If the region is current, then add a weather transition for it. + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + if(player.isInCell()) { - addWeatherTransition(region.getWeather()); + if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + { + addWeatherTransition(region.getWeather()); + } } } -} -inline bool WeatherManager::updateWeatherTime() -{ - mWeatherUpdateTime -= mTimePassed; - mTimePassed = 0.0f; - if(mWeatherUpdateTime <= 0.0f) + inline bool WeatherManager::updateWeatherTime() { - // Expire all regional weather, so that any call to getWeather() will return a new weather ID. - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) + mWeatherUpdateTime -= mTimePassed; + mTimePassed = 0.0f; + if(mWeatherUpdateTime <= 0.0f) { - it->second.setWeather(invalidWeatherID); - } + // Expire all regional weather, so that any call to getWeather() will return a new weather ID. + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + it->second.setWeather(invalidWeatherID); + } - mWeatherUpdateTime += mHoursBetweenWeatherChanges; + mWeatherUpdateTime += mHoursBetweenWeatherChanges; - return true; - } + return true; + } - return false; -} + return false; + } -inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) -{ - if(!playerRegion.empty() && playerRegion != mCurrentRegion) + inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) { - mCurrentRegion = playerRegion; + if(!playerRegion.empty() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; - return true; - } + return true; + } - return false; -} + return false; + } -inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) -{ - // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last - // weather type set, regardless of the remaining transition time. - if(!mFastForward && inTransition()) + inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) { - const float delta = mWeatherSettings[mNextWeather].transitionDelta(); - mTransitionFactor -= elapsedRealSeconds * delta; - if(mTransitionFactor <= 0.0f) + // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last + // weather type set, regardless of the remaining transition time. + if(!mFastForward && inTransition()) { - mCurrentWeather = mNextWeather; - mNextWeather = mQueuedWeather; - mQueuedWeather = invalidWeatherID; + const float delta = mWeatherSettings[mNextWeather].transitionDelta(); + mTransitionFactor -= elapsedRealSeconds * delta; + if(mTransitionFactor <= 0.0f) + { + mCurrentWeather = mNextWeather; + mNextWeather = mQueuedWeather; + mQueuedWeather = invalidWeatherID; - // We may have begun processing the queued transition, so we need to apply the remaining time towards it. - if(inTransition()) + // We may have begun processing the queued transition, so we need to apply the remaining time towards it. + if(inTransition()) + { + const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); + const float remainingSeconds = -(mTransitionFactor / delta); + mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + } + else + { + mTransitionFactor = 0.0f; + } + } + } + else + { + if(mQueuedWeather != invalidWeatherID) { - const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); - const float remainingSeconds = -(mTransitionFactor / delta); - mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + mCurrentWeather = mQueuedWeather; } - else + else if(mNextWeather != invalidWeatherID) { - mTransitionFactor = 0.0f; + mCurrentWeather = mNextWeather; } + + mNextWeather = invalidWeatherID; + mQueuedWeather = invalidWeatherID; + mFastForward = false; } } - else - { - if(mQueuedWeather != invalidWeatherID) - { - mCurrentWeather = mQueuedWeather; - } - else if(mNextWeather != invalidWeatherID) - { - mCurrentWeather = mNextWeather; - } + inline void WeatherManager::forceWeather(const int weatherID) + { + mTransitionFactor = 0.0f; + mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; - mFastForward = false; } -} - -inline void WeatherManager::forceWeather(const int weatherID) -{ - mTransitionFactor = 0.0f; - mCurrentWeather = weatherID; - mNextWeather = invalidWeatherID; - mQueuedWeather = invalidWeatherID; -} -inline bool WeatherManager::inTransition() -{ - return mNextWeather != invalidWeatherID; -} - -inline void WeatherManager::addWeatherTransition(const int weatherID) -{ - // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if - // no transition is in progress, otherwise it queues it to be transitioned. - - assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); - - if(!inTransition() && (weatherID != mCurrentWeather)) + inline bool WeatherManager::inTransition() { - mNextWeather = weatherID; - mTransitionFactor = 1.0f; + return mNextWeather != invalidWeatherID; } - else if(inTransition() && (weatherID != mNextWeather)) - { - mQueuedWeather = weatherID; - } -} -inline void WeatherManager::calculateWeatherResult(const float gameHour, - const float elapsedSeconds, - const bool isPaused) -{ - float flash = 0.0f; - if(!inTransition()) - { - calculateResult(mCurrentWeather, gameHour); - flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); - } - else + inline void WeatherManager::addWeatherTransition(const int weatherID) { - calculateTransitionResult(1 - mTransitionFactor, gameHour); - float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, - elapsedSeconds, - isPaused); - float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, - elapsedSeconds, - isPaused); - flash = currentFlash + nextFlash; - } - osg::Vec4f flashColor(flash, flash, flash, 0.0f); + // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if + // no transition is in progress, otherwise it queues it to be transitioned. - mResult.mFogColor += flashColor; - mResult.mAmbientColor += flashColor; - mResult.mSunColor += flashColor; -} + assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); -inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) -{ - const Weather& current = mWeatherSettings[weatherID]; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mCloudBlendFactor = 0; - mResult.mNextWindSpeed = 0; - mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); - - mResult.mCloudSpeed = current.mCloudSpeed; - mResult.mGlareView = current.mGlareView; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; - mResult.mAmbientSoundVolume = 1.f; - mResult.mEffectFade = 1.f; - - mResult.mIsStorm = current.mIsStorm; - - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mRainDiameter = current.mRainDiameter; - mResult.mRainMinHeight = current.mRainMinHeight; - mResult.mRainMaxHeight = current.mRainMaxHeight; - mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); - mResult.mDLFogFactor = current.mDL.FogFactor; - mResult.mDLFogOffset = current.mDL.FogOffset; - - WeatherSetting setting = mTimeSettings.getSetting("Sun"); - float preSunsetTime = setting.mPreSunsetTime; - - if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) - { - float factor = 1.f; - if (preSunsetTime > 0) - factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; - factor = std::min(1.f, factor); - mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); - // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because - // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline - // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. - // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. - mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); - for (int i=0; i<3; ++i) - mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + if(!inTransition() && (weatherID != mCurrentWeather)) + { + mNextWeather = weatherID; + mTransitionFactor = 1.0f; + } + else if(inTransition() && (weatherID != mNextWeather)) + { + mQueuedWeather = weatherID; + } } - else - mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - if (gameHour >= mTimeSettings.mDayEnd) + inline void WeatherManager::calculateWeatherResult(const float gameHour, + const float elapsedSeconds, + const bool isPaused) { - // sunset - float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); - fade = fade*fade; - mResult.mSunDiscColor.a() = 1.f - fade; - } - else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) - { - // sunrise - mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; - } - else - mResult.mSunDiscColor.a() = 1; - -} - -inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) -{ - calculateResult(mCurrentWeather, gameHour); - const MWRender::WeatherResult current = mResult; - calculateResult(mNextWeather, gameHour); - const MWRender::WeatherResult other = mResult; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mNextCloudTexture = other.mCloudTexture; - mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); - - mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); - mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); - mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); - - mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); - mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); - mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); - mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); - mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + float flash = 0.0f; + if(!inTransition()) + { + calculateResult(mCurrentWeather, gameHour); + flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + } + else + { + calculateTransitionResult(1 - mTransitionFactor, gameHour); + float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, + elapsedSeconds, + isPaused); + float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, + elapsedSeconds, + isPaused); + flash = currentFlash + nextFlash; + } + osg::Vec4f flashColor(flash, flash, flash, 0.0f); - mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); - mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); + mResult.mFogColor += flashColor; + mResult.mAmbientColor += flashColor; + mResult.mSunColor += flashColor; + } - mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); - mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); - mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); - mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) + { + const Weather& current = mWeatherSettings[weatherID]; - mResult.mNight = current.mNight; + mResult.mCloudTexture = current.mCloudTexture; + mResult.mCloudBlendFactor = 0; + mResult.mNextWindSpeed = 0; + mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); + mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; - float threshold = mWeatherSettings[mNextWeather].mRainThreshold; - if (threshold <= 0) - threshold = 0.5f; + mResult.mCloudSpeed = current.mCloudSpeed; + mResult.mGlareView = current.mGlareView; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mPrecipitationAlpha = 1.f; - if(factor < threshold) - { mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = 1 - factor / threshold; - mResult.mEffectFade = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); + + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); + mResult.mDLFogFactor = current.mDL.FogFactor; + mResult.mDLFogOffset = current.mDL.FogOffset; + + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) + { + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; + factor = std::min(1.f, factor); + mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); + // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because + // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline + // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. + // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. + mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); + for (int i=0; i<3; ++i) + mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + } + else + mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); + + if (gameHour >= mTimeSettings.mDayEnd) + { + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); + fade = fade*fade; + mResult.mSunDiscColor.a() = 1.f - fade; + } + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) + { + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; + } + else + mResult.mSunDiscColor.a() = 1; + + mResult.mStormDirection = calculateStormDirection(mResult.mParticleEffect); } - else + + inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) { - mResult.mIsStorm = other.mIsStorm; - mResult.mParticleEffect = other.mParticleEffect; - mResult.mRainEffect = other.mRainEffect; - mResult.mRainSpeed = other.mRainSpeed; - mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); - mResult.mEffectFade = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; - - mResult.mRainDiameter = other.mRainDiameter; - mResult.mRainMinHeight = other.mRainMinHeight; - mResult.mRainMaxHeight = other.mRainMaxHeight; - mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + calculateResult(mCurrentWeather, gameHour); + const MWRender::WeatherResult current = mResult; + calculateResult(mNextWeather, gameHour); + const MWRender::WeatherResult other = mResult; + + mResult.mStormDirection = current.mStormDirection; + mResult.mNextStormDirection = other.mStormDirection; + + mResult.mCloudTexture = current.mCloudTexture; + mResult.mNextCloudTexture = other.mCloudTexture; + mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); + + mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); + mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); + mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); + + mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); + mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); + mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); + mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); + mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + + mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); + mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); + mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); + + mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); + mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); + mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); + mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + + mResult.mNight = current.mNight; + + float threshold = mWeatherSettings[mNextWeather].mRainThreshold; + if (threshold <= 0.f) + threshold = 0.5f; + + if(factor < threshold) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = 1.f - factor / threshold; + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainDiameter = current.mRainDiameter; + mResult.mRainMinHeight = current.mRainMinHeight; + mResult.mRainMaxHeight = current.mRainMaxHeight; + mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + + mResult.mRainDiameter = other.mRainDiameter; + mResult.mRainMinHeight = other.mRainMinHeight; + mResult.mRainMaxHeight = other.mRainMaxHeight; + mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + } } } diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index a3928465c4..21b690f7b6 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWWORLD_WEATHER_H #define GAME_MWWORLD_WEATHER_H -#include +#include #include #include @@ -115,6 +115,8 @@ namespace MWWorld class Weather { public: + static osg::Vec3f defaultDirection(); + Weather(const std::string& name, float stormWindSpeed, float rainSpeed, @@ -189,6 +191,8 @@ namespace MWWorld std::string mRainEffect; + osg::Vec3f mStormDirection; + // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature // is broken in the vanilla game and was disabled. @@ -303,7 +307,11 @@ namespace MWWorld void advanceTime(double hours, bool incremental); - unsigned int getWeatherID() const; + int getWeatherID() const { return mCurrentWeather; } + + int getNextWeatherID() const { return mNextWeather; } + + float getTransitionFactor() const { return mTransitionFactor; } bool useTorches(float hour) const; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index be32765ad4..0cb00341da 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2,18 +2,21 @@ #include #include +#include -#include -#include +#include + +#include #include -#include -#include -#include -#include +#include +#include +#include +#include #include +#include #include #include #include @@ -24,31 +27,36 @@ #include #include +#include + +#include +#include -#include -#include -#include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/aiavoiddoor.hpp" //Used to tell actors to avoid doors #include "../mwmechanics/summoning.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/npcanimation.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/camera.hpp" #include "../mwrender/vismask.hpp" +#include "../mwrender/postprocessor.hpp" #include "../mwscript/globalscripts.hpp" @@ -72,54 +80,48 @@ #include "contentloader.hpp" #include "esmloader.hpp" - -namespace -{ - -// Wraps a value to (-PI, PI] -void wrap(float& rad) -{ - const float pi = static_cast(osg::PI); - if (rad>0) - rad = std::fmod(rad+pi, 2.0f*pi)-pi; - else - rad = std::fmod(rad-pi, 2.0f*pi)+pi; -} - -} +#include "cellutils.hpp" namespace MWWorld { struct GameContentLoader : public ContentLoader { - GameContentLoader(Loading::Listener& listener) - : ContentLoader(listener) - { - } - - bool addLoader(const std::string& extension, ContentLoader* loader) + void addLoader(std::string&& extension, ContentLoader& loader) { - return mLoaders.insert(std::make_pair(extension, loader)).second; + mLoaders.emplace(std::move(extension), &loader); } - void load(const boost::filesystem::path& filepath, int& index) override + void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override { - LoadersContainer::iterator it(mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string()))); + const auto it = mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string())); if (it != mLoaders.end()) { - it->second->load(filepath, index); + const std::string filename = filepath.filename().string(); + Log(Debug::Info) << "Loading content file " << filename; + if (listener != nullptr) + listener->setLabel(MyGUI::TextIterator::toTagsString(filename)); + it->second->load(filepath, index, listener); } else { - std::string msg("Cannot load file: "); - msg += filepath.string(); - throw std::runtime_error(msg.c_str()); + std::string msg("Cannot load file: "); + msg += filepath.string(); + throw std::runtime_error(msg.c_str()); } } private: - typedef std::map LoadersContainer; - LoadersContainer mLoaders; + std::map mLoaders; + }; + + struct OMWScriptsLoader : public ContentLoader + { + ESMStore& mStore; + OMWScriptsLoader(ESMStore& store) : mStore(store) {} + void load(const boost::filesystem::path& filepath, int& /*index*/, Loading::Listener* /*listener*/) override + { + mStore.addOMWScripts(filepath.string()); + } }; void World::adjustSky() @@ -139,70 +141,59 @@ namespace MWWorld Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, const std::string& resourcePath, const std::string& userDataPath) - : mResourceSystem(resourceSystem), mLocalScripts (mStore), - mCells (mStore, mEsm), mSky (true), + : mResourceSystem(resourceSystem), mLocalScripts(mStore), + mCells(mStore, mReaders), mSky(true), mGodMode(false), mScriptsEnabled(true), mDiscardMovements(true), mContentFiles (contentFiles), - mUserDataPath(userDataPath), mShouldUpdateNavigator(false), + mUserDataPath(userDataPath), + mDefaultHalfExtents(Settings::Manager::getVector3("default actor pathfind half extents", "Game")), + mShouldUpdateNavigator(false), mActivationDistanceOverride (activationDistanceOverride), mStartCell(startCell), mDistanceToFacedObject(-1.f), mTeleportEnabled(true), mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f) { - mEsm.resize(contentFiles.size()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener->loadingOn(); - GameContentLoader gameContentLoader(*listener); - EsmLoader esmLoader(mStore, mEsm, encoder, *listener); - - gameContentLoader.addLoader(".esm", &esmLoader); - gameContentLoader.addLoader(".esp", &esmLoader); - gameContentLoader.addLoader(".omwgame", &esmLoader); - gameContentLoader.addLoader(".omwaddon", &esmLoader); - gameContentLoader.addLoader(".project", &esmLoader); - - loadContentFiles(fileCollections, contentFiles, gameContentLoader); + loadContentFiles(fileCollections, contentFiles, encoder, listener); + loadGroundcoverFiles(fileCollections, groundcoverFiles, encoder, listener); listener->loadingOff(); - // insert records that may not be present in all versions of MW - if (mEsm[0].getFormat() == 0) - ensureNeededRecords(); - - mCurrentDate.reset(new DateTimeManager()); + mCurrentDate = std::make_unique(); fillGlobalVariables(); - mStore.setUp(true); + mStore.setUp(); + mStore.validateRecords(mReaders); mStore.movePlayerRecord(); mSwimHeightScale = mStore.get().find("fSwimHeightScale")->mValue.getFloat(); - mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode)); + mPhysics = std::make_unique(resourceSystem, rootNode); - if (auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager()) + if (Settings::Manager::getBool("enable", "Navigator")) { - navigatorSettings->mMaxClimb = MWPhysics::sStepSizeUp; - navigatorSettings->mMaxSlope = MWPhysics::sMaxSlope; - navigatorSettings->mSwimHeightScale = mSwimHeightScale; - DetourNavigator::RecastGlobalAllocator::init(); - mNavigator.reset(new DetourNavigator::NavigatorImpl(*navigatorSettings)); + auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + navigatorSettings.mRecast.mSwimHeightScale = mSwimHeightScale; + mNavigator = DetourNavigator::makeNavigator(navigatorSettings, userDataPath); } else { - mNavigator.reset(new DetourNavigator::NavigatorStub()); + mNavigator = DetourNavigator::makeNavigatorStub(); } - mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator)); - mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get())); + mRendering = std::make_unique(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator, mGroundcoverStore); + mProjectileManager = std::make_unique(mRendering->getLightRoot()->asGroup(), resourceSystem, mRendering.get(), mPhysics.get()); mRendering->preloadCommonAssets(); - mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering, mStore)); + mWeatherManager = std::make_unique(*mRendering, mStore); - mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get(), *mNavigator)); + mWorldScene = std::make_unique(*this, *mRendering.get(), mPhysics.get(), *mNavigator); } void World::fillGlobalVariables() @@ -230,7 +221,7 @@ namespace MWWorld // we don't want old weather to persist on a new game // Note that if reset later, the initial ChangeWeather that the chargen script calls will be lost. mWeatherManager.reset(); - mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering.get(), mStore)); + mWeatherManager = std::make_unique(*mRendering.get(), mStore); if (!bypass) { @@ -285,6 +276,9 @@ namespace MWWorld MWBase::Environment::get().getWindowManager()->updatePlayer(); mCurrentDate->setup(mGlobalVariables); + + // Initial seed. + mPrng.seed(mRandomSeed); } void World::clear() @@ -301,7 +295,7 @@ namespace MWWorld if (mPlayer) { mPlayer->clear(); - mPlayer->setCell(0); + mPlayer->setCell(nullptr); mPlayer->getPlayer().getRefData() = RefData(); mPlayer->set(mStore.get().find ("player")); } @@ -330,7 +324,8 @@ namespace MWWorld +1 // weather record +1 // actorId counter +1 // levitation/teleport enabled state - +1; // camera + +1 // camera + +1; // random state. } int World::countSavedGameCells() const @@ -340,6 +335,10 @@ namespace MWWorld void World::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { + writer.startRecord(ESM::REC_RAND); + writer.writeHNOString("RAND", Misc::Rng::serialize(mPrng)); + writer.endRecord(ESM::REC_RAND); + // Active cells could have a dirty fog of war, sync it to the CellStore first for (CellStore* cellstore : mWorldScene->getActiveCells()) { @@ -378,6 +377,12 @@ namespace MWWorld reader.getHNT(mTeleportEnabled, "TELE"); reader.getHNT(mLevitationEnabled, "LEVT"); return; + case ESM::REC_RAND: + { + auto data = reader.getHNOString("RAND"); + Misc::Rng::deserialize(data, mPrng); + } + break; case ESM::REC_PLAY: mStore.checkPlayer(); mPlayer->readRecord(reader, type); @@ -455,6 +460,7 @@ namespace MWWorld ESM::GameSetting record; record.mId = params.first; record.mValue = params.second; + record.mRecordFlags = 0; mStore.insertStatic(record); } } @@ -485,6 +491,7 @@ namespace MWWorld ESM::Global record; record.mId = params.first; record.mValue = params.second; + record.mRecordFlags = 0; mStore.insertStatic(record); } } @@ -504,6 +511,7 @@ namespace MWWorld ESM::Static record; record.mId = params.first; record.mModel = params.second; + record.mRecordFlags = 0; mStore.insertStatic(record); } } @@ -518,6 +526,7 @@ namespace MWWorld ESM::Door record; record.mId = params.first; record.mModel = params.second; + record.mRecordFlags = 0; mStore.insertStatic(record); } } @@ -529,12 +538,25 @@ namespace MWWorld mProjectileManager->clear(); } - const ESM::Cell *World::getExterior (const std::string& cellName) const + void World::setRandomSeed(uint32_t seed) + { + mRandomSeed = seed; + } + + const ESM::Cell* World::getExterior(const std::string& cellName) const { // first try named cells const ESM::Cell *cell = mStore.get().searchExtByName (cellName); if (cell) return cell; + // treat "Wilderness" like an empty string + static const std::string defaultName = mStore.get().find("sDefaultCellname")->mValue.getString(); + if (Misc::StringUtils::ciEqual(cellName, defaultName)) + { + cell = mStore.get().searchExtByName(""); + if (cell) + return cell; + } // didn't work -> now check for regions for (const ESM::Region ®ion : mStore.get()) @@ -566,6 +588,11 @@ namespace MWWorld return getInterior (id.mWorldspace); } + bool World::isCellActive(CellStore* cell) const + { + return mWorldScene->getActiveCells().count(cell) > 0; + } + void World::testExteriorCells() { mWorldScene->testExteriorCells(); @@ -578,13 +605,7 @@ namespace MWWorld void World::useDeathCamera() { - if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() ) - { - mRendering->getCamera()->togglePreviewMode(false); - mRendering->getCamera()->toggleVanityMode(false); - } - if(mRendering->getCamera()->isFirstPerson()) - mRendering->getCamera()->toggleViewMode(true); + mRendering->getCamera()->setMode(MWRender::Camera::Mode::ThirdPerson); } MWWorld::Player& World::getPlayer() @@ -597,11 +618,6 @@ namespace MWWorld return mStore; } - std::vector& World::getEsmReader() - { - return mEsm; - } - LocalScripts& World::getLocalScripts() { return mLocalScripts; @@ -612,7 +628,7 @@ namespace MWWorld return mWorldScene->hasCellChanged(); } - void World::setGlobalInt (const std::string& name, int value) + void World::setGlobalInt(std::string_view name, int value) { bool dateUpdated = mCurrentDate->updateGlobalInt(name, value); if (dateUpdated) @@ -621,7 +637,7 @@ namespace MWWorld mGlobalVariables[name].setInteger (value); } - void World::setGlobalFloat (const std::string& name, float value) + void World::setGlobalFloat(std::string_view name, float value) { bool dateUpdated = mCurrentDate->updateGlobalFloat(name, value); if (dateUpdated) @@ -630,17 +646,17 @@ namespace MWWorld mGlobalVariables[name].setFloat(value); } - int World::getGlobalInt (const std::string& name) const + int World::getGlobalInt(std::string_view name) const { return mGlobalVariables[name].getInteger(); } - float World::getGlobalFloat (const std::string& name) const + float World::getGlobalFloat(std::string_view name) const { return mGlobalVariables[name].getFloat(); } - char World::getGlobalVariableType (const std::string& name) const + char World::getGlobalVariableType (std::string_view name) const { return mGlobalVariables.getType (name); } @@ -654,13 +670,19 @@ namespace MWWorld { if (!cell) cell = mWorldScene->getCurrentCell(); + return getCellName(cell->getCell()); + } - if (!cell->getCell()->isExterior() || !cell->getCell()->mName.empty()) - return cell->getCell()->mName; - - if (const ESM::Region* region = mStore.get().search (cell->getCell()->mRegion)) - return region->mName; + std::string World::getCellName(const ESM::Cell* cell) const + { + if (cell) + { + if (!cell->isExterior() || !cell->mName.empty()) + return cell->mName; + if (const ESM::Region* region = mStore.get().search (cell->mRegion)) + return region->mName; + } return mStore.get().find ("sDefaultCellname")->mValue.getString(); } @@ -669,7 +691,7 @@ namespace MWWorld mLocalScripts.remove (ref); } - Ptr World::searchPtr (const std::string& name, bool activeOnly, bool searchInContainers) + Ptr World::searchPtr (std::string_view name, bool activeOnly, bool searchInContainers) { Ptr ret; // the player is always in an active cell. @@ -712,12 +734,12 @@ namespace MWWorld return ptr; } - Ptr World::getPtr (const std::string& name, bool activeOnly) + Ptr World::getPtr (std::string_view name, bool activeOnly) { Ptr ret = searchPtr(name, activeOnly); if (!ret.isEmpty()) return ret; - std::string error = "failed to find an instance of object '" + name + "'"; + std::string error = "failed to find an instance of object '" + std::string(name) + "'"; if (activeOnly) error += " in active cells"; throw std::runtime_error(error); @@ -744,7 +766,7 @@ namespace MWWorld FindContainerVisitor(const ConstPtr& containedPtr) : mContainedPtr(containedPtr) {} - bool operator() (Ptr ptr) + bool operator() (const Ptr& ptr) { if (mContainedPtr.getContainerStore() == &ptr.getClass().getContainerStore(ptr)) { @@ -783,9 +805,9 @@ namespace MWWorld void World::addContainerScripts(const Ptr& reference, CellStore * cell) { - if( reference.getTypeName()==typeid (ESM::Container).name() || - reference.getTypeName()==typeid (ESM::NPC).name() || - reference.getTypeName()==typeid (ESM::Creature).name()) + if( reference.getType()==ESM::Container::sRecordId || + reference.getType()==ESM::NPC::sRecordId || + reference.getType()==ESM::Creature::sRecordId) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) @@ -803,7 +825,8 @@ namespace MWWorld void World::enable (const Ptr& reference) { - // enable is a no-op for items in containers + MWBase::Environment::get().getLuaManager()->registerObject(reference); + if (!reference.isInCell()) return; @@ -825,9 +848,9 @@ namespace MWWorld void World::removeContainerScripts(const Ptr& reference) { - if( reference.getTypeName()==typeid (ESM::Container).name() || - reference.getTypeName()==typeid (ESM::NPC).name() || - reference.getTypeName()==typeid (ESM::Creature).name()) + if( reference.getType()==ESM::Container::sRecordId || + reference.getType()==ESM::NPC::sRecordId || + reference.getType()==ESM::Creature::sRecordId) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) @@ -854,6 +877,7 @@ namespace MWWorld if (reference == getPlayerPtr()) throw std::runtime_error("can not disable player object"); + MWBase::Environment::get().getLuaManager()->deregisterObject(reference); reference.getRefData().disable(); if (reference.getCellRef().getRefNum().hasContentFile()) @@ -864,7 +888,10 @@ namespace MWWorld } if(mWorldScene->getActiveCells().find (reference.getCell())!=mWorldScene->getActiveCells().end() && reference.getRefData().getCount()) + { mWorldScene->removeObjectFromScene (reference); + mWorldScene->addPostponedPhysicsObjects(); + } } void World::advanceTime (double hours, bool incremental) @@ -889,6 +916,7 @@ namespace MWWorld { mRendering->notifyWorldSpaceChanged(); mProjectileManager->clear(); + mDiscardMovements = true; } } @@ -897,6 +925,12 @@ namespace MWWorld return mCurrentDate->getTimeScaleFactor(); } + void World::setSimulationTimeScale(float scale) + { + mSimulationTimeScale = std::max(0.f, scale); + MWBase::Environment::get().getSoundManager()->setSimulationTimeScale(mSimulationTimeScale); + } + TimeStamp World::getTimeStamp() const { return mCurrentDate->getTimeStamp(); @@ -984,7 +1018,7 @@ namespace MWWorld return mWorldScene->markCellAsUnchanged(); } - float World::getMaxActivationDistance () + float World::getMaxActivationDistance() const { if (mActivationDistanceOverride >= 0) return static_cast(mActivationDistanceOverride); @@ -1008,7 +1042,7 @@ namespace MWWorld if (!facedObject.isEmpty() && !facedObject.getClass().allowTelekinesis(facedObject) && mDistanceToFacedObject > getMaxActivationDistance() && !MWBase::Environment::get().getWindowManager()->isGuiMode()) - return 0; + return nullptr; } return facedObject; } @@ -1110,18 +1144,12 @@ namespace MWWorld } } - MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z, bool movePhysics) + MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics, bool keepActive) { ESM::Position pos = ptr.getRefData().getPosition(); - - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - + std::memcpy(pos.pos, &position, sizeof(osg::Vec3f)); ptr.getRefData().setPosition(pos); - osg::Vec3f vec(x, y, z); - CellStore *currCell = ptr.isInCell() ? ptr.getCell() : nullptr; // currCell == nullptr should only happen for player, during initial startup bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || (currCell && mWorldScene->isCellActive(*currCell)); @@ -1161,7 +1189,8 @@ namespace MWWorld if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); - mWorldScene->addObjectToScene(newPtr); + if(newPtr.getRefData().isEnabled()) + mWorldScene->addObjectToScene(newPtr); std::string script = newPtr.getClass().getScript(newPtr); if (!script.empty()) @@ -1172,13 +1201,13 @@ namespace MWWorld } else if (!newCellActive && currCellActive) { - mWorldScene->removeObjectFromScene(ptr); + mWorldScene->removeObjectFromScene(ptr, keepActive); mLocalScripts.remove(ptr); removeContainerScripts (ptr); haveToMove = false; newPtr = currCell->moveTo(ptr, newCell); - newPtr.getRefData().setBaseNode(0); + newPtr.getRefData().setBaseNode(nullptr); } else if (!currCellActive && !newCellActive) newPtr = currCell->moveTo(ptr, newCell); @@ -1210,16 +1239,16 @@ namespace MWWorld } if (haveToMove && newPtr.getRefData().getBaseNode()) { - mWorldScene->updateObjectPosition(newPtr, vec, movePhysics); + mWorldScene->updateObjectPosition(newPtr, position, movePhysics); if (movePhysics) { if (const auto object = mPhysics->getObject(ptr)) - updateNavigatorObject(object); + updateNavigatorObject(*object); } } if (isPlayer) - mWorldScene->playerMoved(vec); + mWorldScene->playerMoved(position); else { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); @@ -1229,51 +1258,53 @@ namespace MWWorld return newPtr; } - MWWorld::Ptr World::moveObjectImp(const Ptr& ptr, float x, float y, float z, bool movePhysics, bool moveToActive) + MWWorld::Ptr World::moveObject(const Ptr& ptr, const osg::Vec3f& position, bool movePhysics, bool moveToActive) { - int cellX, cellY; - positionToIndex(x, y, cellX, cellY); + const osg::Vec2i index = positionToCellIndex(position.x(), position.y()); CellStore* cell = ptr.getCell(); - CellStore* newCell = getExterior(cellX, cellY); + CellStore* newCell = getExterior(index.x(), index.y()); bool isCellActive = getPlayerPtr().isInCell() && getPlayerPtr().getCell()->isExterior() && mWorldScene->isCellActive(*newCell); if (cell->isExterior() || (moveToActive && isCellActive && ptr.getClass().isActor())) cell = newCell; - return moveObject(ptr, cell, x, y, z, movePhysics); + return moveObject(ptr, cell, position, movePhysics); } - MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z, bool moveToActive) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) { - return moveObjectImp(ptr, x, y, z, true, moveToActive); + auto* actor = mPhysics->getActor(ptr); + osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; + if (actor) + actor->adjustPosition(vec); + if (ptr.getClass().isActor()) + return moveObject(ptr, newpos, false, ptr != getPlayerPtr()); + return moveObject(ptr, newpos); } - void World::scaleObject (const Ptr& ptr, float scale) + void World::scaleObject (const Ptr& ptr, float scale, bool force) { + if (!force && scale == ptr.getCellRef().getScale()) + return; if (mPhysics->getActor(ptr)) - mNavigator->removeAgent(getPathfindingHalfExtents(ptr)); + mNavigator->removeAgent(getPathfindingAgentBounds(ptr)); - if (scale != ptr.getCellRef().getScale()) - { - ptr.getCellRef().setScale(scale); - mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); - mWorldScene->removeFromPagedRefs(ptr); - } + ptr.getCellRef().setScale(scale); + mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); + mWorldScene->removeFromPagedRefs(ptr); - if(ptr.getRefData().getBaseNode() != 0) + if(ptr.getRefData().getBaseNode() != nullptr) mWorldScene->updateObjectScale(ptr); if (mPhysics->getActor(ptr)) - mNavigator->addAgent(getPathfindingHalfExtents(ptr)); + mNavigator->addAgent(getPathfindingAgentBounds(ptr)); else if (const auto object = mPhysics->getObject(ptr)) - mShouldUpdateNavigator = updateNavigatorObject(object) || mShouldUpdateNavigator; + updateNavigatorObject(*object); } - void World::rotateObjectImp(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) + void World::rotateObject(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) { - const float pi = static_cast(osg::PI); - ESM::Position pos = ptr.getRefData().getPosition(); float *objRot = pos.rot; if (flags & MWBase::RotationFlag_adjust) @@ -1295,13 +1326,9 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - const float half_pi = pi/2.f; - - if(objRot[0] < -half_pi) objRot[0] = -half_pi; - else if(objRot[0] > half_pi) objRot[0] = half_pi; - - wrap(objRot[1]); - wrap(objRot[2]); + objRot[0] = std::clamp(objRot[0], -osg::PI_2, osg::PI_2); + objRot[1] = Misc::normalizeAngle(objRot[1]); + objRot[2] = Misc::normalizeAngle(objRot[2]); } ptr.getRefData().setPosition(pos); @@ -1309,19 +1336,25 @@ namespace MWWorld mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(ptr); - if(ptr.getRefData().getBaseNode() != 0) + if(ptr.getRefData().getBaseNode() != nullptr) { const auto order = flags & MWBase::RotationFlag_inverseOrder ? RotationOrder::inverse : RotationOrder::direct; mWorldScene->updateObjectRotation(ptr, order); if (const auto object = mPhysics->getObject(ptr)) - updateNavigatorObject(object); + updateNavigatorObject(*object); } } void World::adjustPosition(const Ptr &ptr, bool force) { + if (ptr.isEmpty()) + { + Log(Debug::Warning) << "Unable to adjust position for empty object"; + return; + } + osg::Vec3f pos (ptr.getRefData().getPosition().asVec3()); if(!ptr.getRefData().getBaseNode()) @@ -1330,25 +1363,24 @@ namespace MWWorld return; } - float terrainHeight = -std::numeric_limits::max(); - if (ptr.getCell()->isExterior()) - terrainHeight = getTerrainHeightAt(pos); - - if (pos.z() < terrainHeight) - pos.z() = terrainHeight; + if (!ptr.isInCell()) + { + Log(Debug::Warning) << "Unable to adjust position for object '" << ptr.getCellRef().getRefId() << "' - it has no cell"; + return; + } - pos.z() += 20; // place slightly above. will snap down to ground with code below + const float terrainHeight = ptr.getCell()->isExterior() ? getTerrainHeightAt(pos) : -std::numeric_limits::max(); + pos.z() = std::max(pos.z(), terrainHeight) + 20; // place slightly above terrain. will snap down to ground with code below // We still should trace down dead persistent actors - they do not use the "swimdeath" animation. bool swims = ptr.getClass().isActor() && isSwimming(ptr) && !(ptr.getClass().isPersistent(ptr) && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()); if (force || !ptr.getClass().isActor() || (!isFlying(ptr) && !swims && isActorCollisionEnabled(ptr))) { osg::Vec3f traced = mPhysics->traceDown(ptr, pos, Constants::CellSizeInUnits); - if (traced.z() < pos.z()) - pos.z() = traced.z(); + pos.z() = std::min(pos.z(), traced.z()); } - moveObject(ptr, ptr.getCell(), pos.x(), pos.y(), pos.z()); + moveObject(ptr, ptr.getCell(), pos); } void World::fixPosition() @@ -1387,27 +1419,22 @@ namespace MWWorld } } - void World::rotateObject (const Ptr& ptr, float x, float y, float z, MWBase::RotationFlags flags) + void World::rotateWorldObject (const Ptr& ptr, const osg::Quat& rotate) { - rotateObjectImp(ptr, osg::Vec3f(x, y, z), flags); - } - - void World::rotateWorldObject (const Ptr& ptr, osg::Quat rotate) - { - if(ptr.getRefData().getBaseNode() != 0) + if(ptr.getRefData().getBaseNode() != nullptr) { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(ptr); mRendering->rotateObject(ptr, rotate); - mPhysics->updateRotation(ptr); + mPhysics->updateRotation(ptr, rotate); if (const auto object = mPhysics->getObject(ptr)) - updateNavigatorObject(object); + updateNavigatorObject(*object); } } - MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) + MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) { return copyObjectToCell(ptr,cell,pos,ptr.getRefData().getCount(),false); } @@ -1447,17 +1474,11 @@ namespace MWWorld ipos.pos[1] = spawnPoint.y(); ipos.pos[2] = spawnPoint.z(); - if (!referenceObject.getClass().isActor()) - { - ipos.rot[0] = referenceObject.getRefData().getPosition().rot[0]; - ipos.rot[1] = referenceObject.getRefData().getPosition().rot[1]; - } - else + if (referenceObject.getClass().isActor()) { ipos.rot[0] = 0; ipos.rot[1] = 0; } - ipos.rot[2] = referenceObject.getRefData().getPosition().rot[2]; MWWorld::Ptr placed = copyObjectToCell(ptr, referenceCell, ipos, ptr.getRefData().getCount(), false); adjustPosition(placed, true); // snap to ground @@ -1478,12 +1499,6 @@ namespace MWWorld } } - void World::positionToIndex (float x, float y, int &cellX, int &cellY) const - { - cellX = static_cast(std::floor(x / Constants::CellSizeInUnits)); - cellY = static_cast(std::floor(y / Constants::CellSizeInUnits)); - } - void World::queueMovement(const Ptr &ptr, const osg::Vec3f &velocity) { mPhysics->queueObjectMovement(ptr, velocity); @@ -1494,53 +1509,44 @@ namespace MWWorld mPhysics->updateAnimatedCollisionShape(ptr); } - void World::doPhysics(float duration) + void World::doPhysics(float duration, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { - mPhysics->stepSimulation(); processDoors(duration); - mProjectileManager->update(duration); - - const auto results = mPhysics->applyQueuedMovement(duration, mDiscardMovements); + mPhysics->stepSimulation(duration, mDiscardMovements, frameStart, frameNumber, stats); + mProjectileManager->processHits(); mDiscardMovements = false; - - for(const auto& result : results) - { - // Handle player last, in case a cell transition occurs - if(result.first != getPlayerPtr()) - moveObjectImp(result.first, result.second.x(), result.second.y(), result.second.z(), false); - } - - const auto player = results.find(getPlayerPtr()); - if (player != results.end()) - moveObjectImp(player->first, player->second.x(), player->second.y(), player->second.z(), false); + mPhysics->moveActors(); } void World::updateNavigator() { - mPhysics->forEachAnimatedObject([&] (const MWPhysics::Object* object) + mPhysics->forEachAnimatedObject([&] (const auto& pair) { - mShouldUpdateNavigator = updateNavigatorObject(object) || mShouldUpdateNavigator; + const auto [object, changed] = pair; + if (changed) + updateNavigatorObject(*object); }); for (const auto& door : mDoorStates) if (const auto object = mPhysics->getObject(door.first)) - mShouldUpdateNavigator = updateNavigatorObject(object) || mShouldUpdateNavigator; + updateNavigatorObject(*object); - if (mShouldUpdateNavigator) + auto player = getPlayerPtr(); + if (mShouldUpdateNavigator && player.getCell() != nullptr) { - mNavigator->update(getPlayerPtr().getRefData().getPosition().asVec3()); + mNavigator->update(player.getRefData().getPosition().asVec3()); mShouldUpdateNavigator = false; } } - bool World::updateNavigatorObject(const MWPhysics::Object* object) + void World::updateNavigatorObject(const MWPhysics::Object& object) { - const DetourNavigator::ObjectShapes shapes { - *object->getShapeInstance()->getCollisionShape(), - object->getShapeInstance()->getAvoidCollisionShape() - }; - return mNavigator->updateObject(DetourNavigator::ObjectId(object), shapes, object->getTransform()); + const MWWorld::Ptr ptr = object.getPtr(); + const DetourNavigator::ObjectShapes shapes(object.getShapeInstance(), + DetourNavigator::ObjectTransform {ptr.getRefData().getPosition(), ptr.getCellRef().getScale()}); + mShouldUpdateNavigator = mNavigator->updateObject(DetourNavigator::ObjectId(&object), shapes, object.getTransform()) + || mShouldUpdateNavigator; } const MWPhysics::RayCastingInterface* World::getRayCasting() const @@ -1572,14 +1578,16 @@ namespace MWWorld bool World::rotateDoor(const Ptr door, MWWorld::DoorState state, float duration) { const ESM::Position& objPos = door.getRefData().getPosition(); - float oldRot = objPos.rot[2]; + auto oldRot = objPos.asRotationVec3(); + auto newRot = oldRot; float minRot = door.getCellRef().getPosition().rot[2]; float maxRot = minRot + osg::DegreesToRadians(90.f); float diff = duration * osg::DegreesToRadians(90.f) * (state == MWWorld::DoorState::Opening ? 1 : -1); - float targetRot = std::min(std::max(minRot, oldRot + diff), maxRot); - rotateObject(door, objPos.rot[0], objPos.rot[1], targetRot, MWBase::RotationFlag_none); + float targetRot = std::clamp(oldRot.z() + diff, minRot, maxRot); + newRot.z() = targetRot; + rotateObject(door, newRot, MWBase::RotationFlag_none); bool reached = (targetRot == maxRot && state != MWWorld::DoorState::Idle) || targetRot == minRot; @@ -1630,7 +1638,7 @@ namespace MWWorld MWBase::Environment::get().getSoundManager()->stopSound3D(door, closeSound); } - rotateObject(door, objPos.rot[0], objPos.rot[1], oldRot, MWBase::RotationFlag_none); + rotateObject(door, oldRot, MWBase::RotationFlag_none); } return reached; @@ -1814,11 +1822,13 @@ namespace MWWorld updateNavigator(); } - updatePlayer(); + mPlayer->update(); mPhysics->debugDraw(); - mWorldScene->update (duration, paused); + mWorldScene->update(duration); + + mRendering->update(duration, paused); updateSoundListener(); @@ -1830,60 +1840,19 @@ namespace MWWorld } } - void World::updatePhysics (float duration, bool paused) + void World::updatePhysics (float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { if (!paused) { - doPhysics (duration); - } - } - - void World::updatePlayer() - { - MWWorld::Ptr player = getPlayerPtr(); - - // TODO: move to MWWorld::Player - - if (player.getCell()->isExterior()) - { - ESM::Position pos = player.getRefData().getPosition(); - mPlayer->setLastKnownExteriorPosition(pos.asVec3()); - } - - bool isWerewolf = player.getClass().getNpcStats(player).isWerewolf(); - bool isFirstPerson = mRendering->getCamera()->isFirstPerson(); - if (isWerewolf && isFirstPerson) - { - float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV"); - if (werewolfFov != 0) - mRendering->overrideFieldOfView(werewolfFov); - MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(true); + doPhysics (duration, frameStart, frameNumber, stats); } else { - mRendering->resetFieldOfView(); - MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(false); + // zero the async stats if we are paused + stats.setAttribute(frameNumber, "physicsworker_time_begin", 0); + stats.setAttribute(frameNumber, "physicsworker_time_taken", 0); + stats.setAttribute(frameNumber, "physicsworker_time_end", 0); } - - // Sink the camera while sneaking - bool sneaking = player.getClass().getCreatureStats(getPlayerPtr()).getStance(MWMechanics::CreatureStats::Stance_Sneak); - bool swimming = isSwimming(player); - bool flying = isFlying(player); - - static const float i1stPersonSneakDelta = mStore.get().find("i1stPersonSneakDelta")->mValue.getFloat(); - if (sneaking && !swimming && !flying) - mRendering->getCamera()->setSneakOffset(i1stPersonSneakDelta); - else - mRendering->getCamera()->setSneakOffset(0.f); - - int blind = 0; - auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); - if (!mGodMode) - blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); - MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); - - int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); - mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); } void World::preloadSpells() @@ -1921,11 +1890,12 @@ namespace MWWorld void World::updateSoundListener() { + osg::Vec3f cameraPosition = mRendering->getCamera()->getPosition(); const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition(); osg::Vec3f listenerPos; if (isFirstPerson()) - listenerPos = mRendering->getCameraPosition(); + listenerPos = cameraPosition; else listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z()); @@ -1936,7 +1906,7 @@ namespace MWWorld osg::Vec3f forward = listenerOrient * osg::Vec3f(0,1,0); osg::Vec3f up = listenerOrient * osg::Vec3f(0,0,1); - bool underwater = isUnderwater(getPlayerPtr().getCell(), mRendering->getCameraPosition()); + bool underwater = isUnderwater(getPlayerPtr().getCell(), cameraPosition); MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater); } @@ -1989,7 +1959,7 @@ namespace MWWorld rayToObject = mRendering->castCameraToViewportRay(0.5f, 0.5f, maxDistance, ignorePlayer); facedObject = rayToObject.mHitObject; - if (facedObject.isEmpty() && rayToObject.mHitRefnum.hasContentFile()) + if (facedObject.isEmpty() && rayToObject.mHitRefnum.isSet()) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { @@ -2004,6 +1974,25 @@ namespace MWWorld return facedObject; } + bool World::castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, + bool ignorePlayer, bool ignoreActors) + { + MWRender::RenderingManager::RayResult rayRes = mRendering->castRay(from, to, ignorePlayer, ignoreActors); + res.mHit = rayRes.mHit; + res.mHitPos = rayRes.mHitPointWorld; + res.mHitNormal = rayRes.mHitNormalWorld; + res.mHitObject = rayRes.mHitObject; + if (res.mHitObject.isEmpty() && rayRes.mHitRefnum.isSet()) + { + for (CellStore* cellstore : mWorldScene->getActiveCells()) + { + res.mHitObject = cellstore->searchViaRefNum(rayRes.mHitRefnum); + if (!res.mHitObject.isEmpty()) break; + } + } + return res.mHit; + } + bool World::isCellExterior() const { const CellStore *currentCell = mWorldScene->getCurrentCell(); @@ -2032,6 +2021,16 @@ namespace MWWorld return mWeatherManager->getWeatherID(); } + int World::getNextWeather() const + { + return mWeatherManager->getNextWeatherID(); + } + + float World::getWeatherTransition() const + { + return mWeatherManager->getTransitionFactor(); + } + unsigned int World::getNightDayMode() const { return mWeatherManager->getNightDayMode(); @@ -2062,11 +2061,6 @@ namespace MWWorld struct GetDoorMarkerVisitor { - GetDoorMarkerVisitor(std::vector& out) - : mOut(out) - { - } - std::vector& mOut; bool operator()(const MWWorld::Ptr& ptr) @@ -2092,11 +2086,9 @@ namespace MWWorld else { cellid.mPaged = true; - MWBase::Environment::get().getWorld()->positionToIndex( - ref.mRef.getDoorDest().pos[0], - ref.mRef.getDoorDest().pos[1], - cellid.mIndex.mX, - cellid.mIndex.mY); + const osg::Vec2i index = positionToCellIndex(ref.mRef.getDoorDest().pos[0], ref.mRef.getDoorDest().pos[1]); + cellid.mIndex.mX = index.x(); + cellid.mIndex.mY = index.y(); } newMarker.dest = cellid; @@ -2112,7 +2104,7 @@ namespace MWWorld void World::getDoorMarkers (CellStore* cell, std::vector& out) { - GetDoorMarkerVisitor visitor(out); + GetDoorMarkerVisitor visitor {out}; cell->forEachType(visitor); } @@ -2198,9 +2190,8 @@ namespace MWWorld throw std::runtime_error("copyObjectToCell(): cannot copy object to null cell"); if (cell->isExterior()) { - int cellX, cellY; - positionToIndex(pos.pos[0], pos.pos[1], cellX, cellY); - cell = mCells.getExterior(cellX, cellY); + const osg::Vec2i index = positionToCellIndex(pos.pos[0], pos.pos[1]); + cell = mCells.getExterior(index.x(), index.y()); } MWWorld::Ptr dropped = @@ -2240,7 +2231,7 @@ namespace MWWorld pos.pos[0] -= adjust.x(); pos.pos[1] -= adjust.y(); pos.pos[2] -= adjust.z(); - moveObject(dropped, pos.pos[0], pos.pos[1], pos.pos[2]); + moveObject(dropped, pos.asVec3()); } } @@ -2290,8 +2281,12 @@ namespace MWWorld if (stats.isDead()) return false; + const bool isPlayer = ptr == getPlayerConstPtr(); + if (!(isPlayer && mGodMode) && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0) + return false; + if (ptr.getClass().canFly(ptr)) - return !stats.isParalyzed(); + return true; if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && isLevitationEnabled()) @@ -2384,7 +2379,7 @@ namespace MWWorld bool World::isFirstPerson() const { - return mRendering->getCamera()->isFirstPerson(); + return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::FirstPerson; } bool World::isPreviewModeEnabled() const @@ -2392,11 +2387,6 @@ namespace MWWorld return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::Preview; } - void World::togglePreviewMode(bool enable) - { - mRendering->getCamera()->togglePreviewMode(enable); - } - bool World::toggleVanityMode(bool enable) { return mRendering->getCamera()->toggleVanityMode(enable); @@ -2412,48 +2402,49 @@ namespace MWWorld mRendering->getCamera()->applyDeferredPreviewRotationToPlayer(dt); } - void World::allowVanityMode(bool allow) - { - mRendering->getCamera()->allowVanityMode(allow); - } + MWRender::Camera* World::getCamera() { return mRendering->getCamera(); } bool World::vanityRotateCamera(float * rot) { - if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled()) + auto* camera = mRendering->getCamera(); + if(!camera->isVanityOrPreviewModeEnabled()) return false; - mRendering->getCamera()->rotateCamera(rot[0], rot[2], true); + camera->setPitch(camera->getPitch() + rot[0]); + camera->setYaw(camera->getYaw() + rot[2]); return true; } - void World::adjustCameraDistance(float dist) + void World::saveLoaded() { - mRendering->getCamera()->adjustCameraDistance(dist); + mStore.validateDynamic(); } void World::setupPlayer() { const ESM::NPC *player = mStore.get().find("player"); if (!mPlayer) - mPlayer.reset(new MWWorld::Player(player)); + mPlayer = std::make_unique(player); else { // Remove the old CharacterController - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); - mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); + mNavigator->removeAgent(getPathfindingAgentBounds(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(getPlayerPtr()); mPlayer->set(player); } Ptr ptr = mPlayer->getPlayer(); mRendering->setupPlayer(ptr); + MWBase::Environment::get().getLuaManager()->setupPlayer(ptr); } void World::renderPlayer() { - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); MWWorld::Ptr player = getPlayerPtr(); @@ -2462,8 +2453,8 @@ namespace MWWorld player.getClass().getInventoryStore(player).setInvListener(anim, player); player.getClass().getInventoryStore(player).setContListener(anim); - scaleObject(player, player.getCellRef().getScale()); // apply race height - rotateObject(player, 0.f, 0.f, 0.f, MWBase::RotationFlag_inverseOrder | MWBase::RotationFlag_adjust); + scaleObject(player, player.getCellRef().getScale(), true); // apply race height + rotateObject(player, osg::Vec3f(), MWBase::RotationFlag_inverseOrder | MWBase::RotationFlag_adjust); MWBase::Environment::get().getMechanicsManager()->add(getPlayerPtr()); MWBase::Environment::get().getWindowManager()->watchActor(getPlayerPtr()); @@ -2475,8 +2466,7 @@ namespace MWWorld applyLoopingParticles(player); - mDefaultHalfExtents = mPhysics->getOriginalHalfExtents(getPlayerPtr()); - mNavigator->addAgent(getPathfindingHalfExtents(getPlayerConstPtr())); + mNavigator->addAgent(getPathfindingAgentBounds(getPlayerConstPtr())); } World::RestPermitted World::canRest () const @@ -2510,7 +2500,14 @@ namespace MWWorld MWRender::Animation* World::getAnimation(const MWWorld::Ptr &ptr) { - return mRendering->getAnimation(ptr); + auto* animation = mRendering->getAnimation(ptr); + if(!animation) { + mWorldScene->removeFromPagedRefs(ptr); + animation = mRendering->getAnimation(ptr); + if(animation) + mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); + } + return animation; } const MWRender::Animation* World::getAnimation(const MWWorld::ConstPtr &ptr) const @@ -2520,12 +2517,12 @@ namespace MWWorld void World::screenshot(osg::Image* image, int w, int h) { - mRendering->screenshotFramebuffer(image, w, h); + mRendering->screenshot(image, w, h); } - bool World::screenshot360(osg::Image* image, std::string settingStr) + bool World::screenshot360(osg::Image* image) { - return mRendering->screenshot360(image,settingStr); + return mRendering->screenshot360(image); } void World::activateDoor(const MWWorld::Ptr& door) @@ -2801,10 +2798,9 @@ namespace MWWorld // door to exterior if (door->getDestCell().empty()) { - int x, y; ESM::Position doorDest = door->getDoorDest(); - positionToIndex(doorDest.pos[0], doorDest.pos[1], x, y); - source = getExterior(x, y); + const osg::Vec2i index = positionToCellIndex(doorDest.pos[0], doorDest.pos[1]); + source = getExterior(index.x(), index.y()); } // door to interior else @@ -2901,7 +2897,7 @@ namespace MWWorld mRendering->rebuildPtr(getPlayerPtr()); } - bool World::getGodModeState() + bool World::getGodModeState() const { return mGodMode; } @@ -2924,9 +2920,21 @@ namespace MWWorld return mScriptsEnabled; } - void World::loadContentFiles(const Files::Collections& fileCollections, - const std::vector& content, ContentLoader& contentLoader) + void World::loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, + ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) { + GameContentLoader gameContentLoader; + EsmLoader esmLoader(mStore, mReaders, encoder); + + gameContentLoader.addLoader(".esm", esmLoader); + gameContentLoader.addLoader(".esp", esmLoader); + gameContentLoader.addLoader(".omwgame", esmLoader); + gameContentLoader.addLoader(".omwaddon", esmLoader); + gameContentLoader.addLoader(".project", esmLoader); + + OMWScriptsLoader omwScriptsLoader(mStore); + gameContentLoader.addLoader(".omwscripts", omwScriptsLoader); + int idx = 0; for (const std::string &file : content) { @@ -2934,7 +2942,7 @@ namespace MWWorld const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string()); if (col.doesExist(file)) { - contentLoader.load(col.getPath(file), idx); + gameContentLoader.load(col.getPath(file), idx, listener); } else { @@ -2943,6 +2951,19 @@ namespace MWWorld } idx++; } + + if (const auto v = esmLoader.getMasterFileFormat(); v.has_value() && *v == 0) + ensureNeededRecords(); // Insert records that may not be present in all versions of master files. + } + + void World::loadGroundcoverFiles(const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) + { + if (!Settings::Manager::getBool("enabled", "Groundcover")) return; + + Log(Debug::Info) << "Loading groundcover:"; + + mGroundcoverStore.init(mStore.get(), fileCollections, groundcoverFiles, encoder, listener); } bool World::startSpellCast(const Ptr &actor) @@ -2958,11 +2979,12 @@ namespace MWWorld if (!selectedSpell.empty()) { const ESM::Spell* spell = mStore.get().find(selectedSpell); + int spellCost = MWMechanics::calcSpellCost(*spell); // Check mana bool godmode = (isPlayer && mGodMode); MWMechanics::DynamicStat magicka = stats.getMagicka(); - if (magicka.getCurrent() < spell->mData.mCost && !godmode) + if (spellCost > 0 && magicka.getCurrent() < spellCost && !godmode) { message = "#{sMagicInsufficientSP}"; fail = true; @@ -2978,7 +3000,7 @@ namespace MWWorld // Reduce mana if (!fail && !godmode) { - magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); + magicka.setCurrent(magicka.getCurrent() - spellCost); stats.setMagicka(magicka); } } @@ -3087,7 +3109,20 @@ namespace MWWorld { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (inv.getSelectedEnchantItem() != inv.end()) - cast.cast(*inv.getSelectedEnchantItem()); + { + const auto& itemPtr = *inv.getSelectedEnchantItem(); + auto [slots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + int slot = 0; + for(std::size_t i = 0; i < slots.size(); ++i) + { + if(inv.getSlot(slots[i]) == inv.getSelectedEnchantItem()) + { + slot = slots[i]; + break; + } + } + cast.cast(itemPtr, slot); + } } } @@ -3101,9 +3136,10 @@ namespace MWWorld const osg::Vec3f sourcePos = worldPos + orient * osg::Vec3f(0,-1,0) * 64.f; // Early out if the launch position is underwater - bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); + bool underwater = isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); if (underwater) { + MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); mRendering->emitWaterRipple(worldPos); return; } @@ -3121,51 +3157,34 @@ namespace MWWorld mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } - void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) + void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) { - mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); + mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection, slot); } - class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor + void World::updateProjectilesCasters() { - private: - MWWorld::Ptr mActor; - - public: - ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) - : mActor(actor) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override - { - const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const auto magicEffect = store.get().find(key.mId); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) - return; - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = store.get().find (magicEffect->mHit); - else - castStatic = store.get().find ("VFX_DefaultHit"); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); - } - }; + mProjectileManager->updateCasters(); + } - void World::applyLoopingParticles(const MWWorld::Ptr& ptr) + void World::applyLoopingParticles(const MWWorld::Ptr& ptr) const { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { - ApplyLoopingParticlesVisitor visitor(ptr); - cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); - cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); - if (cls.hasInventoryStore(ptr)) - cls.getInventoryStore(ptr).visitEffectSources(visitor); + std::set playing; + for(const auto& params : cls.getCreatureStats(ptr).getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(playing.insert(effect.mEffectId).second) + { + const auto magicEffect = getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) + MWMechanics::playEffects(ptr, *magicEffect, false); + } + } + } } } @@ -3176,10 +3195,7 @@ namespace MWWorld void World::breakInvisibility(const Ptr &actor) { - actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility); - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(actor, ESM::MagicEffect::Invisibility); // Normally updated once per frame, but here it is kinda important to do it right away. MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); @@ -3381,15 +3397,12 @@ namespace MWWorld struct AddDetectedReferenceVisitor { - AddDetectedReferenceVisitor(std::vector& out, const Ptr& detector, World::DetectionType type, float squaredDist) - : mOut(out), mDetector(detector), mSquaredDist(squaredDist), mType(type) - { - } - std::vector& mOut; Ptr mDetector; float mSquaredDist; World::DetectionType mType; + const MWWorld::ESMStore& mStore; + bool operator() (const MWWorld::Ptr& ptr) { if ((ptr.getRefData().getPosition().asVec3() - mDetector.getRefData().getPosition().asVec3()).length2() >= mSquaredDist) @@ -3399,12 +3412,33 @@ namespace MWWorld return true; // Consider references inside containers as well (except if we are looking for a Creature, they cannot be in containers) - bool isContainer = ptr.getClass().getTypeName() == typeid(ESM::Container).name(); + bool isContainer = ptr.getClass().getType() == ESM::Container::sRecordId; if (mType != World::Detect_Creature && (ptr.getClass().isActor() || isContainer)) { // but ignore containers without resolved content if (isContainer && ptr.getRefData().getCustomData() == nullptr) + { + for(const auto& containerItem : ptr.get()->mBase->mInventory.mList) + { + if(containerItem.mCount) + { + try + { + ManualRef ref(mStore, containerItem.mItem, containerItem.mCount); + if(needToAdd(ref.getPtr(), mDetector)) + { + mOut.push_back(ptr); + return true; + } + } + catch (const std::exception&) + { + // Ignore invalid item id + } + } + } return true; + } MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); { @@ -3432,10 +3466,10 @@ namespace MWWorld // If in werewolf form, this detects only NPCs, otherwise only creatures if (detector.getClass().isNpc() && detector.getClass().getNpcStats(detector).isWerewolf()) { - if (ptr.getClass().getTypeName() != typeid(ESM::NPC).name()) + if (ptr.getClass().getType() != ESM::NPC::sRecordId) return false; } - else if (ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) + else if (ptr.getClass().getType() != ESM::Creature::sRecordId) return false; if (ptr.getClass().getCreatureStats(ptr).isDead()) @@ -3465,7 +3499,7 @@ namespace MWWorld dist = feetToGameUnits(dist); - AddDetectedReferenceVisitor visitor (out, ptr, type, dist*dist); + AddDetectedReferenceVisitor visitor {out, ptr, dist * dist, type, mStore}; for (CellStore* cellStore : mWorldScene->getActiveCells()) { @@ -3561,14 +3595,13 @@ namespace MWWorld void World::goToJail() { + const MWWorld::Ptr player = getPlayerPtr(); if (!mGoToJail) { // Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first) mGoToJail = true; mPlayerInJail = true; - MWWorld::Ptr player = getPlayerPtr(); - int bounty = player.getClass().getNpcStats(player).getBounty(); player.getClass().getNpcStats(player).setBounty(0); mPlayer->recordCrimeId(); @@ -3581,6 +3614,12 @@ namespace MWWorld } else { + if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(player)) + { + mPlayer->setAttackingOrSpell(false); + } + + mPlayer->setDrawState(MWMechanics::DrawState::Nothing); mGoToJail = false; MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); @@ -3638,11 +3677,11 @@ namespace MWWorld const ESM::CreatureLevList* list = mStore.get().find(creatureList); static int iNumberCreatures = mStore.get().find("iNumberCreatures")->mValue.getInteger(); - int numCreatures = 1 + Misc::Rng::rollDice(iNumberCreatures); // [1, iNumberCreatures] + int numCreatures = 1 + Misc::Rng::rollDice(iNumberCreatures, mPrng); // [1, iNumberCreatures] for (int i=0; igetVFS()); mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); } @@ -3672,7 +3713,7 @@ namespace MWWorld } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, - const std::string& id, const std::string& sourceName, const bool fromProjectile) + const std::string& id, const std::string& sourceName, const bool fromProjectile, int slot) { std::map > toApply; for (const ESM::ENAMstruct& effectInfo : effects.mList) @@ -3700,11 +3741,15 @@ namespace MWWorld if (effectInfo.mArea <= 0) { if (effectInfo.mRange == ESM::RT_Target) - mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, 1.0f); + mRendering->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, mResourceSystem->getVFS()), + texture, origin, 1.0f); continue; } else - mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, static_cast(effectInfo.mArea * 2)); + mRendering->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, mResourceSystem->getVFS()), + texture, origin, static_cast(effectInfo.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) static const std::string schools[] = { @@ -3750,10 +3795,10 @@ namespace MWWorld cast.mHitPosition = origin; cast.mId = id; cast.mSourceName = sourceName; - cast.mStack = false; + cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; - cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); + cast.inflict(applyPair.first, caster, effectsToApply, rangeType, true); } } @@ -3763,14 +3808,17 @@ namespace MWWorld if (object.getRefData().activate()) { - std::shared_ptr action = (object.getClass().activate(object, actor)); + MWBase::Environment::get().getLuaManager()->objectActivated(object, actor); + std::unique_ptr action = object.getClass().activate(object, actor); action->execute (actor); } } struct ResetActorsVisitor { - bool operator() (Ptr ptr) + World& mWorld; + + bool operator() (const Ptr& ptr) { if (ptr.getClass().isActor() && ptr.getCellRef().hasContentFile()) { @@ -3778,18 +3826,19 @@ namespace MWWorld return true; const ESM::Position& origPos = ptr.getCellRef().getPosition(); - MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.pos[0], origPos.pos[1], origPos.pos[2]); - MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.rot[0], origPos.rot[1], origPos.rot[2]); + mWorld.moveObject(ptr, origPos.asVec3()); + mWorld.rotateObject(ptr, origPos.asRotationVec3()); ptr.getClass().adjustPosition(ptr, true); } return true; } }; + void World::resetActors() { for (CellStore* cellstore : mWorldScene->getActiveCells()) { - ResetActorsVisitor visitor; + ResetActorsVisitor visitor {*this}; cellstore->forEach(visitor); } } @@ -3802,10 +3851,11 @@ namespace MWWorld return false; } - osg::Vec3f World::aimToTarget(const ConstPtr &actor, const MWWorld::ConstPtr& target) + osg::Vec3f World::aimToTarget(const ConstPtr &actor, const ConstPtr &target, bool isRangedCombat) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); - weaponPos.z() += mPhysics->getHalfExtents(actor).z(); + float heightRatio = isRangedCombat ? 2.f * Constants::TorsoHeight : 1.f; + weaponPos.z() += mPhysics->getHalfExtents(actor).z() * heightRatio; osg::Vec3f targetPos = mPhysics->getCollisionObjectPosition(target); return (targetPos - weaponPos); } @@ -3830,7 +3880,7 @@ namespace MWWorld if (!model.empty()) scene->preload(model, ref.getPtr().getClass().useAnim()); } - catch(std::exception& e) + catch(std::exception&) { } } @@ -3863,9 +3913,9 @@ namespace MWWorld } void World::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const { - mRendering->updateActorPath(actor, path, halfExtents, start, end); + mRendering->updateActorPath(actor, path, agentBounds, start, end); } void World::removeActorPath(const MWWorld::ConstPtr& actor) const @@ -3878,12 +3928,13 @@ namespace MWWorld mRendering->setNavMeshNumber(value); } - osg::Vec3f World::getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const + DetourNavigator::AgentBounds World::getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const { - if (actor.isInCell() && actor.getCell()->isExterior()) - return mDefaultHalfExtents; // Using default half extents for better performance + const MWPhysics::Actor* physicsActor = mPhysics->getActor(actor); + if (physicsActor == nullptr || (actor.isInCell() && actor.getCell()->isExterior())) + return DetourNavigator::AgentBounds {DetourNavigator::defaultCollisionShapeType, mDefaultHalfExtents}; else - return getHalfExtents(actor); + return DetourNavigator::AgentBounds {physicsActor->getCollisionShapeType(), physicsActor->getHalfExtents()}; } bool World::hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const @@ -3895,7 +3946,7 @@ namespace MWWorld btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto toLocal = object->getTransform().inverse(); const auto localFrom = toLocal(Misc::Convert::toBullet(position)); @@ -3906,9 +3957,10 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const Misc::Span& ignore, std::vector* occupyingActors) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); + return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const @@ -3927,4 +3979,20 @@ namespace MWWorld { return mCells.getAll(id); } + + Misc::Rng::Generator& World::getPrng() + { + return mPrng; + } + + MWRender::PostProcessor* World::getPostProcessor() + { + return mRendering->getPostProcessor(); + } + + void World::setActorActive(const MWWorld::Ptr& ptr, bool value) + { + if (MWPhysics::Actor* const actor = mPhysics->getActor(ptr)) + actor->setActive(value); + } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 909ac1d412..1849937c63 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -4,7 +4,8 @@ #include #include -#include +#include +#include #include "../mwbase/world.hpp" @@ -16,6 +17,7 @@ #include "timestamp.hpp" #include "globals.hpp" #include "contentloader.hpp" +#include "groundcoverstore.hpp" namespace osg { @@ -53,6 +55,7 @@ namespace MWRender class SkyManager; class Animation; class Camera; + class PostProcessor; } namespace ToUTF8 @@ -60,8 +63,6 @@ namespace ToUTF8 class Utf8Encoder; } -struct ContentLoader; - namespace MWPhysics { class Object; @@ -81,11 +82,12 @@ namespace MWWorld private: Resource::ResourceSystem* mResourceSystem; - std::vector mEsm; + ESM::ReadersCache mReaders; MWWorld::ESMStore mStore; + GroundcoverStore mGroundcoverStore; LocalScripts mLocalScripts; MWWorld::Globals mGlobalVariables; - + Misc::Rng::Generator mPrng; Cells mCells; std::string mCurrentWorldSpace; @@ -97,7 +99,7 @@ namespace MWWorld std::unique_ptr mWorldScene; std::unique_ptr mWeatherManager; std::unique_ptr mCurrentDate; - std::shared_ptr mProjectileManager; + std::unique_ptr mProjectileManager; bool mSky; bool mGodMode; @@ -130,21 +132,19 @@ namespace MWWorld std::map mDoorStates; ///< only holds doors that are currently moving. 1 = opening, 2 = closing + uint32_t mRandomSeed{}; + + float mSimulationTimeScale = 1.0; + // not implemented World (const World&); World& operator= (const World&); void updateWeather(float duration, bool paused = false); - void rotateObjectImp (const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags); - - Ptr moveObjectImp (const Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false); - ///< @return an updated Ptr in case the Ptr's cell changes - Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos); void updateSoundListener(); - void updatePlayer(); void preloadSpells(); @@ -157,12 +157,12 @@ namespace MWWorld void processDoors(float duration); ///< Run physics simulation and modify \a world accordingly. - void doPhysics(float duration); + void doPhysics(float duration, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); ///< Run physics simulation and modify \a world accordingly. void updateNavigator(); - bool updateNavigatorObject(const MWPhysics::Object* object); + void updateNavigatorObject(const MWPhysics::Object& object); void ensureNeededRecords(); @@ -170,14 +170,12 @@ namespace MWWorld void updateSkyDate(); - /** - * @brief loadContentFiles - Loads content files (esm,esp,omwgame,omwaddon) - * @param fileCollections- Container which holds content file names and their paths - * @param content - Container which holds content file names - * @param contentLoader - - */ - void loadContentFiles(const Files::Collections& fileCollections, - const std::vector& content, ContentLoader& contentLoader); + void loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, + ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener); + + void loadGroundcoverFiles(const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, + Loading::Listener* listener); float feetToGameUnits(float feet); float getActivationDistancePlusTelekinesis(); @@ -196,12 +194,15 @@ namespace MWWorld Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, const std::string& resourcePath, const std::string& userDataPath); virtual ~World(); + void setRandomSeed(uint32_t seed) override; + void startNewGame (bool bypass) override; ///< \param bypass Bypass regular game start. @@ -221,6 +222,8 @@ namespace MWWorld CellStore *getCell (const ESM::CellId& id) override; + bool isCellActive(CellStore* cell) const override; + void testExteriorCells() override; void testInteriorCells() override; @@ -229,13 +232,13 @@ namespace MWWorld void setWaterHeight(const float height) override; - void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) override; + void rotateWorldObject (const MWWorld::Ptr& ptr, const osg::Quat& rotate) override; bool toggleWater() override; bool toggleWorld() override; bool toggleBorders() override; - void adjustSky() override; + void adjustSky(); Player& getPlayer() override; MWWorld::Ptr getPlayerPtr() override; @@ -243,8 +246,6 @@ namespace MWWorld const MWWorld::ESMStore& getStore() const override; - std::vector& getEsmReader() override; - LocalScripts& getLocalScripts() override; bool hasCellChanged() const override; @@ -260,35 +261,36 @@ namespace MWWorld void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) override; ///< get a list of teleport door markers for a given cell, to be displayed on the local map - void setGlobalInt (const std::string& name, int value) override; + void setGlobalInt(std::string_view name, int value) override; ///< Set value independently from real type. - void setGlobalFloat (const std::string& name, float value) override; + void setGlobalFloat(std::string_view name, float value) override; ///< Set value independently from real type. - int getGlobalInt (const std::string& name) const override; + int getGlobalInt(std::string_view name) const override; ///< Get value independently from real type. - float getGlobalFloat (const std::string& name) const override; + float getGlobalFloat(std::string_view name) const override; ///< Get value independently from real type. - char getGlobalVariableType (const std::string& name) const override; + char getGlobalVariableType(std::string_view name) const override; ///< Return ' ', if there is no global variable with this name. - std::string getCellName (const MWWorld::CellStore *cell = 0) const override; + std::string getCellName (const MWWorld::CellStore *cell = nullptr) const override; ///< Return name of the cell. /// /// \note If cell==0, the cell the player is currently in will be used instead to /// generate a name. + std::string getCellName(const ESM::Cell* cell) const override; void removeRefScript (MWWorld::RefData *ref) override; //< Remove the script attached to ref from mLocalScripts - Ptr getPtr (const std::string& name, bool activeOnly) override; + Ptr getPtr (std::string_view name, bool activeOnly) override; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. - Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = false) override; + Ptr searchPtr (std::string_view name, bool activeOnly, bool searchInContainers = false) override; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do not search inactive cells. @@ -331,6 +333,10 @@ namespace MWWorld int getCurrentWeather() const override; + int getNextWeather() const override; + + float getWeatherTransition() const override; + unsigned int getNightDayMode() const override; int getMasserPhase() const override; @@ -343,6 +349,10 @@ namespace MWWorld float getTimeScaleFactor() const override; + float getSimulationTimeScale() const override { return mSimulationTimeScale; } + + void setSimulationTimeScale(float scale) override; + void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) override; ///< Move to interior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes @@ -374,37 +384,36 @@ namespace MWWorld void undeleteObject (const Ptr& ptr) override; - MWWorld::Ptr moveObject (const Ptr& ptr, float x, float y, float z, bool moveToActive=false) override; + MWWorld::Ptr moveObject (const Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) override; ///< @return an updated Ptr in case the Ptr's cell changes - MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z, bool movePhysics=true) override; + MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr - void scaleObject (const Ptr& ptr, float scale) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) override; + ///< @return an updated Ptr + + void scaleObject (const Ptr& ptr, float scale, bool force = false) override; /// World rotates object, uses radians /// @note Rotations via this method use a different rotation order than the initial rotations in the CS. This /// could be considered a bug, but is needed for MW compatibility. /// \param adjust indicates rotation should be set or adjusted - void rotateObject (const Ptr& ptr, float x, float y, float z, - MWBase::RotationFlags flags = MWBase::RotationFlag_inverseOrder) override; + void rotateObject (const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags = MWBase::RotationFlag_inverseOrder) override; - MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) override; + MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) override; ///< Place an object. Makes a copy of the Ptr. MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) override; ///< Place an object in a safe place next to \a referenceObject. \a direction and \a distance specify the wanted placement /// relative to \a referenceObject (but the object may be placed somewhere else if the wanted location is obstructed). - float getMaxActivationDistance() override; + float getMaxActivationDistance() const override; void indexToPosition (int cellX, int cellY, float &x, float &y, bool centre = false) const override; ///< Convert cell numbers to position. - void positionToIndex (float x, float y, int &cellX, int &cellY) const override; - ///< Convert position to cell numbers - void queueMovement(const Ptr &ptr, const osg::Vec3f &velocity) override; ///< Queues movement for \a ptr (in local space), to be applied in the next call to /// doPhysics. @@ -420,6 +429,9 @@ namespace MWWorld bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) override; + bool castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, + bool ignorePlayer, bool ignoreActors) override; + void setActorCollisionMode(const Ptr& ptr, bool internal, bool external) override; bool isActorCollisionEnabled(const Ptr& ptr) override; @@ -492,10 +504,10 @@ namespace MWWorld ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record - void update (float duration, bool paused) override; - void updatePhysics (float duration, bool paused) override; + void update(float duration, bool paused); + void updatePhysics(float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - void updateWindowManager () override; + void updateWindowManager(); MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) override; ///< copy and place an object into the gameworld at the specified cursor position @@ -533,17 +545,16 @@ namespace MWWorld bool isFirstPerson() const override; bool isPreviewModeEnabled() const override; - void togglePreviewMode(bool enable) override; - bool toggleVanityMode(bool enable) override; - void allowVanityMode(bool allow) override; + MWRender::Camera* getCamera() override; bool vanityRotateCamera(float * rot) override; - void adjustCameraDistance(float dist) override; void applyDeferredPreviewRotationToPlayer(float dt) override; void disableDeferredPreviewRotation() override; + void saveLoaded() override; + void setupPlayer() override; void renderPlayer() override; @@ -585,7 +596,7 @@ namespace MWWorld ///< check if the player is allowed to rest void rest(double hours) override; - void rechargeItems(double duration, bool activeOnly) override; + void rechargeItems(double duration, bool activeOnly); /// \todo Probably shouldn't be here MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) override; @@ -594,7 +605,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; - bool screenshot360 (osg::Image* image, std::string settingStr) override; + bool screenshot360 (osg::Image* image) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise @@ -616,7 +627,7 @@ namespace MWWorld /// Returns true if levitation spell effect is allowed. bool isLevitationEnabled() const override; - bool getGodModeState() override; + bool getGodModeState() const override; bool toggleGodMode() override; @@ -636,11 +647,12 @@ namespace MWWorld */ void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; - void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; + void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) override; void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override; + void updateProjectilesCasters() override; - void applyLoopingParticles(const MWWorld::Ptr& ptr) override; + void applyLoopingParticles(const MWWorld::Ptr& ptr) const override; const std::vector& getContentFiles() const override; void breakInvisibility (const MWWorld::Ptr& actor) override; @@ -680,7 +692,7 @@ namespace MWWorld void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, - const bool fromProjectile=false) override; + const bool fromProjectile=false, int slot = 0) override; void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; @@ -697,7 +709,7 @@ namespace MWWorld /// Return a vector aiming the actor's weapon towards a target. /// @note The length of the vector is the distance between actor and target. - osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) override; + osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) override; /// Return the distance between actor's weapon and target's collision box. float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) override; @@ -723,22 +735,30 @@ namespace MWWorld DetourNavigator::Navigator* getNavigator() const override; void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const override; + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const override; void removeActorPath(const MWWorld::ConstPtr& actor) const override; void setNavMeshNumberToRender(const std::size_t value) override; - /// Return physical half extents of the given actor to be used in pathfinding - osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const override; + DetourNavigator::AgentBounds getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const override; bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const Misc::Span& ignore, std::vector* occupyingActors) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; std::vector getAll(const std::string& id) override; + + Misc::Rng::Generator& getPrng() override; + + MWRender::RenderingManager* getRenderingManager() override { return mRendering.get(); } + + MWRender::PostProcessor* getPostProcessor() override; + + void setActorActive(const MWWorld::Ptr& ptr, bool value) override; }; } diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp new file mode 100644 index 0000000000..f557e42282 --- /dev/null +++ b/apps/openmw/options.cpp @@ -0,0 +1,110 @@ +#include "options.hpp" + +#include +#include +#include + + +namespace +{ + namespace bpo = boost::program_options; + typedef std::vector StringsVector; +} + +namespace OpenMW +{ + bpo::options_description makeOptionsDescription() + { + bpo::options_description desc("Syntax: openmw \nAllowed options"); + Files::ConfigurationManager::addCommonOptions(desc); + + desc.add_options() + ("help", "print help message") + ("version", "print version information and quit") + + ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::MaybeQuotedPath(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), + "set resources directory") + + ("start", bpo::value()->default_value(""), + "set initial cell") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") + + ("groundcover", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") + + ("no-sound", bpo::value()->implicit_value(true) + ->default_value(false), "disable all sounds") + + ("script-all", bpo::value()->implicit_value(true) + ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") + + ("script-all-dialogue", bpo::value()->implicit_value(true) + ->default_value(false), "compile all dialogue scripts at startup") + + ("script-console", bpo::value()->implicit_value(true) + ->default_value(false), "enable console-only script functionality") + + ("script-run", bpo::value()->default_value(""), + "select a file containing a list of console commands that is executed on startup") + + ("script-warn", bpo::value()->implicit_value (1) + ->default_value (1), + "handling of warnings when compiling scripts\n" + "\t0 - ignore warning\n" + "\t1 - show warning but consider script as correctly compiled anyway\n" + "\t2 - treat warnings as errors") + + ("script-blacklist", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") + + ("script-blacklist-use", bpo::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting") + + ("load-savegame", bpo::value()->default_value(Files::MaybeQuotedPath(), ""), + "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") + + ("skip-menu", bpo::value()->implicit_value(true) + ->default_value(false), "skip main menu on game startup") + + ("new-game", bpo::value()->implicit_value(true) + ->default_value(false), "run new game sequence (ignored if skip-menu=0)") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + + ("fallback", bpo::value()->default_value(Fallback::FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + + ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") + + ("export-fonts", bpo::value()->implicit_value(true) + ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") + + ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") + + ("random-seed", bpo::value () + ->default_value(Misc::Rng::generateDefaultSeed()), + "seed value for random number generator") + ; + + return desc; + } +} diff --git a/apps/openmw/options.hpp b/apps/openmw/options.hpp new file mode 100644 index 0000000000..246999eb19 --- /dev/null +++ b/apps/openmw/options.hpp @@ -0,0 +1,11 @@ +#ifndef APPS_OPENMW_OPTIONS_H +#define APPS_OPENMW_OPTIONS_H + +#include + +namespace OpenMW +{ + boost::program_options::options_description makeOptionsDescription(); +} + +#endif diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index cd2d2e80a6..072514d0cc 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -1,20 +1,39 @@ -find_package(GTest REQUIRED) -find_package(GMock REQUIRED) +find_package(GTest 1.10 REQUIRED) +find_package(GMock 1.10 REQUIRED) if (GTEST_FOUND AND GMOCK_FOUND) 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 mwworld/test_store.cpp mwdialogue/test_keywordsearch.cpp + mwscript/test_scripts.cpp + esm/test_fixed_string.cpp + esm/variant.cpp + + lua/test_lua.cpp + lua/test_scriptscontainer.cpp + lua/test_utilpackage.cpp + lua/test_serialization.cpp + lua/test_configuration.cpp + lua/test_l10n.cpp + lua/test_storage.cpp + + lua/test_ui_content.cpp misc/test_stringops.cpp + misc/test_endianness.cpp + misc/test_resourcehelpers.cpp + misc/progressreporter.cpp + misc/compression.cpp nifloader/testbulletnifloader.cpp @@ -25,12 +44,45 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/navmeshdb.cpp + detournavigator/serialization.cpp + detournavigator/asyncnavmeshupdater.cpp + + serialization/binaryreader.cpp + serialization/binarywriter.cpp + serialization/sizeaccumulator.cpp + serialization/integration.cpp settings/parser.cpp + settings/shadermanager.cpp shader/parsedefines.cpp shader/parsefors.cpp + shader/parselinks.cpp shader/shadermanager.cpp + + ../openmw/options.cpp + openmw/options.cpp + + sqlite3/db.cpp + sqlite3/request.cpp + sqlite3/statement.cpp + sqlite3/transaction.cpp + + esmloader/load.cpp + esmloader/esmdata.cpp + esmloader/record.cpp + + files/hash.cpp + + toutf8/toutf8.cpp + + esm4/includes.cpp + + fx/lexer.cpp + fx/technique.cpp + + esm3/readerscache.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) @@ -48,12 +100,31 @@ if (GTEST_FOUND AND GMOCK_FOUND) target_link_libraries(openmw_test_suite gcov) endif() - if (MSVC) - if (CMAKE_CL_64) - # Debug version of openmw_unit_tests needs increased number of sections beyond 2^16 - # just like openmw and openmw-cs - set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") - endif (CMAKE_CL_64) - endif (MSVC) + file(DOWNLOAD + https://gitlab.com/OpenMW/example-suite/-/raw/8966dab24692555eec720c854fb0f73d108070cd/data/template.omwgame + ${CMAKE_CURRENT_BINARY_DIR}/data/template.omwgame + EXPECTED_HASH SHA512=6e38642bcf013c5f496a9cb0bf3ec7c9553b6e86b836e7844824c5a05f556c9391167214469b6318401684b702d7569896bf743c85aee4198612b3315ba778d6 + ) + + target_compile_definitions(openmw_test_suite + PRIVATE OPENMW_DATA_DIR="${CMAKE_CURRENT_BINARY_DIR}/data" + OPENMW_TEST_SUITE_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") + if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(openmw_test_suite PRIVATE + + + + + + + + + + + + + + ) + endif() endif() diff --git a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp new file mode 100644 index 0000000000..692b82ef96 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp @@ -0,0 +1,298 @@ +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + void addHeightFieldPlane(TileCachedRecastMeshManager& recastMeshManager, const osg::Vec2i cellPosition = osg::Vec2i(0, 0)) + { + const int cellSize = 8192; + recastMeshManager.addHeightfield(cellPosition, cellSize, HeightfieldPlane {0}); + } + + void addObject(const btBoxShape& shape, TileCachedRecastMeshManager& recastMeshManager) + { + const ObjectId id(&shape); + osg::ref_ptr bulletShape(new Resource::BulletShape); + bulletShape->mFileName = "test.nif"; + bulletShape->mFileHash = "test_hash"; + ObjectTransform objectTransform; + std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f); + std::fill(std::begin(objectTransform.mPosition.rot), std::end(objectTransform.mPosition.rot), 0.2f); + objectTransform.mScale = 3.14f; + const CollisionShape collisionShape( + osg::ref_ptr(new Resource::BulletShapeInstance(bulletShape)), + shape, objectTransform + ); + recastMeshManager.addObject(id, collisionShape, btTransform::getIdentity(), AreaType_ground, [] (auto) {}); + } + + struct DetourNavigatorAsyncNavMeshUpdaterTest : Test + { + Settings mSettings = makeSettings(); + TileCachedRecastMeshManager mRecastMeshManager {mSettings.mRecast}; + OffMeshConnectionsManager mOffMeshConnectionsManager {mSettings.mRecast}; + const AgentBounds mAgentBounds {CollisionShapeType::Aabb, {29, 29, 66}}; + const TilePosition mPlayerTile {0, 0}; + const std::string mWorldspace = "sys::default"; + const btBoxShape mBox {btVector3(100, 100, 20)}; + Loading::Listener mListener; + }; + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_all_jobs_done_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater {mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr}; + updater.wait(mListener, WaitConditionType::allJobsDone); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_required_tiles_present_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + updater.wait(mListener, WaitConditionType::requiredTilesPresent); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_generate_navmesh_tile) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_post_should_lead_to_cache_hit) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_for_update_change_type_should_not_update_cache) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::update}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_write_generated_tile_to_db) + { + mRecastMeshManager.setWorldspace(mWorldspace); + 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 auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const TilePosition tilePosition {0, 0}; + const std::map changedTiles {{tilePosition, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + 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, mAgentBounds, *recastMesh, objects)); + ASSERT_TRUE(tile.has_value()); + EXPECT_EQ(tile->mTileId, 1); + EXPECT_EQ(tile->mVersion, navMeshFormatVersion); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_tiles) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + mSettings.mWriteToNavMeshDb = false; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const TilePosition tilePosition {0, 0}; + const std::map changedTiles {{tilePosition, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + 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, mAgentBounds, *recastMesh, objects)); + ASSERT_FALSE(tile.has_value()); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_shapes) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + mSettings.mWriteToNavMeshDb = false; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const TilePosition tilePosition {0, 0}; + const std::map changedTiles {{tilePosition, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + updater.stop(); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + const auto objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v); }); + EXPECT_FALSE(objects.has_value()); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_read_from_db_on_cache_miss) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mMaxNavMeshTilesCacheSize = 0; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, + std::make_unique(":memory:", std::numeric_limits::max())); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + ASSERT_EQ(stats.mDb->mGetTileCount, 1); + ASSERT_EQ(stats.mDbGetTileHits, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + EXPECT_EQ(stats.mDb->mGetTileCount, 2); + EXPECT_EQ(stats.mDbGetTileHits, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, on_changing_player_tile_post_should_remove_tiles_out_of_range) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTilesAdd {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTilesAdd); + updater.wait(mListener, WaitConditionType::allJobsDone); + ASSERT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + const std::map changedTilesRemove {{TilePosition {0, 0}, ChangeType::remove}}; + const TilePosition playerTile(100, 100); + updater.post(mAgentBounds, navMeshCacheItem, playerTile, mWorldspace, changedTilesRemove); + updater.wait(mListener, WaitConditionType::allJobsDone); + EXPECT_EQ(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_stop_writing_to_db_when_size_limit_is_reached) + { + mRecastMeshManager.setWorldspace(mWorldspace); + for (int x = -1; x <= 1; ++x) + for (int y = -1; y <= 1; ++y) + addHeightFieldPlane(mRecastMeshManager, osg::Vec2i(x, y)); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", 4097); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + std::map changedTiles; + for (int x = -5; x <= 5; ++x) + for (int y = -5; y <= 5; ++y) + changedTiles.emplace(TilePosition {x, y}, ChangeType::add); + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + 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), + }; + for (int x = -5; x <= 5; ++x) + for (int y = -5; y <= 5; ++y) + { + const TilePosition tilePosition(x, y); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + const std::optional> objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v); }); + if (!objects.has_value()) + continue; + EXPECT_EQ(dbPtr->findTile(mWorldspace, tilePosition, + serialize(mSettings.mRecast, mAgentBounds, *recastMesh, *objects)).has_value(), + present.find(tilePosition) != present.end()) + << tilePosition.x() << " " << tilePosition.y() << " present=" << (present.find(tilePosition) != present.end()); + } + } +} diff --git a/apps/openmw_test_suite/detournavigator/generate.hpp b/apps/openmw_test_suite/detournavigator/generate.hpp new file mode 100644 index 0000000000..52d04495a7 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/generate.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H + +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + template + inline auto generateValue(T& value, Random& random) + -> std::enable_if_t= 2> + { + using Distribution = std::conditional_t< + std::is_floating_point_v, + std::uniform_real_distribution, + std::uniform_int_distribution + >; + Distribution distribution(std::numeric_limits::min(), std::numeric_limits::max()); + value = distribution(random); + } + + template + inline auto generateValue(T& value, Random& random) + -> std::enable_if_t + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateValue(unsigned char& value, Random& random) + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateRange(I begin, I end, Random& random) + { + std::for_each(begin, end, [&] (auto& v) { generateValue(v, random); }); + } + } +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp index 1ad5c063d0..70c6ef9a09 100644 --- a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp +++ b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -21,7 +22,7 @@ namespace struct DetourNavigatorGetTilesPositionsTest : Test { - Settings mSettings; + RecastSettings mSettings; std::vector mTilesPositions; CollectTilesPositions mCollect {mTilesPositions}; @@ -36,35 +37,35 @@ namespace TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_in_single_tile_should_return_one_tile) { - getTilesPositions(osg::Vec3f(2, 2, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect); + 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(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 31, 1), mSettings, mCollect); + 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(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 32, 1), mSettings, mCollect); + 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(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 31, 32), mSettings, mCollect); + 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(osg::Vec3f(-31, -31, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect); + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(-31, -31), osg::Vec2f(31, 31), mSettings), mCollect); EXPECT_THAT(mTilesPositions, ElementsAre( TilePosition(-1, -1), @@ -78,7 +79,7 @@ namespace { mSettings.mBorderSize = 1; - getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31.5, 31.5, 1), mSettings, mCollect); + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31.5, 31.5), mSettings), mCollect); EXPECT_THAT(mTilesPositions, ElementsAre( TilePosition(-1, -1), @@ -97,7 +98,7 @@ namespace { mSettings.mRecastScaleFactor = 0.5; - getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 32, 1), mSettings, mCollect); + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(32, 32), mSettings), mCollect); EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); } diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index ae345d187f..f49f3cdc74 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -1,8 +1,17 @@ #include "operators.hpp" +#include "settings.hpp" #include #include +#include +#include #include +#include +#include +#include +#include + +#include #include #include @@ -11,322 +20,356 @@ #include #include +#include #include +#include +#include MATCHER_P3(Vec3fEq, x, y, z, "") { - return std::abs(arg.x() - x) < 1e-4 && std::abs(arg.y() - y) < 1e-4 && std::abs(arg.z() - z) < 1e-4; + return std::abs(arg.x() - x) < 1e-3 && std::abs(arg.y() - y) < 1e-3 && std::abs(arg.z() - z) < 1e-3; } namespace { using namespace testing; using namespace DetourNavigator; + using namespace DetourNavigator::Tests; struct DetourNavigatorNavigatorTest : Test { - Settings mSettings; + Settings mSettings = makeSettings(); std::unique_ptr mNavigator; - osg::Vec3f mPlayerPosition; - osg::Vec3f mAgentHalfExtents; + const osg::Vec3f mPlayerPosition; + const std::string mWorldspace; + const AgentBounds mAgentBounds {CollisionShapeType::Aabb, {29, 29, 66}}; osg::Vec3f mStart; osg::Vec3f mEnd; std::deque mPath; std::back_insert_iterator> mOut; float mStepSize; 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(0, 0, 0) - , mAgentHalfExtents(29, 29, 66) - , mStart(-215, 215, 1) - , mEnd(215, -215, 1) + : mPlayerPosition(256, 256, 0) + , mWorldspace("sys::default") + , mStart(52, 460, 1) + , mEnd(460, 52, 1) , mOut(mPath) , mStepSize(28.333332061767578125f) { - mSettings.mEnableWriteRecastMeshToFile = false; - mSettings.mEnableWriteNavMeshToFile = false; - mSettings.mEnableRecastMeshFileNameRevision = false; - mSettings.mEnableNavMeshFileNameRevision = false; - mSettings.mBorderSize = 16; - mSettings.mCellHeight = 0.2f; - mSettings.mCellSize = 0.2f; - mSettings.mDetailSampleDist = 6; - mSettings.mDetailSampleMaxError = 1; - mSettings.mMaxClimb = 34; - mSettings.mMaxSimplificationError = 1.3f; - mSettings.mMaxSlope = 49; - mSettings.mRecastScaleFactor = 0.017647058823529415f; - mSettings.mSwimHeightScale = 0.89999997615814208984375f; - mSettings.mMaxEdgeLen = 12; - mSettings.mMaxNavMeshQueryNodes = 2048; - mSettings.mMaxVertsPerPoly = 6; - mSettings.mRegionMergeSize = 20; - mSettings.mRegionMinSize = 8; - mSettings.mTileSize = 64; - mSettings.mAsyncNavMeshUpdaterThreads = 1; - mSettings.mMaxNavMeshTilesCacheSize = 1024 * 1024; - mSettings.mMaxPolygonPathSize = 1024; - mSettings.mMaxSmoothPathSize = 1024; - mSettings.mTrianglesPerChunk = 256; - mSettings.mMaxPolys = 4096; - mSettings.mMaxTilesNumber = 512; - mSettings.mMinUpdateInterval = std::chrono::milliseconds(50); - mNavigator.reset(new NavigatorImpl(mSettings)); + mNavigator.reset(new NavigatorImpl(mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); } }; + template + std::unique_ptr makeSquareHeightfieldTerrainShape(const std::array& values, + btScalar heightScale = 1, int upAxis = 2, PHY_ScalarType heightDataType = PHY_FLOAT, bool flipQuadEdges = false) + { + const int width = static_cast(std::sqrt(size)); + const btScalar min = *std::min_element(values.begin(), values.end()); + const btScalar max = *std::max_element(values.begin(), values.end()); + const btScalar greater = std::max(std::abs(min), std::abs(max)); + return std::make_unique(width, width, values.data(), heightScale, -greater, greater, + upAxis, heightDataType, flipQuadEdges); + } + + template + HeightfieldSurface makeSquareHeightfieldSurface(const std::array& values) + { + const auto [min, max] = std::minmax_element(values.begin(), values.end()); + const float greater = std::max(std::abs(*min), std::abs(*max)); + HeightfieldSurface surface; + surface.mHeights = values.data(); + surface.mMinHeight = -greater; + surface.mMaxHeight = greater; + surface.mSize = static_cast(std::sqrt(size)); + return surface; + } + + template + osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) + { + osg::ref_ptr bulletShape(new Resource::BulletShape); + bulletShape->mCollisionShape.reset(std::move(shape).release()); + return new Resource::BulletShapeInstance(bulletShape); + } + + template + class CollisionShapeInstance + { + public: + CollisionShapeInstance(std::unique_ptr&& shape) : mInstance(makeBulletShapeInstance(std::move(shape))) {} + + T& shape() { return static_cast(*mInstance->mCollisionShape); } + const osg::ref_ptr& instance() const { return mInstance; } + + private: + osg::ref_ptr mInstance; + }; + + btVector3 getHeightfieldShift(const osg::Vec2i& cellPosition, int cellSize, float minHeight, float maxHeight) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.x(), cellSize, minHeight, maxHeight); + } + TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { - mNavigator->addAgent(mAgentHalfExtents); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), + mNavigator->addAgent(mAgentBounds); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } TEST_F(DetourNavigatorNavigatorTest, add_agent_should_count_each_agent) { - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->removeAgent(mAgentHalfExtents); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), + mNavigator->addAgent(mAgentBounds); + mNavigator->addAgent(mAgentBounds); + mNavigator->removeAgent(mAgentBounds); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { - const std::array heightfieldData {{ + constexpr std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.85963428020477294921875), - Vec3fEq(-194.9653167724609375, 194.9653167724609375, -6.57602214813232421875), - Vec3fEq(-174.930633544921875, 174.930633544921875, -15.01167774200439453125), - Vec3fEq(-154.8959503173828125, 154.8959503173828125, -23.4473361968994140625), - Vec3fEq(-134.86126708984375, 134.86126708984375, -31.8829936981201171875), - Vec3fEq(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875), - Vec3fEq(-94.7918853759765625, 94.7918853759765625, -47.3990631103515625), - Vec3fEq(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375), - Vec3fEq(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625), - Vec3fEq(-34.68780517578125, 34.68780517578125, -66.37931060791015625), - Vec3fEq(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875), - Vec3fEq(5.3815765380859375, -5.3815765380859375, -75.35065460205078125), - Vec3fEq(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), - Vec3fEq(45.450958251953125, -45.450958251953125, -60.5882568359375), - Vec3fEq(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - Vec3fEq(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), - Vec3fEq(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), - Vec3fEq(125.5897216796875, -125.5897216796875, -31.063449859619140625), - Vec3fEq(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - Vec3fEq(165.659088134765625, -165.659088134765625, -16.3010501861572265625), - Vec3fEq(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), - Vec3fEq(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), - Vec3fEq(215, -215, 1.87718021869659423828125) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - btBoxShape boxShape(btVector3(20, 20, 100)); - btCompoundShape compoundShape; - compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape); + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.85963428020477294921875), - Vec3fEq(-194.9653167724609375, 194.9653167724609375, -6.57602214813232421875), - Vec3fEq(-174.930633544921875, 174.930633544921875, -15.01167774200439453125), - Vec3fEq(-154.8959503173828125, 154.8959503173828125, -23.4473361968994140625), - Vec3fEq(-134.86126708984375, 134.86126708984375, -31.8829936981201171875), - Vec3fEq(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875), - Vec3fEq(-94.7918853759765625, 94.7918853759765625, -47.3990631103515625), - Vec3fEq(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375), - Vec3fEq(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625), - Vec3fEq(-34.68780517578125, 34.68780517578125, -66.37931060791015625), - Vec3fEq(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875), - Vec3fEq(5.3815765380859375, -5.3815765380859375, -75.35065460205078125), - Vec3fEq(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), - Vec3fEq(45.450958251953125, -45.450958251953125, -60.5882568359375), - Vec3fEq(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - Vec3fEq(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), - Vec3fEq(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), - Vec3fEq(125.5897216796875, -125.5897216796875, -31.063449859619140625), - Vec3fEq(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - Vec3fEq(165.659088134765625, -165.659088134765625, -16.3010501861572265625), - Vec3fEq(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), - Vec3fEq(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), - Vec3fEq(215, -215, 1.87718021869659423828125) - )); - - mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; + + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.87826788425445556640625), - Vec3fEq(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375), - Vec3fEq(-184.5936431884765625, 167.1819915771484375, -8.97847843170166015625), - Vec3fEq(-169.3904571533203125, 143.2729949951171875, -14.408184051513671875), - Vec3fEq(-154.1872711181640625, 119.36397552490234375, -19.837890625), - Vec3fEq(-138.9840850830078125, 95.45496368408203125, -25.2675991058349609375), - Vec3fEq(-123.78090667724609375, 71.54595184326171875, -30.6973056793212890625), - Vec3fEq(-108.57772064208984375, 47.636936187744140625, -36.12701416015625), - Vec3fEq(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125), - Vec3fEq(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625), - Vec3fEq(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375), - Vec3fEq(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625), - Vec3fEq(-23.852447509765625, -63.196765899658203125, -33.97112274169921875), - Vec3fEq(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875), - Vec3fEq(23.97260284423828125, -93.5920867919921875, -40.774089813232421875), - Vec3fEq(47.885128021240234375, -108.78974151611328125, -36.05129241943359375), - Vec3fEq(71.7976531982421875, -123.98740386962890625, -30.6235561370849609375), - Vec3fEq(95.71018218994140625, -139.18505859375, -25.1958255767822265625), - Vec3fEq(119.6226959228515625, -154.382720947265625, -19.7680912017822265625), - Vec3fEq(143.53521728515625, -169.58038330078125, -14.34035205841064453125), - Vec3fEq(167.4477386474609375, -184.778045654296875, -8.9126186370849609375), - Vec3fEq(191.360260009765625, -199.9757080078125, -3.4848802089691162109375), - Vec3fEq(215, -215, 1.87826788425445556640625) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125), + Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875), + Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625), + Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875), + Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - btBoxShape boxShape(btVector3(20, 20, 100)); - btCompoundShape compoundShape; - compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape); + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.87826788425445556640625), - Vec3fEq(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375), - Vec3fEq(-184.5936431884765625, 167.1819915771484375, -8.97847843170166015625), - Vec3fEq(-169.3904571533203125, 143.2729949951171875, -14.408184051513671875), - Vec3fEq(-154.1872711181640625, 119.36397552490234375, -19.837890625), - Vec3fEq(-138.9840850830078125, 95.45496368408203125, -25.2675991058349609375), - Vec3fEq(-123.78090667724609375, 71.54595184326171875, -30.6973056793212890625), - Vec3fEq(-108.57772064208984375, 47.636936187744140625, -36.12701416015625), - Vec3fEq(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125), - Vec3fEq(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625), - Vec3fEq(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375), - Vec3fEq(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625), - Vec3fEq(-23.852447509765625, -63.196765899658203125, -33.97112274169921875), - Vec3fEq(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875), - Vec3fEq(23.97260284423828125, -93.5920867919921875, -40.774089813232421875), - Vec3fEq(47.885128021240234375, -108.78974151611328125, -36.05129241943359375), - Vec3fEq(71.7976531982421875, -123.98740386962890625, -30.6235561370849609375), - Vec3fEq(95.71018218994140625, -139.18505859375, -25.1958255767822265625), - Vec3fEq(119.6226959228515625, -154.382720947265625, -19.7680912017822265625), - Vec3fEq(143.53521728515625, -169.58038330078125, -14.34035205841064453125), - Vec3fEq(167.4477386474609375, -184.778045654296875, -8.9126186370849609375), - Vec3fEq(191.360260009765625, -199.9757080078125, -3.4848802089691162109375), - Vec3fEq(215, -215, 1.87826788425445556640625) - )); - - compoundShape.updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); - - mNavigator->updateObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125), + Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875), + Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625), + Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875), + Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; + + compound.shape().updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); + + mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.85963428020477294921875), - Vec3fEq(-194.9653167724609375, 194.9653167724609375, -6.57602214813232421875), - Vec3fEq(-174.930633544921875, 174.930633544921875, -15.01167774200439453125), - Vec3fEq(-154.8959503173828125, 154.8959503173828125, -23.4473361968994140625), - Vec3fEq(-134.86126708984375, 134.86126708984375, -31.8829936981201171875), - Vec3fEq(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875), - Vec3fEq(-94.7918853759765625, 94.7918853759765625, -47.3990631103515625), - Vec3fEq(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375), - Vec3fEq(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625), - Vec3fEq(-34.68780517578125, 34.68780517578125, -66.37931060791015625), - Vec3fEq(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875), - Vec3fEq(5.3815765380859375, -5.3815765380859375, -75.35065460205078125), - Vec3fEq(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), - Vec3fEq(45.450958251953125, -45.450958251953125, -60.5882568359375), - Vec3fEq(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - Vec3fEq(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), - Vec3fEq(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), - Vec3fEq(125.5897216796875, -125.5897216796875, -31.063449859619140625), - Vec3fEq(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - Vec3fEq(165.659088134765625, -165.659088134765625, -16.3010501861572265625), - Vec3fEq(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), - Vec3fEq(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), - Vec3fEq(215, -215, 1.87718021869659423828125) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; } - TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_should_use_higher) + TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) { - const std::array heightfieldData {{ + const std::array heightfieldData1 {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1)); + heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); const std::array heightfieldData2 {{ -25, -25, -25, -25, -25, @@ -335,46 +378,75 @@ namespace -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, }}; - btHeightfieldTerrainShape shape2(5, 5, heightfieldData2.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape2.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield2(makeSquareHeightfieldTerrainShape(heightfieldData2)); + heightfield2.shape().setLocalScaling(btVector3(128, 128, 1)); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape2), shape2, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance(), mObjectTransform), mTransform); + mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.96328866481781005859375), - Vec3fEq(-194.9653167724609375, 194.9653167724609375, -0.242215454578399658203125), - Vec3fEq(-174.930633544921875, 174.930633544921875, -2.447719097137451171875), - Vec3fEq(-154.8959503173828125, 154.8959503173828125, -4.65322399139404296875), - Vec3fEq(-134.86126708984375, 134.86126708984375, -6.858726978302001953125), - Vec3fEq(-114.82657623291015625, 114.82657623291015625, -9.06423282623291015625), - Vec3fEq(-94.7918853759765625, 94.7918853759765625, -11.26973628997802734375), - Vec3fEq(-74.75719451904296875, 74.75719451904296875, -13.26497173309326171875), - Vec3fEq(-54.722499847412109375, 54.722499847412109375, -15.24860477447509765625), - Vec3fEq(-34.68780517578125, 34.68780517578125, -17.23223876953125), - Vec3fEq(-14.6531162261962890625, 14.6531162261962890625, -19.215869903564453125), - Vec3fEq(5.3815765380859375, -5.3815765380859375, -20.1338443756103515625), - Vec3fEq(25.41626739501953125, -25.41626739501953125, -18.1502132415771484375), - Vec3fEq(45.450958251953125, -45.450958251953125, -16.1665802001953125), - Vec3fEq(65.48564910888671875, -65.48564910888671875, -14.18294620513916015625), - Vec3fEq(85.5203399658203125, -85.5203399658203125, -12.199314117431640625), - Vec3fEq(105.55503082275390625, -105.55503082275390625, -10.08488368988037109375), - Vec3fEq(125.5897216796875, -125.5897216796875, -7.87938022613525390625), - Vec3fEq(145.6244049072265625, -145.6244049072265625, -5.673875331878662109375), - Vec3fEq(165.659088134765625, -165.659088134765625, -3.468370914459228515625), - Vec3fEq(185.6937713623046875, -185.6937713623046875, -1.26286637783050537109375), - Vec3fEq(205.7284698486328125, -205.7284698486328125, 0.942641556262969970703125), - Vec3fEq(215, -215, 1.96328866481781005859375) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.903246104717254638671875), + Vec3fEq(96.73604583740234375, 419.93060302734375, -3.8064472675323486328125), + Vec3fEq(116.770751953125, 399.89593505859375, -6.709649562835693359375), + Vec3fEq(136.8054351806640625, 379.861236572265625, -9.33333873748779296875), + Vec3fEq(156.840118408203125, 359.826568603515625, -9.33333873748779296875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -9.33333873748779296875), + Vec3fEq(196.90948486328125, 319.757171630859375, -9.33333873748779296875), + Vec3fEq(216.944183349609375, 299.722503662109375, -9.33333873748779296875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -9.33333873748779296875), + Vec3fEq(257.0135498046875, 259.65313720703125, -9.33333873748779296875), + Vec3fEq(277.048248291015625, 239.618438720703125, -9.33333873748779296875), + Vec3fEq(297.082916259765625, 219.583740234375, -9.33333873748779296875), + Vec3fEq(317.11761474609375, 199.549041748046875, -9.33333873748779296875), + Vec3fEq(337.15228271484375, 179.5143585205078125, -9.33333873748779296875), + Vec3fEq(357.186981201171875, 159.47967529296875, -9.33333873748779296875), + Vec3fEq(377.221649169921875, 139.4449920654296875, -9.33333873748779296875), + Vec3fEq(397.25634765625, 119.41030120849609375, -6.891522884368896484375), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.053897380828857421875), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.21627247333526611328125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.621352672576904296875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) + { + const std::array heightfieldData1 {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); + const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); + + const std::array heightfieldData2 {{ + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + }}; + const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); + const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1); + + mNavigator->addAgent(mAgentBounds); + EXPECT_TRUE(mNavigator->addHeightfield(mCellPosition, cellSize1, surface1)); + EXPECT_FALSE(mNavigator->addHeightfield(mCellPosition, cellSize2, surface2)); } TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape) { + osg::ref_ptr bulletShape(new Resource::BulletShape); + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, @@ -382,8 +454,9 @@ namespace 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); + shapePtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mCollisionShape.reset(shapePtr.release()); std::array heightfieldDataAvoid {{ -25, -25, -25, -25, -25, @@ -392,93 +465,96 @@ namespace -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, }}; - btHeightfieldTerrainShape shapeAvoid(5, 5, heightfieldDataAvoid.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shapeAvoid.setLocalScaling(btVector3(128, 128, 1)); + std::unique_ptr shapeAvoidPtr = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); + shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mAvoidCollisionShape.reset(shapeAvoidPtr.release()); + + osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), ObjectShapes {shape, &shapeAvoid}, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance, mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.9393787384033203125), - Vec3fEq(-200.8159637451171875, 190.47265625, -0.639537751674652099609375), - Vec3fEq(-186.6319427490234375, 165.9453125, -3.2184507846832275390625), - Vec3fEq(-172.447906494140625, 141.41796875, -5.797363758087158203125), - Vec3fEq(-158.263885498046875, 116.8906097412109375, -8.37627696990966796875), - Vec3fEq(-144.079864501953125, 92.3632659912109375, -10.9551906585693359375), - Vec3fEq(-129.89581298828125, 67.83591461181640625, -13.53410625457763671875), - Vec3fEq(-115.7117919921875, 43.308563232421875, -16.1130199432373046875), - Vec3fEq(-101.5277557373046875, 18.7812137603759765625, -18.6919345855712890625), - Vec3fEq(-87.34372711181640625, -5.7461376190185546875, -20.4680538177490234375), - Vec3fEq(-67.02922821044921875, -25.4970550537109375, -20.514247894287109375), - Vec3fEq(-46.714717864990234375, -45.2479705810546875, -20.560443878173828125), - Vec3fEq(-26.40021514892578125, -64.99889373779296875, -20.6066417694091796875), - Vec3fEq(-6.085712432861328125, -84.74980926513671875, -20.652835845947265625), - Vec3fEq(14.22879505157470703125, -104.50072479248046875, -18.151397705078125), - Vec3fEq(39.05098724365234375, -118.16222381591796875, -15.66748714447021484375), - Vec3fEq(63.87317657470703125, -131.82373046875, -13.18358135223388671875), - Vec3fEq(88.69537353515625, -145.4852142333984375, -10.699672698974609375), - Vec3fEq(113.51757049560546875, -159.146697998046875, -8.21576786041259765625), - Vec3fEq(138.3397674560546875, -172.808197021484375, -5.731859683990478515625), - Vec3fEq(163.1619720458984375, -186.469696044921875, -3.2479507923126220703125), - Vec3fEq(187.984161376953125, -200.1311798095703125, -0.764044821262359619140625), - Vec3fEq(212.8063507080078125, -213.7926788330078125, 1.719865322113037109375), - Vec3fEq(215, -215, 1.9393787384033203125) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.013885498046875, 434.49853515625, -0.74384129047393798828125), + Vec3fEq(81.36110687255859375, 408.997100830078125, -3.4876689910888671875), + Vec3fEq(93.7083282470703125, 383.495635986328125, -6.2314929962158203125), + Vec3fEq(106.0555419921875, 357.99420166015625, -8.97531890869140625), + Vec3fEq(118.40276336669921875, 332.49273681640625, -11.7191448211669921875), + Vec3fEq(130.7499847412109375, 306.991302490234375, -14.4629726409912109375), + Vec3fEq(143.0972137451171875, 281.4898681640625, -17.206798553466796875), + Vec3fEq(155.4444122314453125, 255.9884033203125, -19.9506206512451171875), + Vec3fEq(167.7916412353515625, 230.4869537353515625, -19.91887664794921875), + Vec3fEq(189.053619384765625, 211.75982666015625, -20.1138629913330078125), + Vec3fEq(210.3155975341796875, 193.032684326171875, -20.3088512420654296875), + Vec3fEq(231.577606201171875, 174.3055419921875, -20.503841400146484375), + Vec3fEq(252.839599609375, 155.5784149169921875, -19.9803981781005859375), + Vec3fEq(278.407989501953125, 143.3704071044921875, -17.2675113677978515625), + Vec3fEq(303.976348876953125, 131.16241455078125, -14.55462360382080078125), + Vec3fEq(329.54473876953125, 118.9544219970703125, -11.84173583984375), + Vec3fEq(355.11309814453125, 106.74642181396484375, -9.12884807586669921875), + Vec3fEq(380.681488037109375, 94.538421630859375, -6.4159603118896484375), + Vec3fEq(406.249847412109375, 82.33042144775390625, -3.7030735015869140625), + Vec3fEq(431.8182373046875, 70.1224365234375, -0.990187108516693115234375), + Vec3fEq(457.38665771484375, 57.9144439697265625, 1.72269880771636962890625), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_ground_lower_than_water_with_only_swim_flag) { - std::array heightfieldData {{ + std::array heightfieldData {{ -50, -50, -50, -50, 0, -50, -100, -150, -100, -50, -50, -150, -200, -150, -100, -50, -100, -150, -100, -100, 0, -50, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, 300, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addWater(mCellPosition, cellSize, 300); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; + mStart.x() = 256; mStart.z() = 300; - mEnd.x() = 0; + mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mOut), Status::Success); - - EXPECT_EQ(mPath, std::deque({ - osg::Vec3f(0, 215, 185.33331298828125), - osg::Vec3f(0, 186.6666717529296875, 185.33331298828125), - osg::Vec3f(0, 158.333343505859375, 185.33331298828125), - osg::Vec3f(0, 130.0000152587890625, 185.33331298828125), - osg::Vec3f(0, 101.66667938232421875, 185.33331298828125), - osg::Vec3f(0, 73.333343505859375, 185.33331298828125), - osg::Vec3f(0, 45.0000152587890625, 185.33331298828125), - osg::Vec3f(0, 16.6666812896728515625, 185.33331298828125), - osg::Vec3f(0, -11.66664981842041015625, 185.33331298828125), - osg::Vec3f(0, -39.999980926513671875, 185.33331298828125), - osg::Vec3f(0, -68.33331298828125, 185.33331298828125), - osg::Vec3f(0, -96.66664886474609375, 185.33331298828125), - osg::Vec3f(0, -124.99997711181640625, 185.33331298828125), - osg::Vec3f(0, -153.33331298828125, 185.33331298828125), - osg::Vec3f(0, -181.6666412353515625, 185.33331298828125), - osg::Vec3f(0, -209.999969482421875, 185.33331298828125), - osg::Vec3f(0, -215, 185.33331298828125), - })) << mPath; + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(256, 460, 185.33331298828125), + Vec3fEq(256, 431.666656494140625, 185.33331298828125), + Vec3fEq(256, 403.33331298828125, 185.33331298828125), + Vec3fEq(256, 375, 185.33331298828125), + Vec3fEq(256, 346.666656494140625, 185.33331298828125), + Vec3fEq(256, 318.33331298828125, 185.33331298828125), + Vec3fEq(256, 290, 185.33331298828125), + Vec3fEq(256, 261.666656494140625, 185.33331298828125), + Vec3fEq(256, 233.3333282470703125, 185.33331298828125), + Vec3fEq(256, 205, 185.33331298828125), + Vec3fEq(256, 176.6666717529296875, 185.33331298828125), + Vec3fEq(256, 148.3333282470703125, 185.33331298828125), + Vec3fEq(256, 120, 185.33331298828125), + Vec3fEq(256, 91.6666717529296875, 185.33331298828125), + Vec3fEq(255.999969482421875, 63.33333587646484375, 185.33331298828125), + Vec3fEq(255.999969482421875, 56.66666412353515625, 185.33331298828125) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_swim_and_walk_flags) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -487,45 +563,44 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addWater(mCellPosition, cellSize, -25); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); - EXPECT_EQ(mPath, std::deque({ - osg::Vec3f(0, 215, -94.75363922119140625), - osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625), - osg::Vec3f(0, 158.333343505859375, -115.85507965087890625), - osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375), - osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875), - osg::Vec3f(0, 73.333343505859375, -143.3333587646484375), - osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375), - osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375), - osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375), - osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375), - osg::Vec3f(0, -68.33331298828125, -143.3333587646484375), - osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875), - osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375), - osg::Vec3f(0, -153.33331298828125, -117.59423065185546875), - osg::Vec3f(0, -181.6666412353515625, -107.73915863037109375), - osg::Vec3f(0, -209.999969482421875, -97.7971343994140625), - osg::Vec3f(0, -215, -94.75363922119140625), - })) << mPath; + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_max_int_cells_size_and_swim_and_walk_flags) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -534,45 +609,44 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); - mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits::max(), -25, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addWater(mCellPosition, std::numeric_limits::max(), -25); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); - EXPECT_EQ(mPath, std::deque({ - osg::Vec3f(0, 215, -94.75363922119140625), - osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625), - osg::Vec3f(0, 158.333343505859375, -115.85507965087890625), - osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375), - osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875), - osg::Vec3f(0, 73.333343505859375, -143.3333587646484375), - osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375), - osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375), - osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375), - osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375), - osg::Vec3f(0, -68.33331298828125, -143.3333587646484375), - osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875), - osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375), - osg::Vec3f(0, -153.33331298828125, -117.59423065185546875), - osg::Vec3f(0, -181.6666412353515625, -107.73915863037109375), - osg::Vec3f(0, -209.999969482421875, -97.7971343994140625), - osg::Vec3f(0, -215, -94.75363922119140625), - })) << mPath; + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) + )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_ground_when_ground_cross_water_with_only_walk_flag) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -581,43 +655,42 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addWater(mCellPosition, cellSize, -25); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(0, 215, -94.75363922119140625), - Vec3fEq(9.8083515167236328125, 188.4185333251953125, -105.199951171875), - Vec3fEq(19.6167049407958984375, 161.837066650390625, -114.25495147705078125), - Vec3fEq(29.42505645751953125, 135.255615234375, -123.309967041015625), - Vec3fEq(39.23340606689453125, 108.674163818359375, -132.3649749755859375), - Vec3fEq(49.04175567626953125, 82.09270477294921875, -137.2874755859375), - Vec3fEq(58.8501129150390625, 55.5112457275390625, -139.2451171875), - Vec3fEq(68.6584625244140625, 28.9297885894775390625, -141.2027740478515625), - Vec3fEq(78.4668121337890625, 2.3483295440673828125, -143.1604156494140625), - Vec3fEq(88.27516937255859375, -24.233127593994140625, -141.3894805908203125), - Vec3fEq(83.73651885986328125, -52.2005767822265625, -142.3761444091796875), - Vec3fEq(79.19786834716796875, -80.16802978515625, -143.114837646484375), - Vec3fEq(64.8477935791015625, -104.598602294921875, -137.840911865234375), - Vec3fEq(50.497714996337890625, -129.0291748046875, -131.45831298828125), - Vec3fEq(36.147632598876953125, -153.459747314453125, -121.42321014404296875), - Vec3fEq(21.7975559234619140625, -177.8903350830078125, -111.38811492919921875), - Vec3fEq(7.44747829437255859375, -202.3209075927734375, -101.19382476806640625), - Vec3fEq(0, -215, -94.75363922119140625) - )); + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) + )) << mPath; } - TEST_F(DetourNavigatorNavigatorTest, update_remove_and_update_then_find_path_should_return_path) + TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) { const std::array heightfieldData {{ 0, 0, 0, 0, 0, @@ -626,180 +699,450 @@ namespace 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData)); + heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->removeObject(ObjectId(&shape)); + mNavigator->removeObject(ObjectId(&heightfield.shape())); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), mTransform); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.85963428020477294921875), - Vec3fEq(-194.9653167724609375, 194.9653167724609375, -6.57602214813232421875), - Vec3fEq(-174.930633544921875, 174.930633544921875, -15.01167774200439453125), - Vec3fEq(-154.8959503173828125, 154.8959503173828125, -23.4473361968994140625), - Vec3fEq(-134.86126708984375, 134.86126708984375, -31.8829936981201171875), - Vec3fEq(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875), - Vec3fEq(-94.7918853759765625, 94.7918853759765625, -47.3990631103515625), - Vec3fEq(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375), - Vec3fEq(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625), - Vec3fEq(-34.68780517578125, 34.68780517578125, -66.37931060791015625), - Vec3fEq(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875), - Vec3fEq(5.3815765380859375, -5.3815765380859375, -75.35065460205078125), - Vec3fEq(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), - Vec3fEq(45.450958251953125, -45.450958251953125, -60.5882568359375), - Vec3fEq(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - Vec3fEq(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), - Vec3fEq(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), - Vec3fEq(125.5897216796875, -125.5897216796875, -31.063449859619140625), - Vec3fEq(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - Vec3fEq(165.659088134765625, -165.659088134765625, -16.3010501861572265625), - Vec3fEq(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), - Vec3fEq(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), - Vec3fEq(215, -215, 1.87718021869659423828125) - )); + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; } - TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position) + TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); - mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + mNavigator->removeHeightfield(mCellPosition); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, -25, + 0, -25, -1000, -1000, -100, -100, + 0, -25, -1000, -1000, -100, -100, + 0, -25, -100, -100, -100, -100, + 0, -25, -100, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); Misc::Rng::init(42); - const auto result = mNavigator->findRandomPointAroundCircle(mAgentHalfExtents, mStart, 100.0, Flag_walk); + const auto result = findRandomPointAroundCircle(*mNavigator, mAgentBounds, mStart, 100.0, Flag_walk, + []() { return Misc::Rng::rollClosedProbability(); }); - ASSERT_THAT(result, Optional(Vec3fEq(-209.95985412597656, 129.89768981933594, -0.26253718137741089))); + ASSERT_THAT(result, Optional(Vec3fEq(70.35845947265625, 335.592041015625, -2.6667339801788330078125))) + << (result ? *result : osg::Vec3f()); const auto distance = (*result - mStart).length(); - EXPECT_FLOAT_EQ(distance, 85.260780334472656); + EXPECT_FLOAT_EQ(distance, 125.80865478515625) << distance; } TEST_F(DetourNavigatorNavigatorTest, multiple_threads_should_lock_tiles) { mSettings.mAsyncNavMeshUpdaterThreads = 2; - mNavigator.reset(new NavigatorImpl(mSettings)); + mNavigator.reset(new NavigatorImpl(mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); - const std::vector boxShapes(100, btVector3(20, 20, 100)); + std::vector> boxes; + std::generate_n(std::back_inserter(boxes), 100, [] { return std::make_unique(btVector3(20, 20, 100)); }); - mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addAgent(mAgentBounds); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); - for (std::size_t i = 0; i < boxShapes.size(); ++i) + for (std::size_t i = 0; i < boxes.size(); ++i) { - const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10, i * 10, i * 10)); - mNavigator->addObject(ObjectId(&boxShapes[i]), boxShapes[i], transform); + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10, shift.y() + i * 10, i * 10)); + mNavigator->addObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform); } std::this_thread::sleep_for(std::chrono::microseconds(1)); - for (std::size_t i = 0; i < boxShapes.size(); ++i) + for (std::size_t i = 0; i < boxes.size(); ++i) { - const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10 + 1, i * 10 + 1, i * 10 + 1)); - mNavigator->updateObject(ObjectId(&boxShapes[i]), boxShapes[i], transform); + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10 + 1, shift.y() + i * 10 + 1, i * 10 + 1)); + mNavigator->updateObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform); } mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-215, 215, 1.8782780170440673828125), - Vec3fEq(-199.7968292236328125, 191.09100341796875, -3.54875946044921875), - Vec3fEq(-184.5936431884765625, 167.1819915771484375, -8.97846889495849609375), - Vec3fEq(-169.3904571533203125, 143.2729949951171875, -14.40818119049072265625), - Vec3fEq(-154.1872711181640625, 119.363983154296875, -19.837886810302734375), - Vec3fEq(-138.9840850830078125, 95.4549713134765625, -25.2675952911376953125), - Vec3fEq(-123.78090667724609375, 71.54595947265625, -30.6973056793212890625), - Vec3fEq(-108.57772064208984375, 47.63695526123046875, -36.12701416015625), - Vec3fEq(-93.3745269775390625, 23.72794342041015625, -40.754695892333984375), - Vec3fEq(-78.17134857177734375, -0.18106450140476226806640625, -37.128795623779296875), - Vec3fEq(-62.968158721923828125, -24.0900726318359375, -33.50289154052734375), - Vec3fEq(-47.764972686767578125, -47.99908447265625, -30.797946929931640625), - Vec3fEq(-23.8524494171142578125, -63.196746826171875, -33.97112274169921875), - Vec3fEq(0.0600722394883632659912109375, -78.3944091796875, -37.14543914794921875), - Vec3fEq(23.97259521484375, -93.592071533203125, -40.774089813232421875), - Vec3fEq(47.885120391845703125, -108.78974151611328125, -36.051296234130859375), - Vec3fEq(71.797637939453125, -123.98740386962890625, -30.62355804443359375), - Vec3fEq(95.71016693115234375, -139.18505859375, -25.195819854736328125), - Vec3fEq(119.6226806640625, -154.382720947265625, -19.768085479736328125), - Vec3fEq(143.5352020263671875, -169.5803680419921875, -14.34035015106201171875), - Vec3fEq(167.447723388671875, -184.7780303955078125, -8.912616729736328125), - Vec3fEq(191.3602294921875, -199.9756927490234375, -3.48488140106201171875), - Vec3fEq(215, -215, 1.8782813549041748046875) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -20.3612518310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.578319549560546875), + Vec3fEq(227.9903564453125, 170.28521728515625, -26.291717529296875), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -34.784488677978515625), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -25.72176361083984375), + Vec3fEq(329.774017333984375, 120.44022369384765625, -21.1904010772705078125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -16.6590404510498046875), + Vec3fEq(380.665863037109375, 95.51773834228515625, -12.127681732177734375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -7.5963191986083984375), + Vec3fEq(431.557708740234375, 70.5952606201171875, -3.0649592876434326171875), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.4664003849029541015625), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, update_changed_multiple_times_object_should_delay_navmesh_change) { - const std::vector shapes(100, btVector3(64, 64, 64)); + std::vector> shapes; + std::generate_n(std::back_inserter(shapes), 100, [] { return std::make_unique(btVector3(64, 64, 64)); }); - mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addAgent(mAgentBounds); for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32)); - mNavigator->addObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->addObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform); } mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); const auto start = std::chrono::steady_clock::now(); for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1)); - mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform); } mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2)); - mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform); } mNavigator->update(mPlayerPosition); - mNavigator->wait(); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); const auto duration = std::chrono::steady_clock::now() - start; EXPECT_GT(duration, mSettings.mMinUpdateInterval) << std::chrono::duration_cast>(duration).count() << " ms"; } + + TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + const osg::Vec3f start(57, 460, 1); + const osg::Vec3f end(460, 57, 1); + const auto result = raycast(*mNavigator, mAgentBounds, start, end, Flag_walk); + + ASSERT_THAT(result, Optional(Vec3fEq(end.x(), end.y(), 1.95257937908172607421875))) + << (result ? *result : osg::Vec3f()); + } + + 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, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); + const btVector3 oscillatingBoxShapePosition(288, 288, 400); + CollisionShapeInstance borderBox(std::make_unique(btVector3(50, 50, 50))); + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance(), mObjectTransform), + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition)); + // add this box to make navmesh bound box independent from oscillatingBoxShape rotations + mNavigator->addObject(ObjectId(&borderBox.shape()), ObjectShapes(borderBox.instance(), mObjectTransform), + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200))); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + const Version expectedVersion {1, 4}; + + const auto navMeshes = mNavigator->getNavMeshes(); + ASSERT_EQ(navMeshes.size(), 1); + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); + + for (int n = 0; n < 10; ++n) + { + const btTransform transform(btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10), + oscillatingBoxShapePosition); + mNavigator->updateObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance(), mObjectTransform), transform); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + } + + ASSERT_EQ(navMeshes.size(), 1); + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); + } + + TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) + { + const HeightfieldPlane plane {100}; + const int cellSize = mHeightfieldTileSize * 4; + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, plane); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(56.66666412353515625, 460, 101.99999237060546875), + Vec3fEq(76.70135498046875, 439.965301513671875, 101.99999237060546875), + Vec3fEq(96.73604583740234375, 419.93060302734375, 101.99999237060546875), + Vec3fEq(116.770751953125, 399.89593505859375, 101.99999237060546875), + Vec3fEq(136.8054351806640625, 379.861236572265625, 101.99999237060546875), + Vec3fEq(156.840118408203125, 359.826568603515625, 101.99999237060546875), + Vec3fEq(176.8748016357421875, 339.7918701171875, 101.99999237060546875), + Vec3fEq(196.90948486328125, 319.757171630859375, 101.99999237060546875), + Vec3fEq(216.944183349609375, 299.722503662109375, 101.99999237060546875), + Vec3fEq(236.9788665771484375, 279.68780517578125, 101.99999237060546875), + Vec3fEq(257.0135498046875, 259.65313720703125, 101.99999237060546875), + Vec3fEq(277.048248291015625, 239.618438720703125, 101.99999237060546875), + Vec3fEq(297.082916259765625, 219.583740234375, 101.99999237060546875), + Vec3fEq(317.11761474609375, 199.549041748046875, 101.99999237060546875), + Vec3fEq(337.15228271484375, 179.5143585205078125, 101.99999237060546875), + Vec3fEq(357.186981201171875, 159.47967529296875, 101.99999237060546875), + Vec3fEq(377.221649169921875, 139.4449920654296875, 101.99999237060546875), + Vec3fEq(397.25634765625, 119.41030120849609375, 101.99999237060546875), + Vec3fEq(417.291046142578125, 99.3756103515625, 101.99999237060546875), + Vec3fEq(437.325714111328125, 79.340911865234375, 101.99999237060546875), + Vec3fEq(457.360443115234375, 59.3062286376953125, 101.99999237060546875), + Vec3fEq(460, 56.66666412353515625, 101.99999237060546875) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), + new btBoxShape(btVector3(200, 200, 1000))); + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::PartialPath); + + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(56.66664886474609375, 460, -2.5371043682098388671875), + Vec3fEq(76.42063140869140625, 439.6884765625, -2.9134314060211181640625), + Vec3fEq(96.17461395263671875, 419.376953125, -4.50826549530029296875), + Vec3fEq(115.9285888671875, 399.0654296875, -6.1030979156494140625), + Vec3fEq(135.6825714111328125, 378.753936767578125, -7.697928905487060546875), + Vec3fEq(155.436553955078125, 358.442413330078125, -20.9574985504150390625), + Vec3fEq(175.190521240234375, 338.130889892578125, -35.907512664794921875), + Vec3fEq(194.9445037841796875, 317.8193359375, -50.85752105712890625), + Vec3fEq(214.698486328125, 297.5078125, -65.807525634765625), + Vec3fEq(222.0001068115234375, 290.000091552734375, -71.333465576171875) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), + new btBoxShape(btVector3(100, 100, 1000))); + + mNavigator->addAgent(mAgentBounds); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + const float endTolerance = 1000.0f; + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre( + Vec3fEq(56.66666412353515625, 460, -2.5371043682098388671875), + Vec3fEq(71.5649566650390625, 435.899810791015625, -5.817593097686767578125), + Vec3fEq(86.46324920654296875, 411.79962158203125, -9.66499996185302734375), + Vec3fEq(101.36154937744140625, 387.699462890625, -13.512401580810546875), + Vec3fEq(116.2598419189453125, 363.599273681640625, -17.359806060791015625), + Vec3fEq(131.1581268310546875, 339.499114990234375, -21.2072086334228515625), + Vec3fEq(146.056427001953125, 315.39892578125, -25.0546112060546875), + Vec3fEq(160.9547271728515625, 291.298736572265625, -28.9020137786865234375), + Vec3fEq(175.8530120849609375, 267.198577880859375, -32.749416351318359375), + Vec3fEq(190.751312255859375, 243.098388671875, -33.819454193115234375), + Vec3fEq(205.64959716796875, 218.9982147216796875, -31.020172119140625), + Vec3fEq(220.5478973388671875, 194.898040771484375, -26.844608306884765625), + Vec3fEq(235.446197509765625, 170.7978668212890625, -26.785541534423828125), + Vec3fEq(250.3444671630859375, 146.6976776123046875, -26.7264766693115234375), + Vec3fEq(265.242767333984375, 122.59751129150390625, -20.59339141845703125), + Vec3fEq(280.141021728515625, 98.4973297119140625, -14.040531158447265625), + Vec3fEq(295.039306640625, 74.39715576171875, -7.48766994476318359375), + Vec3fEq(306, 56.66666412353515625, -2.6667339801788330078125) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, only_one_water_per_cell_is_allowed) + { + const int cellSize1 = 100; + const float level1 = 1; + const int cellSize2 = 200; + const float level2 = 2; + + mNavigator->addAgent(mAgentBounds); + EXPECT_TRUE(mNavigator->addWater(mCellPosition, cellSize1, level1)); + EXPECT_FALSE(mNavigator->addWater(mCellPosition, cellSize2, level2)); + } } diff --git a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp new file mode 100644 index 0000000000..55d26e3b78 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp @@ -0,0 +1,181 @@ +#include "generate.hpp" + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + struct Tile + { + std::string mWorldspace; + TilePosition mTilePosition; + std::vector mInput; + std::vector mData; + }; + + struct DetourNavigatorNavMeshDbTest : Test + { + NavMeshDb mDb {":memory:", std::numeric_limits::max()}; + std::minstd_rand mRandom; + + std::vector generateData() + { + std::vector data(32); + generateRange(data.begin(), data.end(), mRandom); + return data; + } + + Tile insertTile(TileId tileId, TileVersion version) + { + std::string worldspace = "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)}; + } + }; + + TEST_F(DetourNavigatorNavMeshDbTest, get_max_tile_id_for_empty_db_should_return_zero) + { + EXPECT_EQ(mDb.getMaxTileId(), TileId {0}); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_be_found_by_key) + { + const TileId tileId {146}; + const TileVersion version {1}; + const auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + const auto result = mDb.findTile(worldspace, tilePosition, input); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->mTileId, tileId); + EXPECT_EQ(result->mVersion, version); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_change_max_tile_id) + { + insertTile(TileId {53}, TileVersion {1}); + EXPECT_EQ(mDb.getMaxTileId(), TileId {53}); + } + + TEST_F(DetourNavigatorNavMeshDbTest, updated_tile_should_change_data) + { + const TileId tileId {13}; + const TileVersion version {1}; + auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + generateRange(data.begin(), data.end(), mRandom); + ASSERT_EQ(mDb.updateTile(tileId, version, data), 1); + const auto row = mDb.getTileData(worldspace, tilePosition, input); + ASSERT_TRUE(row.has_value()); + EXPECT_EQ(row->mTileId, tileId); + EXPECT_EQ(row->mVersion, version); + ASSERT_FALSE(row->mData.empty()); + EXPECT_EQ(row->mData, data); + } + + TEST_F(DetourNavigatorNavMeshDbTest, on_inserted_duplicate_should_throw_exception) + { + const TileId tileId {53}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_duplicate_leaves_db_in_correct_state) + { + const TileId tileId {53}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + EXPECT_NO_THROW(insertTile(TileId {54}, version)); + } + + 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 TilePosition tilePosition {3, 4}; + const std::vector input1 = generateData(); + const std::vector input2 = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(TileId {53}, worldspace, tilePosition, version, input1, data), 1); + ASSERT_EQ(mDb.insertTile(TileId {54}, worldspace, tilePosition, version, input2, data), 1); + ASSERT_EQ(mDb.deleteTilesAt(worldspace, tilePosition), 2); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, input1).has_value()); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, input2).has_value()); + } + + TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_at_except_should_leave_tile_with_given_id) + { + const TileId leftTileId {53}; + const TileId removedTileId {54}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + const std::vector leftInput = generateData(); + const std::vector removedInput = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(leftTileId, worldspace, tilePosition, version, leftInput, data), 1); + ASSERT_EQ(mDb.insertTile(removedTileId, worldspace, tilePosition, version, removedInput, data), 1); + ASSERT_EQ(mDb.deleteTilesAtExcept(worldspace, tilePosition, leftTileId), 1); + const auto left = mDb.findTile(worldspace, tilePosition, leftInput); + ASSERT_TRUE(left.has_value()); + EXPECT_EQ(left->mTileId, leftTileId); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, removedInput).has_value()); + } + + TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_outside_range_should_leave_tiles_inside_given_rectangle) + { + TileId tileId {1}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const std::vector input = generateData(); + const std::vector data = generateData(); + for (int x = -2; x <= 2; ++x) + { + for (int y = -2; y <= 2; ++y) + { + ASSERT_EQ(mDb.insertTile(tileId, worldspace, TilePosition {x, y}, version, input, data), 1); + ++tileId; + } + } + const TilesPositionsRange range {TilePosition {-1, -1}, TilePosition {2, 2}}; + ASSERT_EQ(mDb.deleteTilesOutsideRange(worldspace, range), 16); + for (int x = -2; x <= 2; ++x) + for (int y = -2; y <= 2; ++y) + ASSERT_EQ(mDb.findTile(worldspace, TilePosition {x, y}, input).has_value(), + -1 <= x && x <= 1 && -1 <= y && y <= 1) << "x=" << x << " y=" << y; + } + + TEST_F(DetourNavigatorNavMeshDbTest, should_support_file_size_limit) + { + mDb = NavMeshDb(":memory:", 4096); + const auto f = [&] + { + for (std::int64_t i = 1; i <= 100; ++i) + insertTile(TileId {i}, TileVersion {1}); + }; + EXPECT_THROW(f(), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index e8e7820d91..0180373e10 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -1,50 +1,115 @@ #include "operators.hpp" +#include "generate.hpp" #include #include #include +#include +#include +#include +#include -#include +#include + +#include #include -namespace DetourNavigator -{ - static inline bool operator ==(const NavMeshDataRef& lhs, const NavMeshDataRef& rhs) - { - return std::make_pair(lhs.mValue, lhs.mSize) == std::make_pair(rhs.mValue, rhs.mSize); - } -} +#include +#include namespace { using namespace testing; using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + template + void generateRecastArray(T*& values, int size, Random& random) + { + values = static_cast(permRecastAlloc(size * sizeof(T))); + generateRange(values, values + static_cast(size), random); + } + + template + void generate(rcPolyMesh& value, int size, Random& random) + { + value.nverts = size; + value.maxpolys = size; + value.nvp = size; + value.npolys = size; + rcVcopy(value.bmin, osg::Vec3f(-1, -2, -3).ptr()); + rcVcopy(value.bmax, osg::Vec3f(3, 2, 1).ptr()); + generateValue(value.cs, random); + generateValue(value.ch, random); + generateValue(value.borderSize, random); + generateValue(value.maxEdgeError, random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.polys, getPolysLength(value), random); + generateRecastArray(value.regs, getRegsLength(value), random); + generateRecastArray(value.flags, getFlagsLength(value), random); + generateRecastArray(value.areas, getAreasLength(value), random); + } + + template + void generate(rcPolyMeshDetail& value, int size, Random& random) + { + value.nmeshes = size; + value.nverts = size; + value.ntris = size; + generateRecastArray(value.meshes, getMeshesLength(value), random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.tris, getTrisLength(value), random); + } + + template + void generate(PreparedNavMeshData& value, int size, Random& random) + { + generateValue(value.mUserId, random); + generateValue(value.mCellHeight, random); + generateValue(value.mCellSize, random); + generate(value.mPolyMesh, size, random); + generate(value.mPolyMeshDetail, size, random); + } + + std::unique_ptr makePeparedNavMeshData(int size) + { + std::minstd_rand random; + auto result = std::make_unique(); + generate(*result, size, random); + return result; + } + + std::unique_ptr clone(const PreparedNavMeshData& value) + { + return std::make_unique(value); + } + + Mesh makeMesh() + { + std::vector indices {{0, 1, 2}}; + std::vector vertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}}; + std::vector areaTypes {1, AreaType_ground}; + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } struct DetourNavigatorNavMeshTilesCacheTest : Test { - const osg::Vec3f mAgentHalfExtents {1, 2, 3}; + const AgentBounds mAgentBounds {CollisionShapeType::Aabb, {1, 2, 3}}; const TilePosition mTilePosition {0, 0}; const std::size_t mGeneration = 0; const std::size_t mRevision = 0; - const std::vector mIndices {{0, 1, 2}}; - const std::vector mVertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}}; - const std::vector mAreaTypes {1, AreaType_ground}; - const std::vector mWater {}; - const std::size_t mTrianglesPerChunk {1}; - const RecastMesh mRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, mWater, mTrianglesPerChunk}; - const std::vector mOffMeshConnections {}; - unsigned char* const mData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mNavMeshData {mData, 1}; - - const size_t cRecastMeshKeySize = mRecastMesh.getIndices().size() * sizeof(int) - + mRecastMesh.getVertices().size() * sizeof(float) - + mRecastMesh.getAreaTypes().size() * sizeof(AreaType) - + mRecastMesh.getWater().size() * sizeof(RecastMesh::Water) - + mOffMeshConnections.size() * sizeof(OffMeshConnection); - - const size_t cRecastMeshWithWaterKeySize = cRecastMeshKeySize + sizeof(RecastMesh::Water); + const Mesh mMesh {makeMesh()}; + const std::vector mWater {}; + const std::vector mHeightfields {}; + const std::vector mFlatHeightfields {}; + const std::vector mSources {}; + const RecastMesh mRecastMesh {mGeneration, mRevision, mMesh, mWater, mHeightfields, mFlatHeightfields, mSources}; + std::unique_ptr mPreparedNavMeshData {makePeparedNavMeshData(3)}; + + const std::size_t mRecastMeshSize = sizeof(mRecastMesh) + getSize(mRecastMesh); + const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(CellWater); + const std::size_t mPreparedNavMeshDataSize = sizeof(*mPreparedNavMeshData) + getSize(*mPreparedNavMeshData); }; TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_empty_cache_should_return_empty_value) @@ -52,7 +117,7 @@ namespace const std::size_t maxSize = 0; NavMeshTilesCache cache(maxSize); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_for_not_enought_cache_size_should_return_empty_value) @@ -60,60 +125,56 @@ namespace const std::size_t maxSize = 0; NavMeshTilesCache cache(maxSize); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData))); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData))); + EXPECT_NE(mPreparedNavMeshData, nullptr); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_return_cached_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + ASSERT_EQ(*mPreparedNavMeshData, *copy); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + EXPECT_EQ(result.get(), *copy); } - TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_throw_exception) + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_return_cached_element) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + 2 * navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; - - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_THROW( - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)), - InvalidArgument - ); + auto copy = clone(*mPreparedNavMeshData); + const auto sameCopy = clone(*mPreparedNavMeshData); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_EQ(mPreparedNavMeshData, nullptr); + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(copy)); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), *sameCopy); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto result = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result = cache.get(mAgentBounds, mTilePosition, mRecastMesh); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + EXPECT_EQ(result.get(), *copy); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_agent_half_extents_should_return_empty_value) { const std::size_t maxSize = 1; NavMeshTilesCache cache(maxSize); - const osg::Vec3f unexsistentAgentHalfExtents {1, 1, 1}; + const AgentBounds absentAgentBounds {CollisionShapeType::Aabb, {1, 1, 1}}; - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(unexsistentAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(absentAgentBounds, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_tile_position_should_return_empty_value) @@ -122,231 +183,201 @@ namespace NavMeshTilesCache cache(maxSize); const TilePosition unexistentTilePosition {1, 1}; - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(mAgentHalfExtents, unexistentTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentBounds, unexistentTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_recast_mesh_should_return_empty_value) { const std::size_t maxSize = 1; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh unexistentRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, water, mTrianglesPerChunk}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh unexistentRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh, mOffMeshConnections)); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, unexistentRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, water, mTrianglesPerChunk}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); + const auto copy = clone(*anotherPreparedNavMeshData); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData)); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result = cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, + std::move(anotherPreparedNavMeshData)); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {anotherData, 1})); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_EQ(result.get(), *copy); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_used_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, water, mTrianglesPerChunk}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); - const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); + const auto value = cache.set(mAgentBounds, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, + std::move(anotherPreparedNavMeshData))); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_set_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + 2 * navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + + const std::vector leastRecentlySetWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh leastRecentlySetRecastMesh(mGeneration, mRevision, mMesh, leastRecentlySetWater, + mHeightfields, mFlatHeightfields, mSources); + auto leastRecentlySetData = makePeparedNavMeshData(3); + + const std::vector mostRecentlySetWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); + const RecastMesh mostRecentlySetRecastMesh(mGeneration, mRevision, mMesh, mostRecentlySetWater, + mHeightfields, mFlatHeightfields, mSources); + auto mostRecentlySetData = makePeparedNavMeshData(3); + + ASSERT_TRUE(cache.set(mAgentBounds, mTilePosition, leastRecentlySetRecastMesh, + std::move(leastRecentlySetData))); + ASSERT_TRUE(cache.set(mAgentBounds, mTilePosition, mostRecentlySetRecastMesh, + std::move(mostRecentlySetData))); + + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); - const std::vector leastRecentlySetWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh leastRecentlySetRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, leastRecentlySetWater, mTrianglesPerChunk}; - const auto leastRecentlySetData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData leastRecentlySetNavMeshData {leastRecentlySetData, 1}; - - const std::vector mostRecentlySetWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh mostRecentlySetRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, mostRecentlySetWater, mTrianglesPerChunk}; - const auto mostRecentlySetData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mostRecentlySetNavMeshData {mostRecentlySetData, 1}; - - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections, - std::move(leastRecentlySetNavMeshData))); - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections, - std::move(mostRecentlySetNavMeshData))); - - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); - - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, leastRecentlySetRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mostRecentlySetRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_used_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + 2 * navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector leastRecentlyUsedWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh leastRecentlyUsedRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, leastRecentlyUsedWater, mTrianglesPerChunk}; - const auto leastRecentlyUsedData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData leastRecentlyUsedNavMeshData {leastRecentlyUsedData, 1}; + const std::vector leastRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh leastRecentlyUsedRecastMesh(mGeneration, mRevision, mMesh, leastRecentlyUsedWater, + mHeightfields, mFlatHeightfields, mSources); + auto leastRecentlyUsedData = makePeparedNavMeshData(3); + const auto leastRecentlyUsedCopy = clone(*leastRecentlyUsedData); - const std::vector mostRecentlyUsedWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh mostRecentlyUsedRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, mostRecentlyUsedWater, mTrianglesPerChunk}; - const auto mostRecentlyUsedData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mostRecentlyUsedNavMeshData {mostRecentlyUsedData, 1}; + const std::vector mostRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); + const RecastMesh mostRecentlyUsedRecastMesh(mGeneration, mRevision, mMesh, mostRecentlyUsedWater, + mHeightfields, mFlatHeightfields, mSources); + auto mostRecentlyUsedData = makePeparedNavMeshData(3); + const auto mostRecentlyUsedCopy = clone(*mostRecentlyUsedData); - cache.set(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections, - std::move(leastRecentlyUsedNavMeshData)); - cache.set(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections, - std::move(mostRecentlyUsedNavMeshData)); + cache.set(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh, std::move(leastRecentlyUsedData)); + cache.set(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh, std::move(mostRecentlyUsedData)); { - const auto value = cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections); + const auto value = cache.get(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh); ASSERT_TRUE(value); - ASSERT_EQ(value.get(), (NavMeshDataRef {leastRecentlyUsedData, 1})); + ASSERT_EQ(value.get(), *leastRecentlyUsedCopy); } { - const auto value = cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections); + const auto value = cache.get(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh); ASSERT_TRUE(value); - ASSERT_EQ(value.get(), (NavMeshDataRef {mostRecentlyUsedData, 1})); + ASSERT_EQ(value.get(), *mostRecentlyUsedCopy); } - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + const auto copy = clone(*mPreparedNavMeshData); + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_cache_max_size) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + 2 * navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk}; - const auto tooLargeData = reinterpret_cast(dtAlloc(2, DT_ALLOC_PERM)); - NavMeshData tooLargeNavMeshData {tooLargeData, 2}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh tooLargeRecastMesh(mGeneration, mRevision, mMesh, water, + mHeightfields, mFlatHeightfields, mSources); + auto tooLargeData = makePeparedNavMeshData(10); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections, - std::move(tooLargeNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, tooLargeRecastMesh, std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_size_of_unused_items) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize1 = cRecastMeshKeySize; - const std::size_t navMeshKeySize2 = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * navMeshDataSize + 2 * navMeshKeySize1 + 2 * navMeshKeySize2; + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector anotherWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, anotherWater, mTrianglesPerChunk}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector anotherWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, anotherWater, + mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); - const std::vector tooLargeWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, tooLargeWater, mTrianglesPerChunk}; - const auto tooLargeData = reinterpret_cast(dtAlloc(2, DT_ALLOC_PERM)); - NavMeshData tooLargeNavMeshData {tooLargeData, 2}; + const std::vector tooLargeWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); + const RecastMesh tooLargeRecastMesh(mGeneration, mRevision, mMesh, tooLargeWater, + mHeightfields, mFlatHeightfields, mSources); + auto tooLargeData = makePeparedNavMeshData(10); - const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); + const auto value = cache.set(mAgentBounds, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); ASSERT_TRUE(value); - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections, - std::move(tooLargeNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections)); + ASSERT_TRUE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, + std::move(anotherData))); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, tooLargeRecastMesh, + std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, anotherRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_used_after_set_then_used_by_get_item_should_left_this_item_available) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, water, mTrianglesPerChunk}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); - const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); + const auto firstCopy = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); ASSERT_TRUE(firstCopy); { - const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + const auto secondCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); ASSERT_TRUE(secondCopy); } - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_twice_used_item_should_left_this_item_available) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + 2 * navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); + const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto firstCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto firstCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); ASSERT_TRUE(firstCopy); { - const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + const auto secondCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); ASSERT_TRUE(secondCopy); } - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); } } diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/openmw_test_suite/detournavigator/operators.hpp index 92740c65f1..c2d9424db2 100644 --- a/apps/openmw_test_suite/detournavigator/operators.hpp +++ b/apps/openmw_test_suite/detournavigator/operators.hpp @@ -6,20 +6,10 @@ #include #include -#include #include -#include #include -namespace DetourNavigator -{ - static inline bool operator ==(const TileBounds& lhs, const TileBounds& rhs) - { - return lhs.mMin == rhs.mMin && lhs.mMax == rhs.mMax; - } -} - namespace { template @@ -48,7 +38,7 @@ namespace testing template <> inline testing::Message& Message::operator <<(const osg::Vec3f& value) { - return (*this) << "osg::Vec3f(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() + return (*this) << "Vec3fEq(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.y() << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.z() << ')'; diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp index bcbf448acd..dcdca1ee72 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp @@ -1,24 +1,52 @@ #include "operators.hpp" #include -#include #include #include +#include +#include +#include #include #include #include #include #include +#include + +#include #include #include +#include + namespace DetourNavigator { - static inline bool operator ==(const RecastMesh::Water& lhs, const RecastMesh::Water& rhs) + static inline bool operator ==(const Water& lhs, const Water& rhs) { - return lhs.mCellSize == rhs.mCellSize && lhs.mTransform == rhs.mTransform; + const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); }; + return tie(lhs) == tie(rhs); + } + + static inline bool operator ==(const CellWater& lhs, const CellWater& rhs) + { + const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); }; + return tie(lhs) == tie(rhs); + } + + static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs) + { + return makeTuple(lhs) == makeTuple(rhs); + } + + static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs) + { + const auto tie = [] (const FlatHeightfield& v) + { + return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); + }; + return tie(lhs) == tie(rhs); } } @@ -29,15 +57,14 @@ namespace struct DetourNavigatorRecastMeshBuilderTest : Test { - Settings mSettings; TileBounds mBounds; const std::size_t mGeneration = 0; const std::size_t mRevision = 0; + const osg::ref_ptr mSource {nullptr}; + const ObjectTransform mObjectTransform {ESM::Position {{0, 0, 0}, {0, 0, 0}}, 0.0f}; DetourNavigatorRecastMeshBuilderTest() { - mSettings.mRecastScaleFactor = 1.0f; - mSettings.mTrianglesPerChunk = 256; mBounds.mMin = osg::Vec2f(-std::numeric_limits::max() * std::numeric_limits::epsilon(), -std::numeric_limits::max() * std::numeric_limits::epsilon()); mBounds.mMax = osg::Vec2f(std::numeric_limits::max() * std::numeric_limits::epsilon(), @@ -47,11 +74,11 @@ namespace TEST_F(DetourNavigatorRecastMeshBuilderTest, create_for_empty_should_return_empty) { - RecastMeshBuilder builder(mSettings, mBounds); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector()); - EXPECT_EQ(recastMesh->getIndices(), std::vector()); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector()); + RecastMeshBuilder builder(mBounds); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector()); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape) @@ -60,16 +87,17 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); - builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), + AreaType_ground, mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_bhv_triangle_mesh_shape) @@ -77,70 +105,72 @@ namespace btTriangleMesh mesh; mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 2, 3, 0, - 0, 3, 4, - 0, 3, 0, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 0, 0, 3, + 0, 4, 3, + 2, 0, 3, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_terrian_shape) { const std::array heightfieldData {{0, 0, 0, 0}}; btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - RecastMeshBuilder builder(mSettings, mBounds); - builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -0.5, 0, -0.5, - -0.5, 0, 0.5, - 0.5, 0, -0.5, - 0.5, 0, 0.5, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 2, 1, 3})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), + AreaType_ground, mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -0.5, -0.5, 0, + -0.5, 0.5, 0, + 0.5, -0.5, 0, + 0.5, 0.5, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({0, 1, 2, 2, 1, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_box_shape_should_produce_12_triangles) { btBoxShape shape(btVector3(1, 1, 2)); - RecastMeshBuilder builder(mSettings, mBounds); - builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 2, 1, - -1, 2, 1, - 1, 2, -1, - -1, 2, -1, - 1, -2, 1, - -1, -2, 1, - 1, -2, -1, - -1, -2, -1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({ + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), + AreaType_ground, mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, -2, + -1, -1, 2, + -1, 1, -2, + -1, 1, 2, + 1, -1, -2, + 1, -1, 2, + 1, 1, -2, + 1, 1, 2, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ + 0, 1, 5, 0, 2, 3, - 3, 1, 0, 0, 4, 6, - 6, 2, 0, - 0, 1, 5, - 5, 4, 0, - 7, 5, 1, 1, 3, 7, - 7, 3, 2, 2, 6, 7, - 7, 6, 4, + 3, 1, 0, 4, 5, 7, - })) << recastMesh->getIndices(); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(12, AreaType_ground)); + 5, 4, 0, + 6, 2, 0, + 7, 3, 2, + 7, 5, 1, + 7, 6, 4, + })) << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(12, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_compound_shape) @@ -156,44 +186,44 @@ namespace shape.addChildShape(btTransform::getIdentity(), &triangle1); shape.addChildShape(btTransform::getIdentity(), &box); shape.addChildShape(btTransform::getIdentity(), &triangle2); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -1, -2, -1, - -1, -2, 1, - -1, 0, -1, - -1, 0, 1, - -1, 2, -1, - -1, 2, 1, - 1, -2, -1, - 1, -2, 1, - 1, 0, -1, - 1, 0, 1, - 1, 2, -1, - 1, 2, 1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({ - 8, 3, 2, - 11, 10, 4, - 4, 5, 11, - 11, 7, 6, - 6, 10, 11, - 11, 5, 1, - 1, 7, 11, - 0, 1, 5, - 5, 4, 0, - 0, 4, 10, - 10, 6, 0, - 0, 6, 7, - 7, 1, 0, - 8, 3, 9, - })) << recastMesh->getIndices(); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(14, AreaType_ground)); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, -2, + -1, -1, 0, + -1, -1, 2, + -1, 1, -2, + -1, 1, 0, + -1, 1, 2, + 1, -1, -2, + 1, -1, 0, + 1, -1, 2, + 1, 1, -2, + 1, 1, 0, + 1, 1, 2, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ + 0, 2, 8, + 0, 3, 5, + 0, 6, 9, + 2, 5, 11, + 3, 9, 11, + 5, 2, 0, + 6, 8, 11, + 7, 4, 1, + 7, 4, 10, + 8, 6, 0, + 9, 3, 0, + 11, 5, 3, + 11, 8, 2, + 11, 9, 6, + })) << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(14, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape) @@ -203,20 +233,20 @@ namespace btBvhTriangleMeshShape triangle(&mesh, true); btCompoundShape shape; shape.addChildShape(btTransform::getIdentity(), &triangle); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 2, 3, 0, - 0, 3, 4, - 0, 3, 0, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 0, 0, 3, + 0, 4, 3, + 2, 0, 3, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape_with_transformed_bhv_triangle_shape) @@ -227,20 +257,20 @@ namespace btCompoundShape shape; shape.addChildShape(btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), &triangle); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 3, 12, 2, - 1, 12, 10, - 1, 12, 2, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 1, 2, 12, + 1, 10, 12, + 3, 2, 12, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, without_bounds_add_bhv_triangle_shape_should_not_filter_by_bounds) @@ -249,48 +279,47 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - -2, 0, -3, - -3, 0, -2, - -3, 0, -3, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 3, 4, 5})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(2, AreaType_ground)); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 5, 4, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(2, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_bhv_triangle_shape_should_filter_by_bounds) { - mSettings.mRecastScaleFactor = 0.1f; - mBounds.mMin = osg::Vec2f(-3, -3) * mSettings.mRecastScaleFactor; - mBounds.mMax = osg::Vec2f(-2, -2) * mSettings.mRecastScaleFactor; + mBounds.mMin = osg::Vec2f(-3, -3); + mBounds.mMax = osg::Vec2f(-2, -2); btTriangleMesh mesh; mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -0.2f, 0, -0.3f, - -0.3f, 0, -0.2f, - -0.3f, 0, -0.3f, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_x_bhv_triangle_shape_should_filter_by_bounds) @@ -301,21 +330,21 @@ namespace mesh.addTriangle(btVector3(0, -1, -1), btVector3(0, -1, -1), btVector3(0, 1, -1)); mesh.addTriangle(btVector3(0, -3, -3), btVector3(0, -3, -2), btVector3(0, -2, -3)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(1, 0, 0), static_cast(-osg::PI_4))), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - 0, -0.70710659027099609375, -3.535533905029296875, - 0, 0.707107067108154296875, -3.535533905029296875, - 0, 2.384185791015625e-07, -4.24264049530029296875, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5f), std::vector({ + 0, -4.24264049530029296875f, 4.44089209850062616169452667236328125e-16f, + 0, -3.535533905029296875f, -0.707106769084930419921875f, + 0, -3.535533905029296875f, 0.707106769084930419921875f, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({1, 2, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_y_bhv_triangle_shape_should_filter_by_bounds) @@ -326,21 +355,21 @@ namespace mesh.addTriangle(btVector3(-1, 0, -1), btVector3(-1, 0, 1), btVector3(1, 0, -1)); mesh.addTriangle(btVector3(-3, 0, -3), btVector3(-3, 0, -2), btVector3(-2, 0, -3)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(0, 1, 0), static_cast(osg::PI_4))), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - -3.535533905029296875, -0.70710659027099609375, 0, - -3.535533905029296875, 0.707107067108154296875, 0, - -4.24264049530029296875, 2.384185791015625e-07, 0, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5f), std::vector({ + -4.24264049530029296875f, 0, 4.44089209850062616169452667236328125e-16f, + -3.535533905029296875f, 0, -0.707106769084930419921875f, + -3.535533905029296875f, 0, 0.707106769084930419921875f, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({1, 2, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_z_bhv_triangle_shape_should_filter_by_bounds) @@ -351,21 +380,21 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(0, 0, 1), static_cast(osg::PI_4))), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - 1.41421353816986083984375, 0, 1.1920928955078125e-07, - -1.41421353816986083984375, 0, -1.1920928955078125e-07, - 1.1920928955078125e-07, 0, -1.41421353816986083984375, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5f), std::vector({ + -1.41421353816986083984375f, -1.1102230246251565404236316680908203125e-16f, 0, + 1.1102230246251565404236316680908203125e-16f, -1.41421353816986083984375f, 0, + 1.41421353816986083984375f, 1.1102230246251565404236316680908203125e-16f, 0, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 0, 1})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, flags_values_should_be_corresponding_to_added_objects) @@ -376,37 +405,37 @@ namespace btTriangleMesh mesh2; mesh2.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape2(&mesh2, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape1), btTransform::getIdentity(), - AreaType_ground + AreaType_ground, mSource, mObjectTransform ); builder.addObject( static_cast(shape2), btTransform::getIdentity(), - AreaType_null + AreaType_null, mSource, mObjectTransform ); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - -2, 0, -3, - -3, 0, -2, - -3, 0, -3, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 3, 4, 5})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_null})); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 5, 4, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_null, AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it) { - RecastMeshBuilder builder(mSettings, mBounds); - builder.addWater(1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getWater(), std::vector({ - RecastMesh::Water {1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))} + RecastMeshBuilder builder(mBounds); + builder.addWater(osg::Vec2i(1, 2), Water {1000, 300.0f}); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getWater(), std::vector({ + CellWater {osg::Vec2i(1, 2), Water {1000, 300.0f}} })); } @@ -417,16 +446,126 @@ namespace mesh.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); - builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); - const auto recastMesh = builder.create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -1, 0, -1, - -1, 0, 1, - 1, 0, -1, - 1, 0, 1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({2, 1, 0, 2, 1, 3})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + 1, 1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 2, 1, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_flat_heightfield_should_add_intersection) + { + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float height = 10; + mBounds.mMin = osg::Vec2f(100, 100); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, height); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + EXPECT_EQ(recastMesh->getFlatHeightfields(), std::vector({ + FlatHeightfield {cellPosition, cellSize, height}, + })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_inside_tile) + { + constexpr std::size_t size = 3; + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; + expected.mHeights = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }; + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_to_shifted_cell_inside_tile) + { + constexpr std::size_t size = 3; + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + const osg::Vec2i cellPosition(1, 2); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + RecastMeshBuilder builder(maxCellTileBounds(cellPosition, cellSize)); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; + expected.mHeights = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }; + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_should_add_intersection) + { + constexpr std::size_t size = 3; + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + mBounds.mMin = osg::Vec2f(750, 750); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = 2; + expected.mMinHeight = 0; + expected.mMaxHeight = 8; + expected.mHeights = { + 4, 5, + 7, 8, + }; + expected.mOriginalSize = 3; + expected.mMinX = 1; + expected.mMinY = 1; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); } } diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp index a3606f8273..ff0d3e519c 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp @@ -1,6 +1,7 @@ #include "operators.hpp" #include +#include #include #include @@ -14,20 +15,23 @@ namespace struct DetourNavigatorRecastMeshObjectTest : Test { - btBoxShape mBoxShape {btVector3(1, 2, 3)}; - btCompoundShape mCompoundShape {btVector3(1, 2, 3)}; - btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)}; + btBoxShape mBoxShapeImpl {btVector3(1, 2, 3)}; + const ObjectTransform mObjectTransform {ESM::Position {{1, 2, 3}, {1, 2, 3}}, 0.5f}; + CollisionShape mBoxShape {nullptr, mBoxShapeImpl, mObjectTransform}; + btCompoundShape mCompoundShapeImpl {true}; + CollisionShape mCompoundShape {nullptr, mCompoundShapeImpl, mObjectTransform}; + btTransform mTransform {Misc::Convert::makeBulletTransform(mObjectTransform.mPosition)}; DetourNavigatorRecastMeshObjectTest() { - mCompoundShape.addChildShape(mTransform, std::addressof(mBoxShape)); + mCompoundShapeImpl.addChildShape(mTransform, std::addressof(mBoxShapeImpl)); } }; TEST_F(DetourNavigatorRecastMeshObjectTest, constructed_object_should_have_shape_and_transform) { const RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); - EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShape)); + EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShapeImpl)); EXPECT_EQ(object.getTransform(), mTransform); } @@ -58,14 +62,14 @@ namespace TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_changed_child_transform_should_return_true) { RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); - mCompoundShape.updateChildTransform(0, btTransform::getIdentity()); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); EXPECT_TRUE(object.update(mTransform, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshObjectTest, repeated_update_for_compound_shape_without_changes_should_return_false) { RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); - mCompoundShape.updateChildTransform(0, btTransform::getIdentity()); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); object.update(mTransform, AreaType_ground); EXPECT_FALSE(object.update(mTransform, AreaType_ground)); } @@ -73,7 +77,7 @@ namespace TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_changed_local_scaling_should_return_true) { RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); - mBoxShape.setLocalScaling(btVector3(2, 2, 2)); + mBoxShapeImpl.setLocalScaling(btVector3(2, 2, 2)); EXPECT_TRUE(object.update(mTransform, AreaType_ground)); } } diff --git a/apps/openmw_test_suite/detournavigator/settings.hpp b/apps/openmw_test_suite/detournavigator/settings.hpp new file mode 100644 index 0000000000..dc37dc7550 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/settings.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H + +#include + +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + inline Settings makeSettings() + { + Settings result; + result.mEnableWriteRecastMeshToFile = false; + result.mEnableWriteNavMeshToFile = false; + result.mEnableRecastMeshFileNameRevision = false; + result.mEnableNavMeshFileNameRevision = false; + result.mRecast.mBorderSize = 16; + result.mRecast.mCellHeight = 0.2f; + result.mRecast.mCellSize = 0.2f; + result.mRecast.mDetailSampleDist = 6; + result.mRecast.mDetailSampleMaxError = 1; + result.mRecast.mMaxClimb = 34; + result.mRecast.mMaxSimplificationError = 1.3f; + result.mRecast.mMaxSlope = 49; + result.mRecast.mRecastScaleFactor = 0.017647058823529415f; + result.mRecast.mSwimHeightScale = 0.89999997615814208984375f; + result.mRecast.mMaxEdgeLen = 12; + result.mDetour.mMaxNavMeshQueryNodes = 2048; + result.mRecast.mMaxVertsPerPoly = 6; + result.mRecast.mRegionMergeArea = 400; + result.mRecast.mRegionMinArea = 64; + result.mRecast.mTileSize = 64; + result.mWaitUntilMinDistanceToPlayer = std::numeric_limits::max(); + result.mAsyncNavMeshUpdaterThreads = 1; + result.mMaxNavMeshTilesCacheSize = 1024 * 1024; + result.mDetour.mMaxPolygonPathSize = 1024; + result.mDetour.mMaxSmoothPathSize = 1024; + result.mDetour.mMaxPolys = 4096; + result.mMaxTilesNumber = 512; + result.mMinUpdateInterval = std::chrono::milliseconds(50); + result.mWriteToNavMeshDb = true; + return result; + } + } +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/settingsutils.cpp b/apps/openmw_test_suite/detournavigator/settingsutils.cpp index ffed64ab81..f06f3b3e32 100644 --- a/apps/openmw_test_suite/detournavigator/settingsutils.cpp +++ b/apps/openmw_test_suite/detournavigator/settingsutils.cpp @@ -11,7 +11,7 @@ namespace struct DetourNavigatorGetTilePositionTest : Test { - Settings mSettings; + RecastSettings mSettings; DetourNavigatorGetTilePositionTest() { @@ -47,7 +47,7 @@ namespace struct DetourNavigatorMakeTileBoundsTest : Test { - Settings mSettings; + RecastSettings mSettings; DetourNavigatorMakeTileBoundsTest() { diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp index eac3c024fe..4803f452b3 100644 --- a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp @@ -4,10 +4,6 @@ #include #include -#include -#include -#include -#include #include #include @@ -19,8 +15,12 @@ namespace struct DetourNavigatorTileCachedRecastMeshManagerTest : Test { - Settings mSettings; - std::vector mChangedTiles; + RecastSettings mSettings; + std::vector mAddedTiles; + std::vector> mChangedTiles; + 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); DetourNavigatorTileCachedRecastMeshManagerTest() { @@ -28,25 +28,23 @@ namespace mSettings.mCellSize = 0.2f; mSettings.mRecastScaleFactor = 0.017647058823529415f; mSettings.mTileSize = 64; - mSettings.mTrianglesPerChunk = 256; } - void onChangedTile(const TilePosition& tilePosition) + void onAddedTile(const TilePosition& tilePosition) { - mChangedTiles.push_back(tilePosition); + mAddedTiles.push_back(tilePosition); + } + + void onChangedTile(const TilePosition& tilePosition, ChangeType changeType) + { + mChangedTiles.emplace_back(tilePosition, changeType); } }; TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr) { TileCachedRecastMeshManager manager(mSettings); - EXPECT_EQ(manager.getMesh(TilePosition(0, 0)), nullptr); - } - - TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, has_tile_for_empty_should_return_false) - { - TileCachedRecastMeshManager manager(mSettings); - EXPECT_FALSE(manager.hasTile(TilePosition(0, 0))); + EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero) @@ -59,7 +57,7 @@ namespace { TileCachedRecastMeshManager manager(mSettings); std::size_t calls = 0; - manager.forEachTilePosition([&] (const TilePosition&) { ++calls; }); + manager.forEachTile([&] (const TilePosition&, const CachedRecastMeshManager&) { ++calls; }); EXPECT_EQ(calls, 0); } @@ -67,15 +65,43 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - EXPECT_TRUE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + EXPECT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {})); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_for_existing_object_should_return_false) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {})); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + 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, [] (auto) {})); + for (int x = -1; x < 1; ++x) + for (int y = -1; y < 1; ++y) + ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_return_added_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + TileBounds bounds; + bounds.mMin = osg::Vec2f(182, 182); + bounds.mMax = osg::Vec2f(1000, 1000); + manager.setBounds(bounds); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, + [&] (const auto& v) { onAddedTile(v); }); + EXPECT_THAT(mAddedTiles, ElementsAre(TilePosition(0, 0))); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, update_object_for_changed_object_should_return_changed_tiles) @@ -83,107 +109,134 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); - EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, - [&] (const auto& v) { onChangedTile(v); })); - EXPECT_THAT( - mChangedTiles, - ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(0, -1), TilePosition(0, 0), - TilePosition(1, -1), TilePosition(1, 0)) - ); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + TileBounds bounds; + bounds.mMin = osg::Vec2f(-1000, -1000); + bounds.mMax = osg::Vec2f(1000, 1000); + manager.setBounds(bounds); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, [] (auto) {}); + EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, + [&] (const auto& ... v) { onChangedTile(v ...); })); + EXPECT_THAT(mChangedTiles, ElementsAre( + std::pair(TilePosition(-1, -1), ChangeType::add), + std::pair(TilePosition(-1, 0), ChangeType::add), + std::pair(TilePosition(0, -1), ChangeType::update), + std::pair(TilePosition(0, 0), ChangeType::update), + std::pair(TilePosition(1, -1), ChangeType::remove), + std::pair(TilePosition(1, 0), ChangeType::remove) + )); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, update_object_for_not_changed_object_should_return_empty) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, - [&] (const auto& v) { onChangedTile(v); })); - EXPECT_EQ(mChangedTiles, std::vector()); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, + [&] (const auto& ... v) { onChangedTile(v ...); })); + EXPECT_THAT(mChangedTiles, IsEmpty()); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile) { TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + 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); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_nullptr_for_unused_tile) { TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_recast_mesh_for_each_used_tile) { TileCachedRecastMeshManager manager(mSettings); + TileBounds bounds; + bounds.mMin = osg::Vec2f(-1000, -1000); + bounds.mMax = osg::Vec2f(1000, 1000); + manager.setBounds(bounds); + manager.setWorldspace("worldspace"); + const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); - - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); - EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(1, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(1, -1)), nullptr); - - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); - EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, [] (auto) {}); + 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); + + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto, auto) {}); + 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); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_nullptr_for_unused_tile) { TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); - EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, [] (auto) {}); + EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); + EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); - EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr); - EXPECT_EQ(manager.getMesh(TilePosition(1, -1)), nullptr); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto, auto) {}); + EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr); + EXPECT_EQ(manager.getMesh("worldspace", 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"); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); manager.removeObject(ObjectId(&boxShape)); - EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr); - EXPECT_EQ(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_EQ(manager.getMesh(TilePosition(0, 0)), 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); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_not_changed_object_after_update_should_return_recast_mesh_for_same_tiles) { TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); const btBoxShape boxShape(btVector3(20, 20, 100)); - - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); - - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); - EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + 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); + + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto, auto) {}); + 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); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_after_add_object_new_should_return_incremented_value) @@ -191,7 +244,8 @@ namespace TileCachedRecastMeshManager manager(mSettings); const auto initialRevision = manager.getRevision(); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_EQ(manager.getRevision(), initialRevision + 1); } @@ -199,9 +253,10 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); const auto beforeAddRevision = manager.getRevision(); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {})); EXPECT_EQ(manager.getRevision(), beforeAddRevision); } @@ -210,19 +265,22 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, [] (auto) {}); const auto beforeUpdateRevision = manager.getRevision(); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto, auto) {}); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision + 1); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_after_update_not_changed_object_should_return_same_value) { TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); const auto beforeUpdateRevision = manager.getRevision(); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto, auto) {}); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision); } @@ -230,7 +288,8 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); const auto beforeRemoveRevision = manager.getRevision(); manager.removeObject(ObjectId(&boxShape)); EXPECT_EQ(manager.getRevision(), beforeRemoveRevision + 1); @@ -243,4 +302,132 @@ namespace manager.removeObject(ObjectId(&manager)); EXPECT_EQ(manager.getRevision(), beforeRemoveRevision); } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_new_water_should_return_true) + { + TileCachedRecastMeshManager manager(mSettings); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + EXPECT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) + ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_max_int_should_not_add_new_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + 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, [] (auto) {})); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = std::numeric_limits::max(); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_absent_cell_should_return_nullopt) + { + TileCachedRecastMeshManager manager(mSettings); + EXPECT_EQ(manager.removeWater(osg::Vec2i(0, 0)), std::nullopt); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_return_removed_water) + { + TileCachedRecastMeshManager manager(mSettings); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + const auto result = manager.removeWater(cellPosition); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->mCellSize, cellSize); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + ASSERT_TRUE(manager.removeWater(cellPosition)); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_leave_not_empty_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + 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, [] (auto) {})); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + ASSERT_TRUE(manager.removeWater(cellPosition)); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh("worldspace", 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"); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + 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, [] (auto) {})); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + ASSERT_TRUE(manager.removeObject(ObjectId(&boxShape))); + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) + ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_new_worldspace_should_remove_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace("worldspace"); + 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, [] (auto) {})); + manager.setWorldspace("other"); + for (int x = -1; x < 1; ++x) + for (int y = -1; y < 1; ++y) + ASSERT_EQ(manager.getMesh("other", TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_bounds_should_return_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + TileBounds bounds; + bounds.mMin = osg::Vec2f(182, 0); + bounds.mMax = osg::Vec2f(1000, 1000); + manager.setBounds(bounds); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + bounds.mMin = osg::Vec2f(-1000, -1000); + bounds.mMax = osg::Vec2f(0, -182); + EXPECT_THAT(manager.setBounds(bounds), ElementsAre( + std::pair(TilePosition(-1, -1), ChangeType::add), + std::pair(TilePosition(0, 0), ChangeType::remove) + )); + } } diff --git a/apps/openmw_test_suite/esm/test_fixed_string.cpp b/apps/openmw_test_suite/esm/test_fixed_string.cpp index bd598cc932..1189f667e5 100644 --- a/apps/openmw_test_suite/esm/test_fixed_string.cpp +++ b/apps/openmw_test_suite/esm/test_fixed_string.cpp @@ -1,12 +1,12 @@ #include #include "components/esm/esmcommon.hpp" +#include "components/esm/defs.hpp" TEST(EsmFixedString, operator__eq_ne) { { SCOPED_TRACE("asdc == asdc"); - ESM::NAME name; - name.assign("asdc"); + constexpr ESM::NAME name("asdc"); char s[4] = {'a', 's', 'd', 'c'}; std::string ss(s, 4); @@ -16,8 +16,7 @@ TEST(EsmFixedString, operator__eq_ne) } { SCOPED_TRACE("asdc == asdcx"); - ESM::NAME name; - name.assign("asdc"); + constexpr ESM::NAME name("asdc"); char s[5] = {'a', 's', 'd', 'c', 'x'}; std::string ss(s, 5); @@ -27,8 +26,7 @@ TEST(EsmFixedString, operator__eq_ne) } { SCOPED_TRACE("asdc == asdc[NULL]"); - ESM::NAME name; - name.assign("asdc"); + const ESM::NAME name("asdc"); char s[5] = {'a', 's', 'd', 'c', '\0'}; std::string ss(s, 5); @@ -41,8 +39,7 @@ TEST(EsmFixedString, operator__eq_ne_const) { { SCOPED_TRACE("asdc == asdc (const)"); - ESM::NAME name; - name.assign("asdc"); + constexpr ESM::NAME name("asdc"); const char s[4] = { 'a', 's', 'd', 'c' }; std::string ss(s, 4); @@ -52,8 +49,7 @@ TEST(EsmFixedString, operator__eq_ne_const) } { SCOPED_TRACE("asdc == asdcx (const)"); - ESM::NAME name; - name.assign("asdc"); + constexpr ESM::NAME name("asdc"); const char s[5] = { 'a', 's', 'd', 'c', 'x' }; std::string ss(s, 5); @@ -63,8 +59,7 @@ TEST(EsmFixedString, operator__eq_ne_const) } { SCOPED_TRACE("asdc == asdc[NULL] (const)"); - ESM::NAME name; - name.assign("asdc"); + constexpr ESM::NAME name("asdc"); const char s[5] = { 'a', 's', 'd', 'c', '\0' }; std::string ss(s, 5); @@ -105,3 +100,58 @@ TEST(EsmFixedString, is_pod) ASSERT_TRUE(std::is_pod::value); ASSERT_TRUE(std::is_pod::value); } + +TEST(EsmFixedString, assign_should_zero_untouched_bytes_for_4) +{ + ESM::NAME value; + value = static_cast(0xFFFFFFFFu); + value.assign(std::string(1, 'a')); + EXPECT_EQ(value, static_cast('a')) << value.toInt(); +} + +TEST(EsmFixedString, assign_should_only_truncate_for_4) +{ + ESM::NAME value; + value.assign(std::string(5, 'a')); + EXPECT_EQ(value, std::string(4, 'a')); +} + +TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero) +{ + ESM::FixedString<17> value; + value.assign(std::string(20, 'a')); + EXPECT_EQ(value, std::string(16, 'a')); +} + +TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero_for_32) +{ + ESM::NAME32 value; + value.assign(std::string(33, 'a')); + EXPECT_EQ(value, std::string(31, 'a')); +} + +TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero_for_64) +{ + ESM::NAME64 value; + value.assign(std::string(65, 'a')); + EXPECT_EQ(value, std::string(63, 'a')); +} + +TEST(EsmFixedString, assignment_operator_is_supported_for_uint32) +{ + ESM::NAME value; + value = static_cast(0xFEDCBA98u); + EXPECT_EQ(value, static_cast(0xFEDCBA98u)) << value.toInt(); +} + +TEST(EsmFixedString, construction_from_uint32_is_supported) +{ + constexpr ESM::NAME value(0xFEDCBA98u); + EXPECT_EQ(value, static_cast(0xFEDCBA98u)) << value.toInt(); +} + +TEST(EsmFixedString, construction_from_RecNameInts_is_supported) +{ + constexpr ESM::NAME value(ESM::RecNameInts::REC_ACTI); + EXPECT_EQ(value, static_cast(ESM::RecNameInts::REC_ACTI)) << value.toInt(); +} diff --git a/apps/openmw_test_suite/esm/variant.cpp b/apps/openmw_test_suite/esm/variant.cpp new file mode 100644 index 0000000000..53f31cc1c9 --- /dev/null +++ b/apps/openmw_test_suite/esm/variant.cpp @@ -0,0 +1,513 @@ +#include +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace ESM; + + Variant makeVariant(VarType type) + { + Variant v; + v.setType(type); + return v; + } + + Variant makeVariant(VarType type, int value) + { + Variant v; + v.setType(type); + v.setInteger(value); + return v; + } + + TEST(ESMVariantTest, move_constructed_should_have_data) + { + Variant a(int{42}); + const Variant b(std::move(a)); + ASSERT_EQ(b.getInteger(), 42); + } + + TEST(ESMVariantTest, copy_constructed_is_equal_to_source) + { + const Variant a(int{42}); + const Variant b(a); + ASSERT_EQ(a, b); + } + + TEST(ESMVariantTest, copy_constructed_does_not_share_data_with_source) + { + const Variant a(int{42}); + Variant b(a); + b.setInteger(13); + ASSERT_EQ(a.getInteger(), 42); + ASSERT_EQ(b.getInteger(), 13); + } + + TEST(ESMVariantTest, move_assigned_should_have_data) + { + Variant b; + { + Variant a(int{42}); + b = std::move(a); + } + ASSERT_EQ(b.getInteger(), 42); + } + + TEST(ESMVariantTest, copy_assigned_is_equal_to_source) + { + const Variant a(int{42}); + Variant b; + b = a; + ASSERT_EQ(a, b); + } + + TEST(ESMVariantTest, not_equal_is_negation_of_equal) + { + const Variant a(int{42}); + Variant b; + b = a; + ASSERT_TRUE(!(a != b)); + } + + TEST(ESMVariantTest, different_types_are_not_equal) + { + ASSERT_NE(Variant(int{42}), Variant(float{2.7f})); + } + + struct ESMVariantWriteToOStreamTest : TestWithParam> {}; + + TEST_P(ESMVariantWriteToOStreamTest, should_write) + { + const auto [variant, result] = GetParam(); + std::ostringstream s; + s << variant; + ASSERT_EQ(s.str(), result); + } + + INSTANTIATE_TEST_SUITE_P(VariantAsString, ESMVariantWriteToOStreamTest, Values( + std::make_tuple(Variant(), "variant none"), + std::make_tuple(Variant(int{42}), "variant long: 42"), + std::make_tuple(Variant(float{2.7f}), "variant float: 2.7"), + std::make_tuple(Variant(std::string("foo")), "variant string: \"foo\""), + std::make_tuple(makeVariant(VT_Unknown), "variant unknown"), + std::make_tuple(makeVariant(VT_Short, 42), "variant short: 42"), + std::make_tuple(makeVariant(VT_Int, 42), "variant int: 42") + )); + + struct ESMVariantGetTypeTest : Test {}; + + TEST(ESMVariantGetTypeTest, default_constructed_should_return_none) + { + ASSERT_EQ(Variant().getType(), VT_None); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_int_should_return_long) + { + ASSERT_EQ(Variant(int{}).getType(), VT_Long); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_float_should_return_float) + { + ASSERT_EQ(Variant(float{}).getType(), VT_Float); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_lvalue_string_should_return_string) + { + const std::string string; + ASSERT_EQ(Variant(string).getType(), VT_String); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_rvalue_string_should_return_string) + { + ASSERT_EQ(Variant(std::string{}).getType(), VT_String); + } + + struct ESMVariantGetIntegerTest : Test {}; + + TEST(ESMVariantGetIntegerTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getInteger(), std::runtime_error); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_int_should_return_same_value) + { + const Variant variant(int{42}); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_float_should_return_casted_to_int) + { + const Variant variant(float{2.7}); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_string_should_throw_exception) + { + const Variant variant(std::string("foo")); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantGetFloatTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getFloat(), std::runtime_error); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_int_should_return_casted_to_float) + { + const Variant variant(int{42}); + ASSERT_EQ(variant.getFloat(), 42); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_float_should_return_same_value) + { + const Variant variant(float{2.7f}); + ASSERT_EQ(variant.getFloat(), 2.7f); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_string_should_throw_exception) + { + const Variant variant(std::string("foo")); + ASSERT_THROW(variant.getFloat(), std::runtime_error); + } + + TEST(ESMVariantGetStringTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_int_should_throw_exception) + { + const Variant variant(int{42}); + ASSERT_THROW(variant.getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_float_should_throw_exception) + { + const Variant variant(float{2.7}); + ASSERT_THROW(variant.getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_string_should_return_same_value) + { + const Variant variant(std::string("foo")); + ASSERT_EQ(variant.getString(), "foo"); + } + + TEST(ESMVariantSetTypeTest, for_unknown_should_reset_data) + { + Variant variant(int{42}); + variant.setType(VT_Unknown); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantSetTypeTest, for_none_should_reset_data) + { + Variant variant(int{42}); + variant.setType(VT_None); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantSetTypeTest, for_same_type_should_not_change_value) + { + Variant variant(int{42}); + variant.setType(VT_Long); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_int_should_cast_float_to_int) + { + Variant variant(float{2.7f}); + variant.setType(VT_Int); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_string_replaced_by_int_should_set_default_initialized_data) + { + Variant variant(std::string("foo")); + variant.setType(VT_Int); + ASSERT_EQ(variant.getInteger(), 0); + } + + TEST(ESMVariantSetTypeTest, for_default_constructed_replaced_by_float_should_set_default_initialized_value) + { + Variant variant; + variant.setType(VT_Float); + ASSERT_EQ(variant.getInteger(), 0.0f); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_short_should_cast_data_to_int) + { + Variant variant(float{2.7f}); + variant.setType(VT_Short); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_long_should_cast_data_to_int) + { + Variant variant(float{2.7f}); + variant.setType(VT_Long); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_int_replaced_by_float_should_cast_data_to_float) + { + Variant variant(int{42}); + variant.setType(VT_Float); + ASSERT_EQ(variant.getFloat(), 42.0f); + } + + TEST(ESMVariantSetTypeTest, for_int_replaced_by_string_should_set_default_initialized_data) + { + Variant variant(int{42}); + variant.setType(VT_String); + ASSERT_EQ(variant.getString(), ""); + } + + TEST(ESMVariantSetIntegerTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetIntegerTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetIntegerTest, for_default_int_should_change_value) + { + Variant variant(int{13}); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_int_should_change_value) + { + Variant variant; + variant.setType(VT_Int); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_short_should_change_value) + { + Variant variant; + variant.setType(VT_Short); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_float_should_change_value) + { + Variant variant(float{2.7f}); + variant.setInteger(42); + ASSERT_EQ(variant.getFloat(), 42.0f); + } + + TEST(ESMVariantSetIntegerTest, for_string_should_throw_exception) + { + Variant variant(std::string{}); + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_default_int_should_change_value) + { + Variant variant(int{13}); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_int_should_change_value) + { + Variant variant; + variant.setType(VT_Int); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_short_should_change_value) + { + Variant variant; + variant.setType(VT_Short); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_float_should_change_value) + { + Variant variant(float{2.7f}); + variant.setFloat(3.14f); + ASSERT_EQ(variant.getFloat(), 3.14f); + } + + TEST(ESMVariantSetFloatTest, for_string_should_throw_exception) + { + Variant variant(std::string{}); + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetStringTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_default_int_should_throw_exception) + { + Variant variant(int{13}); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_int_should_throw_exception) + { + Variant variant; + variant.setType(VT_Int); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_short_should_throw_exception) + { + Variant variant; + variant.setType(VT_Short); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_float_should_throw_exception) + { + Variant variant(float{2.7f}); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_string_should_change_value) + { + Variant variant(std::string("foo")); + variant.setString("bar"); + ASSERT_EQ(variant.getString(), "bar"); + } + + struct WriteToESMTestCase + { + Variant mVariant; + Variant::Format mFormat; + std::size_t mDataSize {}; + }; + + std::string write(const Variant& variant, const Variant::Format format) + { + std::ostringstream out; + ESMWriter writer; + writer.save(out); + variant.write(writer, format); + writer.close(); + return out.str(); + } + + Variant read(const Variant::Format format, const std::string& data) + { + Variant result; + ESMReader reader; + reader.open(std::make_unique(data), ""); + result.read(reader, format); + return result; + } + + Variant writeAndRead(const Variant& variant, const Variant::Format format, std::size_t dataSize) + { + const std::string data = write(variant, format); + EXPECT_EQ(data.size(), dataSize); + return read(format, data); + } + + struct ESMVariantToESMTest : TestWithParam {}; + + TEST_P(ESMVariantToESMTest, deserialized_is_equal_to_serialized) + { + const auto param = GetParam(); + const auto result = writeAndRead(param.mVariant, param.mFormat, param.mDataSize); + ASSERT_EQ(param.mVariant, result); + } + + INSTANTIATE_TEST_SUITE_P(VariantAndData, ESMVariantToESMTest, Values( + WriteToESMTestCase {Variant(), Variant::Format_Gmst, 324}, + WriteToESMTestCase {Variant(int{42}), Variant::Format_Global, 345}, + WriteToESMTestCase {Variant(float{2.7f}), Variant::Format_Global, 345}, + WriteToESMTestCase {Variant(float{2.7f}), Variant::Format_Info, 336}, + WriteToESMTestCase {Variant(float{2.7f}), Variant::Format_Local, 336}, + WriteToESMTestCase {makeVariant(VT_Short, 42), Variant::Format_Global, 345}, + WriteToESMTestCase {makeVariant(VT_Short, 42), Variant::Format_Local, 334}, + WriteToESMTestCase {makeVariant(VT_Int, 42), Variant::Format_Info, 336}, + WriteToESMTestCase {makeVariant(VT_Int, 42), Variant::Format_Local, 336} + )); + + struct ESMVariantToESMNoneTest : TestWithParam {}; + + TEST_P(ESMVariantToESMNoneTest, deserialized_is_none) + { + const auto param = GetParam(); + const auto result = writeAndRead(param.mVariant, param.mFormat, param.mDataSize); + ASSERT_EQ(Variant(), result); + } + + INSTANTIATE_TEST_SUITE_P(VariantAndData, ESMVariantToESMNoneTest, Values( + WriteToESMTestCase {Variant(float{2.7f}), Variant::Format_Gmst, 336}, + WriteToESMTestCase {Variant(std::string("foo")), Variant::Format_Gmst, 335}, + WriteToESMTestCase {makeVariant(VT_Int, 42), Variant::Format_Gmst, 336} + )); + + struct ESMVariantWriteToESMFailTest : TestWithParam {}; + + TEST_P(ESMVariantWriteToESMFailTest, write_is_not_supported) + { + const auto param = GetParam(); + std::ostringstream out; + ESMWriter writer; + writer.save(out); + ASSERT_THROW(param.mVariant.write(writer, param.mFormat), std::runtime_error); + } + + INSTANTIATE_TEST_SUITE_P(VariantAndFormat, ESMVariantWriteToESMFailTest, Values( + WriteToESMTestCase {Variant(), Variant::Format_Global}, + WriteToESMTestCase {Variant(), Variant::Format_Info}, + WriteToESMTestCase {Variant(), Variant::Format_Local}, + WriteToESMTestCase {Variant(int{42}), Variant::Format_Gmst}, + WriteToESMTestCase {Variant(int{42}), Variant::Format_Info}, + WriteToESMTestCase {Variant(int{42}), Variant::Format_Local}, + WriteToESMTestCase {Variant(std::string("foo")), Variant::Format_Global}, + WriteToESMTestCase {Variant(std::string("foo")), Variant::Format_Info}, + WriteToESMTestCase {Variant(std::string("foo")), Variant::Format_Local}, + WriteToESMTestCase {makeVariant(VT_Unknown), Variant::Format_Global}, + WriteToESMTestCase {makeVariant(VT_Int, 42), Variant::Format_Global}, + WriteToESMTestCase {makeVariant(VT_Short, 42), Variant::Format_Gmst}, + WriteToESMTestCase {makeVariant(VT_Short, 42), Variant::Format_Info} + )); +} diff --git a/apps/openmw_test_suite/esm3/readerscache.cpp b/apps/openmw_test_suite/esm3/readerscache.cpp new file mode 100644 index 0000000000..720b66a04f --- /dev/null +++ b/apps/openmw_test_suite/esm3/readerscache.cpp @@ -0,0 +1,91 @@ +#include +#include +#include + +#include + +#ifndef OPENMW_DATA_DIR +#error "OPENMW_DATA_DIR is not defined" +#endif + +namespace +{ + using namespace testing; + using namespace ESM; + + TEST(ESM3ReadersCache, onAttemptToRequestTheSameReaderTwiceShouldThrowException) + { + ReadersCache readers(1); + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_THROW(readers.get(0), std::logic_error); + } + + TEST(ESM3ReadersCache, shouldAllowToHaveBusyItemsMoreThanCapacity) + { + ReadersCache readers(1); + const ReadersCache::BusyItem reader0 = readers.get(0); + const ReadersCache::BusyItem reader1 = readers.get(1); + } + + TEST(ESM3ReadersCache, shouldKeepClosedReleasedClosedItem) + { + ReadersCache readers(1); + readers.get(0); + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_FALSE(reader->isOpen()); + } + + struct ESM3ReadersCacheWithContentFile : Test + { + static constexpr std::size_t sInitialOffset = 324; + static constexpr std::size_t sSkip = 100; + const Files::PathContainer mDataDirs {{std::string(OPENMW_DATA_DIR)}}; + const Files::Collections mFileCollections {mDataDirs, true}; + const std::string mContentFile = "template.omwgame"; + const std::string mContentFilePath = mFileCollections.getCollection(".omwgame").getPath(mContentFile).string(); + }; + + TEST_F(ESM3ReadersCacheWithContentFile, shouldKeepOpenReleasedOpenReader) + { + ReadersCache readers(1); + { + const ReadersCache::BusyItem reader = readers.get(0); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset); + ASSERT_GT(reader->getFileSize(), sInitialOffset + sSkip); + reader->skip(sSkip); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + { + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_TRUE(reader->isOpen()); + EXPECT_EQ(reader->getName(), mContentFilePath); + EXPECT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + } + + TEST_F(ESM3ReadersCacheWithContentFile, shouldCloseFreeReaderWhenReachingCapacityLimit) + { + ReadersCache readers(1); + { + const ReadersCache::BusyItem reader = readers.get(0); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset); + ASSERT_GT(reader->getFileSize(), sInitialOffset + sSkip); + reader->skip(sSkip); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + { + const ReadersCache::BusyItem reader = readers.get(1); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + } + { + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_TRUE(reader->isOpen()); + EXPECT_EQ(reader->getFileOffset(), sInitialOffset); + } + } +} diff --git a/apps/openmw_test_suite/esm4/includes.cpp b/apps/openmw_test_suite/esm4/includes.cpp new file mode 100644 index 0000000000..3c6be55230 --- /dev/null +++ b/apps/openmw_test_suite/esm4/includes.cpp @@ -0,0 +1,89 @@ +#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 +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/apps/openmw_test_suite/esmloader/esmdata.cpp b/apps/openmw_test_suite/esmloader/esmdata.cpp new file mode 100644 index 0000000000..a697aae531 --- /dev/null +++ b/apps/openmw_test_suite/esmloader/esmdata.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct Params + { + std::string mRefId; + ESM::RecNameInts mType; + std::string mResult; + std::function mPushBack; + }; + + struct EsmLoaderGetModelTest : TestWithParam {}; + + TEST_P(EsmLoaderGetModelTest, shouldReturnFoundModelName) + { + EsmData data; + GetParam().mPushBack(data); + EXPECT_EQ(EsmLoader::getModel(data, GetParam().mRefId, GetParam().mType), GetParam().mResult); + } + + void pushBack(ESM::Activator&& value, EsmData& esmData) + { + esmData.mActivators.push_back(std::move(value)); + } + + void pushBack(ESM::Container&& value, EsmData& esmData) + { + esmData.mContainers.push_back(std::move(value)); + } + + void pushBack(ESM::Door&& value, EsmData& esmData) + { + esmData.mDoors.push_back(std::move(value)); + } + + void pushBack(ESM::Static&& value, EsmData& esmData) + { + esmData.mStatics.push_back(std::move(value)); + } + + template + struct PushBack + { + std::string mId; + std::string mModel; + + void operator()(EsmData& esmData) const + { + T value; + value.mId = mId; + value.mModel = mModel; + pushBack(std::move(value), esmData); + } + }; + + const std::array params = { + Params {"acti_ref_id", ESM::REC_ACTI, "acti_model", PushBack {"acti_ref_id", "acti_model"}}, + Params {"cont_ref_id", ESM::REC_CONT, "cont_model", PushBack {"cont_ref_id", "cont_model"}}, + Params {"door_ref_id", ESM::REC_DOOR, "door_model", PushBack {"door_ref_id", "door_model"}}, + Params {"static_ref_id", ESM::REC_STAT, "static_model", PushBack {"static_ref_id", "static_model"}}, + Params {"acti_ref_id_a", ESM::REC_ACTI, "", PushBack {"acti_ref_id_z", "acti_model"}}, + Params {"cont_ref_id_a", ESM::REC_CONT, "", PushBack {"cont_ref_id_z", "cont_model"}}, + Params {"door_ref_id_a", ESM::REC_DOOR, "", PushBack {"door_ref_id_z", "door_model"}}, + Params {"static_ref_id_a", ESM::REC_STAT, "", PushBack {"static_ref_id_z", "static_model"}}, + Params {"acti_ref_id_z", ESM::REC_ACTI, "", PushBack {"acti_ref_id_a", "acti_model"}}, + Params {"cont_ref_id_z", ESM::REC_CONT, "", PushBack {"cont_ref_id_a", "cont_model"}}, + Params {"door_ref_id_z", ESM::REC_DOOR, "", PushBack {"door_ref_id_a", "door_model"}}, + Params {"static_ref_id_z", ESM::REC_STAT, "", PushBack {"static_ref_id_a", "static_model"}}, + Params {"ref_id", ESM::REC_STAT, "", [] (EsmData&) {}}, + Params {"ref_id", ESM::REC_BOOK, "", [] (EsmData&) {}}, + }; + + INSTANTIATE_TEST_SUITE_P(Params, EsmLoaderGetModelTest, ValuesIn(params)); + + TEST(EsmLoaderGetGameSettingTest, shouldReturnFoundValue) + { + std::vector settings; + ESM::GameSetting setting; + setting.mId = "setting"; + setting.mValue = ESM::Variant(42); + settings.push_back(setting); + EXPECT_EQ(EsmLoader::getGameSetting(settings, "setting"), ESM::Variant(42)); + } + + TEST(EsmLoaderGetGameSettingTest, shouldThrowExceptionWhenNotFound) + { + const std::vector settings; + EXPECT_THROW(EsmLoader::getGameSetting(settings, "setting"), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/esmloader/load.cpp b/apps/openmw_test_suite/esmloader/load.cpp new file mode 100644 index 0000000000..ea090466a3 --- /dev/null +++ b/apps/openmw_test_suite/esmloader/load.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifndef OPENMW_DATA_DIR +#error "OPENMW_DATA_DIR is not defined" +#endif + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct EsmLoaderTest : Test + { + const Files::PathContainer mDataDirs {{std::string(OPENMW_DATA_DIR)}}; + const Files::Collections mFileCollections {mDataDirs, true}; + const std::vector mContentFiles {{"template.omwgame"}}; + }; + + TEST_F(EsmLoaderTest, loadEsmDataShouldSupportOmwgame) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsWhenQueryLoadCellsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = false; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsGameSettingsWhenQueryLoadGameSettingsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = false; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreAllWithDefaultQuery) + { + const Query query; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } + + TEST_F(EsmLoaderTest, loadEsmDataShouldSkipUnsupportedFormats) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const std::vector contentFiles {{"script.omwscripts"}}; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, contentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } +} diff --git a/apps/openmw_test_suite/esmloader/record.cpp b/apps/openmw_test_suite/esmloader/record.cpp new file mode 100644 index 0000000000..fbbbdf18d8 --- /dev/null +++ b/apps/openmw_test_suite/esmloader/record.cpp @@ -0,0 +1,105 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct Value + { + int mKey; + int mValue; + }; + + auto tie(const Value& v) + { + return std::tie(v.mKey, v.mValue); + } + + bool operator==(const Value& l, const Value& r) + { + return tie(l) == tie(r); + } + + std::ostream& operator<<(std::ostream& s, const Value& v) + { + return s << "Value {" << v.mKey << ", " << v.mValue << "}"; + } + + Record present(const Value& v) + { + return Record(false, v); + } + + Record deleted(const Value& v) + { + return Record(true, v); + } + + struct Params + { + Records mRecords; + std::vector mResult; + }; + + struct EsmLoaderPrepareRecordTest : TestWithParam {}; + + TEST_P(EsmLoaderPrepareRecordTest, prepareRecords) + { + auto records = GetParam().mRecords; + const auto getKey = [&] (const Record& v) { return v.mValue.mKey; }; + EXPECT_THAT(prepareRecords(records, getKey), ElementsAreArray(GetParam().mResult)); + } + + const std::array params = { + Params {{}, {}}, + Params { + {present(Value {1, 1})}, + {Value {1, 1}} + }, + Params { + {deleted(Value {1, 1})}, + {} + }, + Params { + {present(Value {1, 1}), present(Value {2, 2})}, + {Value {1, 1}, Value {2, 2}} + }, + Params { + {present(Value {2, 2}), present(Value {1, 1})}, + {Value {1, 1}, Value {2, 2}} + }, + Params { + {present(Value {1, 1}), present(Value {1, 2})}, + {Value {1, 2}} + }, + Params { + {present(Value {1, 2}), present(Value {1, 1})}, + {Value {1, 1}} + }, + Params { + {present(Value {1, 1}), deleted(Value {1, 2})}, + {} + }, + Params { + {deleted(Value {1, 1}), present(Value {1, 2})}, + {Value {1, 2}} + }, + Params { + {present(Value {1, 2}), deleted(Value {1, 1})}, + {} + }, + Params { + {deleted(Value {1, 2}), present(Value {1, 1})}, + {Value {1, 1}} + }, + }; + + INSTANTIATE_TEST_SUITE_P(Params, EsmLoaderPrepareRecordTest, ValuesIn(params)); +} diff --git a/apps/openmw_test_suite/files/hash.cpp b/apps/openmw_test_suite/files/hash.cpp new file mode 100644 index 0000000000..f8303ac1f3 --- /dev/null +++ b/apps/openmw_test_suite/files/hash.cpp @@ -0,0 +1,69 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + using namespace Files; + + struct Params + { + std::size_t mSize; + std::array mHash; + }; + + struct FilesGetHash : TestWithParam {}; + + TEST(FilesGetHash, shouldClearErrors) + { + const std::string fileName = temporaryFilePath("fileName"); + std::string content; + 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)); + } + + TEST_P(FilesGetHash, shouldReturnHashForStringStream) + { + const std::string fileName = temporaryFilePath("fileName"); + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + std::istringstream stream(content); + EXPECT_EQ(getHash(fileName, stream), GetParam().mHash); + } + + TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream) + { + std::string fileName(UnitTest::GetInstance()->current_test_info()->name()); + std::replace(fileName.begin(), fileName.end(), '/', '_'); + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + fileName = outputFilePath(fileName); + std::fstream(fileName, std::ios_base::out | std::ios_base::binary) + .write(content.data(), static_cast(content.size())); + const auto stream = Files::openConstrainedFileStream(fileName, 0, content.size()); + EXPECT_EQ(getHash(fileName, *stream), GetParam().mHash); + } + + INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash, Values( + Params {0, {0, 0}}, + Params {1, {9607679276477937801ull, 16624257681780017498ull}}, + Params {128, {15287858148353394424ull, 16818615825966581310ull}}, + Params {1000, {11018119256083894017ull, 6631144854802791578ull}}, + Params {4096, {11972283295181039100ull, 16027670129106775155ull}}, + Params {4097, {16717956291025443060ull, 12856404199748778153ull}}, + Params {5000, {15775925571142117787ull, 10322955217889622896ull}} + )); +} diff --git a/apps/openmw_test_suite/fx/lexer.cpp b/apps/openmw_test_suite/fx/lexer.cpp new file mode 100644 index 0000000000..5024622a71 --- /dev/null +++ b/apps/openmw_test_suite/fx/lexer.cpp @@ -0,0 +1,216 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace fx::Lexer; + + struct LexerTest : Test {}; + + struct LexerSingleTokenTest : Test + { + template + void test() + { + const std::string content = std::string(Token::repr); + Lexer lexer(content); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + } + }; + + TEST_F(LexerSingleTokenTest, single_token_shared) { test(); } + TEST_F(LexerSingleTokenTest, single_token_technique) { test(); } + TEST_F(LexerSingleTokenTest, single_token_main_pass) { test(); } + TEST_F(LexerSingleTokenTest, single_token_render_target) { test(); } + TEST_F(LexerSingleTokenTest, single_token_vertex) { test(); } + TEST_F(LexerSingleTokenTest, single_token_fragment) { test(); } + TEST_F(LexerSingleTokenTest, single_token_compute) { test(); } + TEST_F(LexerSingleTokenTest, single_token_sampler_1d) { test(); } + TEST_F(LexerSingleTokenTest, single_token_sampler_2d) { test(); } + TEST_F(LexerSingleTokenTest, single_token_sampler_3d) { test(); } + TEST_F(LexerSingleTokenTest, single_token_true) { test(); } + TEST_F(LexerSingleTokenTest, single_token_false) { test(); } + TEST_F(LexerSingleTokenTest, single_token_vec2) { test(); } + TEST_F(LexerSingleTokenTest, single_token_vec3) { test(); } + TEST_F(LexerSingleTokenTest, single_token_vec4) { test(); } + + TEST(LexerTest, peek_whitespace_only_content_should_be_eof) + { + Lexer lexer(R"( + + )"); + + EXPECT_TRUE(std::holds_alternative(lexer.peek())); + } + + TEST(LexerTest, float_with_no_prefixed_digits) + { + Lexer lexer(R"( + 0.123; + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 0.123f); + } + + TEST(LexerTest, float_with_alpha_prefix) + { + Lexer lexer(R"( + abc.123; + )"); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 0.123f); + } + + TEST(LexerTest, float_with_numeric_prefix) + { + Lexer lexer(R"( + 123.123; + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 123.123f); + } + + TEST(LexerTest, int_should_not_be_float) + { + Lexer lexer(R"( + 123 + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_EQ(std::get(token).value, 123); + } + + TEST(LexerTest, simple_string) + { + Lexer lexer(R"( + "test string" + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + + std::string parsed = std::string(std::get(token).value); + EXPECT_EQ("test string", parsed); + } + + TEST(LexerTest, fail_on_unterminated_double_quotes) + { + Lexer lexer(R"( + "unterminated string' + )"); + + EXPECT_THROW(lexer.next(), LexerException); + } + + TEST(LexerTest, multiline_strings_with_single_quotes) + { + Lexer lexer(R"( + "string that is + on multiple with 'single quotes' + and correctly terminated!" + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + } + + TEST(LexerTest, fail_on_unterminated_double_quotes_with_multiline_strings) + { + Lexer lexer(R"( + "string that is + on multiple with 'single quotes' + and but is unterminated :( + )"); + + EXPECT_THROW(lexer.next(), LexerException); + } + + TEST(LexerTest, jump_with_single_nested_bracket) + { + const std::string content = R"( + #version 120 + + void main() + { + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, jump_with_single_line_comments_and_mismatching_brackets) + { + const std::string content = R"( + #version 120 + + void main() + { + // } + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, jump_with_multi_line_comments_and_mismatching_brackets) + { + const std::string content = R"( + #version 120 + + void main() + { + /* + } + */ + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, immediate_closed_blocks) + { + Lexer lexer(R"(block{})"); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + EXPECT_TRUE(std::holds_alternative(lexer.next())); + auto block = lexer.jump(); + EXPECT_TRUE(block.has_value()); + EXPECT_TRUE(block.value().empty()); + EXPECT_TRUE(std::holds_alternative(lexer.next())); + } + +} diff --git a/apps/openmw_test_suite/fx/technique.cpp b/apps/openmw_test_suite/fx/technique.cpp new file mode 100644 index 0000000000..3273d69eb2 --- /dev/null +++ b/apps/openmw_test_suite/fx/technique.cpp @@ -0,0 +1,204 @@ +#include "gmock/gmock.h" +#include + +#include +#include +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + +TestingOpenMW::VFSTestFile technique_properties(R"( + fragment main {} + vertex main {} + technique { + passes = main; + version = "0.1a"; + description = "description"; + author = "author"; + glsl_version = 330; + glsl_profile = "compatability"; + glsl_extensions = GL_EXT_gpu_shader4, GL_ARB_uniform_buffer_object; + flags = disable_sunglare; + hdr = true; + } +)"); + +TestingOpenMW::VFSTestFile rendertarget_properties{R"( + render_target rendertarget { + width_ratio = 0.5; + height_ratio = 0.5; + internal_format = r16f; + source_type = float; + source_format = red; + mipmaps = true; + wrap_s = clamp_to_edge; + wrap_t = repeat; + min_filter = linear; + mag_filter = nearest; + } + fragment downsample2x(target=rendertarget) { + + omw_In vec2 omw_TexCoord; + + void main() + { + omw_FragColor.r = omw_GetLastShader(omw_TexCoord).r; + } + } + fragment main { } + technique { passes = downsample2x, main; } +)"}; + + +TestingOpenMW::VFSTestFile uniform_properties{R"( + uniform_vec4 uVec4 { + default = vec4(0,0,0,0); + min = vec4(0,1,0,0); + max = vec4(0,0,1,0); + step = 0.5; + header = "header"; + static = true; + description = "description"; + } + fragment main { } + technique { passes = main; } +)"}; + + +TestingOpenMW::VFSTestFile missing_sampler_source{R"( + sampler_1d mysampler1d { } + fragment main { } + technique { passes = main; } +)"}; + +TestingOpenMW::VFSTestFile repeated_shared_block{R"( + shared { + float myfloat = 1.0; + } + shared {} + fragment main { } + technique { passes = main; } +)"}; + + + using namespace testing; + using namespace fx; + + struct TechniqueTest : Test + { + std::unique_ptr mVFS; + Resource::ImageManager mImageManager; + std::unique_ptr mTechnique; + + 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}, + })) + , mImageManager(mVFS.get()) + { + Settings::Manager::setBool("radial fog", "Fog", true); + Settings::Manager::setBool("stereo enabled", "Stereo", false); + } + + void compile(const std::string& name) + { + mTechnique = std::make_unique(*mVFS.get(), mImageManager, name, 1, 1, true, true); + mTechnique->compile(); + } + }; + + TEST_F(TechniqueTest, technique_properties) + { + std::unordered_set targetExtensions = { + "GL_EXT_gpu_shader4", + "GL_ARB_uniform_buffer_object" + }; + + compile("technique_properties"); + + EXPECT_EQ(mTechnique->getVersion(), "0.1a"); + EXPECT_EQ(mTechnique->getDescription(), "description"); + EXPECT_EQ(mTechnique->getAuthor(), "author"); + EXPECT_EQ(mTechnique->getGLSLVersion(), 330); + EXPECT_EQ(mTechnique->getGLSLProfile(), "compatability"); + EXPECT_EQ(mTechnique->getGLSLExtensions(), targetExtensions); + EXPECT_EQ(mTechnique->getFlags(), Technique::Flag_Disable_SunGlare); + EXPECT_EQ(mTechnique->getHDR(), true); + EXPECT_EQ(mTechnique->getPasses().size(), 1); + EXPECT_EQ(mTechnique->getPasses().front()->getName(), "main"); + } + + TEST_F(TechniqueTest, rendertarget_properties) + { + compile("rendertarget_properties"); + + EXPECT_EQ(mTechnique->getRenderTargetsMap().size(), 1); + + const std::string_view name = mTechnique->getRenderTargetsMap().begin()->first; + auto& rt = mTechnique->getRenderTargetsMap().begin()->second; + auto& texture = rt.mTarget; + + EXPECT_EQ(name, "rendertarget"); + EXPECT_EQ(rt.mMipMap, true); + EXPECT_EQ(rt.mSize.mWidthRatio, 0.5f); + EXPECT_EQ(rt.mSize.mHeightRatio, 0.5f); + EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_S), osg::Texture::CLAMP_TO_EDGE); + EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_T), osg::Texture::REPEAT); + EXPECT_EQ(texture->getFilter(osg::Texture::MIN_FILTER), osg::Texture::LINEAR); + EXPECT_EQ(texture->getFilter(osg::Texture::MAG_FILTER), osg::Texture::NEAREST); + EXPECT_EQ(texture->getSourceType(), static_cast(GL_FLOAT)); + EXPECT_EQ(texture->getSourceFormat(), static_cast(GL_RED)); + EXPECT_EQ(texture->getInternalFormat(), static_cast(GL_R16F)); + + EXPECT_EQ(mTechnique->getPasses().size(), 2); + EXPECT_EQ(mTechnique->getPasses()[0]->getTarget(), "rendertarget"); + } + + TEST_F(TechniqueTest, uniform_properties) + { + compile("uniform_properties"); + + EXPECT_EQ(mTechnique->getUniformMap().size(), 1); + + const auto& uniform = mTechnique->getUniformMap().front(); + + EXPECT_TRUE(uniform->mStatic); + EXPECT_DOUBLE_EQ(uniform->mStep, 0.5); + EXPECT_EQ(uniform->getDefault(), osg::Vec4f(0,0,0,0)); + EXPECT_EQ(uniform->getMin(), osg::Vec4f(0,1,0,0)); + EXPECT_EQ(uniform->getMax(), osg::Vec4f(0,0,1,0)); + EXPECT_EQ(uniform->mHeader, "header"); + EXPECT_EQ(uniform->mDescription, "description"); + EXPECT_EQ(uniform->mName, "uVec4"); + } + + TEST_F(TechniqueTest, fail_with_missing_source_for_sampler) + { + internal::CaptureStdout(); + + compile("missing_sampler_source"); + + std::string output = internal::GetCapturedStdout(); + Log(Debug::Error) << output; + EXPECT_THAT(output, HasSubstr("sampler_1d 'mysampler1d' requires a filename")); + } + + TEST_F(TechniqueTest, fail_with_repeated_shared_block) + { + internal::CaptureStdout(); + + compile("repeated_shared_block"); + + std::string output = internal::GetCapturedStdout(); + Log(Debug::Error) << output; + EXPECT_THAT(output, HasSubstr("repeated 'shared' block")); + } +} \ No newline at end of file diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp new file mode 100644 index 0000000000..39c4f06201 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -0,0 +1,243 @@ +#include "gmock/gmock.h" +#include + +#include + +#include +#include +#include +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + + using testing::ElementsAre; + using testing::Pair; + + std::vector> asVector(const LuaUtil::ScriptIdsWithInitializationData& d) + { + std::vector> res; + for (const auto& [k, v] : d) + res.emplace_back(k, std::string(v)); + return res; + } + + TEST(LuaConfigurationTest, ValidOMWScripts) + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( + # Lines starting with '#' are comments + GLOBAL: my_mod/#some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER :my_mod/player.lua + CUSTOM : my_mod/some_other_script.lua + NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCREATURE,CUSTOM: my_mod/creature.lua\r\n"); + + ASSERT_EQ(cfg.mScripts.size(), 6); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); + 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[5]), "CUSTOM CREATURE : my_mod/creature.lua"); + + LuaUtil::ScriptsConfiguration conf; + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 4); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); + // 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[3]), "CUSTOM CREATURE : my_mod/creature.lua"); + + EXPECT_THAT(asVector(conf.getGlobalConf()), ElementsAre(Pair(0, ""))); + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "something", ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "something", ESM::RefNum())), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "something", ESM::RefNum())), ElementsAre(Pair(1, ""), Pair(3, ""))); + + // Check that initialization cleans old data + cfg = ESM::LuaScriptsCfg(); + conf.init(std::move(cfg)); + EXPECT_EQ(conf.size(), 0); + } + + TEST(LuaConfigurationTest, InvalidOMWScripts) + { + ESM::LuaScriptsCfg cfg; + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), + "Lua script should have suffix '.lua', got: GLOBAL: something"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), + "No flags found in: something.lua"); + + cfg.mScripts.clear(); + EXPECT_NO_THROW(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua")); + LuaUtil::ScriptsConfiguration conf; + EXPECT_ERROR(conf.init(std::move(cfg)), "Global script can not have local flags"); + } + + TEST(LuaConfigurationTest, ConfInit) + { + ESM::LuaScriptsCfg cfg; + ESM::LuaScriptCfg& script1 = cfg.mScripts.emplace_back(); + script1.mScriptPath = "Script1.lua"; + script1.mInitializationData = "data1"; + script1.mFlags = ESM::LuaScriptCfg::sPlayer; + script1.mTypes.push_back(ESM::REC_CREA); + script1.mRecords.push_back({true, "record1", "dataRecord1"}); + script1.mRefs.push_back({true, 2, 3, ""}); + script1.mRefs.push_back({true, 2, 4, ""}); + + ESM::LuaScriptCfg& script2 = cfg.mScripts.emplace_back(); + script2.mScriptPath = "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.mFlags = ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sMerge; + script1Extra.mTypes.push_back(ESM::REC_NPC_); + script1Extra.mRecords.push_back({false, "rat", ""}); + script1Extra.mRecords.push_back({true, "record2", ""}); + script1Extra.mRefs.push_back({true, 3, 5, "dataRef35"}); + script1Extra.mRefs.push_back({false, 2, 3, ""}); + + LuaUtil::ScriptsConfiguration conf; + 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"); + + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "something", ESM::RefNum())), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "guar", ESM::RefNum())), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "rat", ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, "record1", ESM::RefNum())), ElementsAre(Pair(0, "dataRecord1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, "record2", ESM::RefNum())), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {1, 1})), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {2, 3})), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {3, 5})), ElementsAre(Pair(0, "dataRef35"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "record4", {2, 4})), ElementsAre(Pair(0, "data1"), Pair(1, ""))); + + ESM::LuaScriptCfg& script3 = cfg.mScripts.emplace_back(); + script3.mScriptPath = "script1.lua"; + script3.mFlags = ESM::LuaScriptCfg::sGlobal; + EXPECT_ERROR(conf.init(cfg), "Flags mismatch for script1.lua"); + } + + TEST(LuaConfigurationTest, Serialization) + { + sol::state lua; + LuaUtil::BasicSerializer serializer; + + ESM::ESMWriter writer; + writer.setAuthor(""); + writer.setDescription(""); + writer.setRecordCount(1); + writer.setFormat(ESM::Header::CurrentFormat); + writer.setVersion(); + writer.addMaster("morrowind.esm", 0); + + ESM::LuaScriptsCfg cfg; + std::string luaData; + { + sol::table data(lua, sol::create); + data["number"] = 5; + data["string"] = "some value"; + data["fargoth"] = ESM::RefNum{128964, 1}; + luaData = LuaUtil::serialize(data, &serializer); + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = "test_global.lua"; + script.mFlags = ESM::LuaScriptCfg::sGlobal; + script.mInitializationData = luaData; + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = "test_local.lua"; + script.mFlags = ESM::LuaScriptCfg::sMerge; + script.mTypes.push_back(ESM::REC_DOOR); + script.mTypes.push_back(ESM::REC_MISC); + script.mRecords.push_back({true, "rat", luaData}); + script.mRecords.push_back({false, "chargendoorjournal", ""}); + script.mRefs.push_back({true, 128964, 1, ""}); + script.mRefs.push_back({true, 128962, 1, luaData}); + } + + std::stringstream stream; + writer.save(stream); + writer.startRecord(ESM::REC_LUAL); + cfg.save(writer); + writer.endRecord(ESM::REC_LUAL); + writer.close(); + std::string serializedOMWAddon = stream.str(); + + { + // Save for manual testing. + std::ofstream f(TestingOpenMW::outputFilePath("lua_conf_test.omwaddon"), std::ios::binary); + f << serializedOMWAddon; + f.close(); + } + + ESM::ESMReader reader; + reader.open(std::make_unique(serializedOMWAddon), "lua_conf_test.omwaddon"); + ASSERT_EQ(reader.getRecordCount(), 1); + ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_LUAL); + reader.getRecHeader(); + ESM::LuaScriptsCfg loadedCfg; + loadedCfg.load(reader); + + ASSERT_EQ(loadedCfg.mScripts.size(), cfg.mScripts.size()); + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + EXPECT_EQ(loadedCfg.mScripts[i].mScriptPath, cfg.mScripts[i].mScriptPath); + EXPECT_EQ(loadedCfg.mScripts[i].mFlags, cfg.mScripts[i].mFlags); + EXPECT_EQ(loadedCfg.mScripts[i].mInitializationData, cfg.mScripts[i].mInitializationData); + ASSERT_EQ(loadedCfg.mScripts[i].mTypes.size(), cfg.mScripts[i].mTypes.size()); + for (size_t j = 0; j < cfg.mScripts[i].mTypes.size(); ++j) + EXPECT_EQ(loadedCfg.mScripts[i].mTypes[j], cfg.mScripts[i].mTypes[j]); + ASSERT_EQ(loadedCfg.mScripts[i].mRecords.size(), cfg.mScripts[i].mRecords.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRecords.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mAttach, cfg.mScripts[i].mRecords[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mRecordId, cfg.mScripts[i].mRecords[j].mRecordId); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mInitializationData, cfg.mScripts[i].mRecords[j].mInitializationData); + } + ASSERT_EQ(loadedCfg.mScripts[i].mRefs.size(), cfg.mScripts[i].mRefs.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRefs.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mAttach, cfg.mScripts[i].mRefs[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mRefnumIndex, cfg.mScripts[i].mRefs[j].mRefnumIndex); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mRefnumContentFile, cfg.mScripts[i].mRefs[j].mRefnumContentFile); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mInitializationData, cfg.mScripts[i].mRefs[j].mInitializationData); + } + } + + { + ESM::ReadersCache readers(4); + readers.get(0)->openRaw(std::make_unique("dummyData"), "a.omwaddon"); + readers.get(1)->openRaw(std::make_unique("dummyData"), "b.omwaddon"); + readers.get(2)->openRaw(std::make_unique("dummyData"), "Morrowind.esm"); + readers.get(3)->openRaw(std::make_unique("dummyData"), "c.omwaddon"); + reader.setIndex(3); + reader.resolveParentFileIndices(readers); + } + loadedCfg.adjustRefNums(reader); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumIndex, cfg.mScripts[1].mRefs[0].mRefnumIndex); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumContentFile, 2); + { + sol::table data = LuaUtil::deserialize(lua.lua_state(), loadedCfg.mScripts[1].mRefs[1].mInitializationData, &serializer); + ESM::RefNum adjustedRef = data["fargoth"].get(); + EXPECT_EQ(adjustedRef.mIndex, 128964u); + EXPECT_EQ(adjustedRef.mContentFile, 2); + } + } +} diff --git a/apps/openmw_test_suite/lua/test_l10n.cpp b/apps/openmw_test_suite/lua/test_l10n.cpp new file mode 100644 index 0000000000..6204a8022a --- /dev/null +++ b/apps/openmw_test_suite/lua/test_l10n.cpp @@ -0,0 +1,164 @@ +#include "gmock/gmock.h" +#include + +#include + +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + template + T get(sol::state& lua, const std::string& luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + 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({ + {"l10n/Test1/en.yaml", &test1En}, + {"l10n/Test1/en_US.yaml", &test1EnUS}, + {"l10n/Test1/de.yaml", &test1De}, + {"l10n/Test2/en.yaml", &test2En}, + {"l10n/Test3/en.yaml", &test1En}, + {"l10n/Test3/de.yaml", &test1De}, + }); + + LuaUtil::ScriptsConfiguration mCfg; + }; + + TEST_F(LuaL10nTest, L10n) + { + internal::CaptureStdout(); + LuaUtil::LuaState lua{mVFS.get(), &mCfg}; + sol::state& l = lua.sol(); + LuaUtil::L10nManager l10n(mVFS.get(), &lua); + l10n.init(); + l10n.setPreferredLocales({"de", "en"}); + EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n"); + + internal::CaptureStdout(); + l["t1"] = l10n.getContext("Test1"); + EXPECT_THAT(internal::GetCapturedStdout(), + "Fallback locale: en\n" + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n"); + + internal::CaptureStdout(); + l["t2"] = l10n.getContext("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(); + l10n.setPreferredLocales({"en", "de"}); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: 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. + l10n.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(); + l10n.setPreferredLocales({"en-GB-oed", "de"}); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: 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["t3"] = l10n.getContext("Test3", "de"); + l10n.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/openmw_test_suite/lua/test_lua.cpp new file mode 100644 index 0000000000..566fefa726 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -0,0 +1,178 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + + TestingOpenMW::VFSTestFile counterFile(R"X( +x = 42 +return { + get = function() return x end, + inc = function(v) x = x + v end +} +)X"); + + TestingOpenMW::VFSTestFile invalidScriptFile("Invalid script"); + + TestingOpenMW::VFSTestFile testsFile(R"X( +return { + -- should work + sin = function(x) return math.sin(x) end, + requireMathSin = function(x) return require('math').sin(x) end, + useCounter = function() + local counter = require('aaa.counter') + counter.inc(1) + return counter.get() + end, + callRawset = function() + t = {a = 1, b = 2} + rawset(t, 'b', 3) + return t.b + end, + print = print, + + -- should throw an error + incorrectRequire = function() require('counter') end, + modifySystemLib = function() math.sin = 5 end, + modifySystemLib2 = function() math.__index.sin = 5 end, + rawsetSystemLib = function() rawset(math, 'sin', 5) end, + callLoadstring = function() loadstring('print(1)') end, + setSqr = function() require('sqrlib').sqr = math.sin end, + setOmwName = function() require('openmw').name = 'abc' end, + + -- should work if API is registered + sqr = function(x) return require('sqrlib').sqr(x) end, + apiName = function() return require('test.api').name end +} +)X"); + + struct LuaStateTest : Test + { + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ + {"aaa/counter.lua", &counterFile}, + {"bbb/tests.lua", &testsFile}, + {"invalid.lua", &invalidScriptFile} + }); + + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + }; + + TEST_F(LuaStateTest, Sandbox) + { + sol::table script1 = mLua.runInNewSandbox("aaa/counter.lua"); + + 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"); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 42); + LuaUtil::call(script2["inc"], 1); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 43); + + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); + } + + 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::nil), "nil"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), "something")), "\"something\""); + } + + TEST_F(LuaStateTest, ErrorHandling) + { + EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:"); + } + + TEST_F(LuaStateTest, CustomRequire) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + + EXPECT_FLOAT_EQ(LuaUtil::call(script["sin"], 1).get(), + -LuaUtil::call(script["requireMathSin"], -1).get()); + + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 43); + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 44); + { + sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua"); + EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get(), 43); + } + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 45); + + EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "module not found: counter"); + } + + TEST_F(LuaStateTest, ReadOnly) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + + // rawset itself is allowed + EXPECT_EQ(LuaUtil::call(script["callRawset"]).get(), 3); + + // but read-only object can not be modified even with rawset + EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib2"]), "a nil value"); + + EXPECT_EQ(LuaUtil::getMutableFromReadOnly(LuaUtil::makeReadOnly(script)), script); + } + + TEST_F(LuaStateTest, Print) + { + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + 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"); + } + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua", "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"); + } + } + + TEST_F(LuaStateTest, UnsafeFunction) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + EXPECT_ERROR(LuaUtil::call(script["callLoadstring"]), "a nil value"); + } + + TEST_F(LuaStateTest, ProvideAPI) + { + LuaUtil::LuaState lua(mVFS.get(), &mCfg); + + sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); + sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); + + sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api1}}); + + lua.addCommonPackage( + "sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; })); + + sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api2}}); + + 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"); + } + + TEST_F(LuaStateTest, GetLuaVersion) + { + EXPECT_THAT(LuaUtil::getLuaVersion(), HasSubstr("Lua")); + } + +} diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp new file mode 100644 index 0000000000..84b632913c --- /dev/null +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -0,0 +1,471 @@ +#include "gmock/gmock.h" +#include + +#include + +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + VFSTestFile invalidScript("not a script"); + VFSTestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + VFSTestFile emptyScript(""); + + VFSTestFile testScript(R"X( +return { + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, + eventHandlers = { + Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, + Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, + Print = function() print('print') end + } +} +)X"); + + VFSTestFile stopEventScript(R"X( +return { + eventHandlers = { + Event1 = function(eventData) + print(' event1 ' .. tostring(eventData.x)) + return eventData.x >= 1 + end + } +} +)X"); + + VFSTestFile loadSaveScript(R"X( +x = 0 +y = 0 +return { + engineHandlers = { + onSave = function(state) + return {x = x, y = y} + end, + onLoad = function(state) + x, y = state.x, state.y + end + }, + eventHandlers = { + Set = function(eventData) + eventData.n = eventData.n - 1 + if eventData.n == 0 then + x, y = eventData.x, eventData.y + end + end, + Print = function() + print(x, y) + end + } +} +)X"); + + VFSTestFile interfaceScript(R"X( +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) print('FN', x) end, + value = 3.5 + }, +} +)X"); + + VFSTestFile overrideInterfaceScript(R"X( +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} +return { + interfaceName = "TestInterface", + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end + }, +} +)X"); + + VFSTestFile useInterfaceScript(R"X( +local interfaces = require('openmw.interfaces') +return { + engineHandlers = { + onUpdate = function() + interfaces.TestInterface.fn(interfaces.TestInterface.value) + 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}, + }); + + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( +CUSTOM: invalid.lua +CUSTOM: incorrect.lua +CUSTOM: empty.lua +CUSTOM: test1.lua +CUSTOM: stopEvent.lua +CUSTOM: test2.lua +NPC: loadSave1.lua +CUSTOM, NPC: loadSave2.lua +CUSTOM, PLAYER: testInterface.lua +CUSTOM, PLAYER: overrideInterface.lua +CUSTOM, PLAYER: useInterface.lua +)X"); + mCfg.init(std::move(cfg)); + } + }; + + TEST_F(LuaScriptsContainerTest, VerifyStructure) + { + 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(), ""); + } + } + + 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"))); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n"); + } + + 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)); + + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("SomeEvent", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test has received event 'SomeEvent', but there are no handlers for this event\n"); + } + { + testing::internal::CaptureStdout(); + 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[test1.lua]:\t event1 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 1.5\n" + "Test[test1.lua]:\t event2 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopEvent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 0.5\n" + "Test[test1.lua]:\t event2 0.5\n"); + } + } + + 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)); + + { + testing::internal::CaptureStdout(); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "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"); + } + { + testing::internal::CaptureStdout(); + int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "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[test1.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.removeScript(*mCfg.findId("test1.lua")); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, AutoStart) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getPlayerConf()); + testing::internal::CaptureStdout(); + 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_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getLocalConf(ESM::REC_CREA, "", ESM::RefNum())); + int addIfaceId = *mCfg.findId("testInterface.lua"); + int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); + int useIfaceId = *mCfg.findId("useInterface.lua"); + + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + 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_F(LuaScriptsContainerTest, LoadSave) + { + LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + scripts1.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, "", ESM::RefNum())); + scripts2.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, "", ESM::RefNum())); + scripts3.setAutoStartConf(mCfg.getPlayerConf()); + + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); + + 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))); + + ESM::LuaScripts data; + scripts1.save(data); + + { + testing::internal::CaptureStdout(); + scripts2.load(data); + 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[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); + } + { + testing::internal::CaptureStdout(); + scripts3.load(data); + scripts3.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "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[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); + } + } + + TEST_F(LuaScriptsContainerTest, Timers) + { + using TimerType = LuaUtil::ScriptsContainer::TimerType; + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + int test1Id = *mCfg.findId("test1.lua"); + int test2Id = *mCfg.findId("test2.lua"); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + 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; }); + + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); + + 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.setupUnsavableTimer(TimerType::SIMULATION_TIME, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 15, test2Id, fn1); + + EXPECT_EQ(counter1, 0); + EXPECT_EQ(counter3, 0); + + scripts.processTimers(6, 4); + + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 0); + + scripts.processTimers(6, 8); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 0); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 2); + + scripts.processTimers(11, 12); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 5); + + testing::internal::CaptureStdout(); + ESM::LuaScripts data; + scripts.save(data); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); + + testing::internal::CaptureStdout(); + scripts.processTimers(20, 20); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua] callTimer failed: Callback 'A' doesn't exist\n"); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 25); + } + + TEST_F(LuaScriptsContainerTest, CallbackWrapper) + { + LuaUtil::Callback callback{mLua.sol()["print"], mLua.newTable()}; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua"; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptsContainer::ScriptId{nullptr, 0}; + + testing::internal::CaptureStdout(); + callback.call(1.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\n"); + + testing::internal::CaptureStdout(); + callback.call(1.5, 2.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\t2.5\n"); + + 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"); + } + +} diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/openmw_test_suite/lua/test_serialization.cpp new file mode 100644 index 0000000000..a74081266f --- /dev/null +++ b/apps/openmw_test_suite/lua/test_serialization.cpp @@ -0,0 +1,274 @@ +#include "gmock/gmock.h" +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + + TEST(LuaSerializationTest, Nil) + { + sol::state lua; + EXPECT_EQ(LuaUtil::serialize(sol::nil), ""); + EXPECT_EQ(LuaUtil::deserialize(lua, ""), sol::nil); + } + + TEST(LuaSerializationTest, Number) + { + sol::state lua; + std::string serialized = LuaUtil::serialize(sol::make_object(lua, 3.14)); + EXPECT_EQ(serialized.size(), 10); // version, type, 8 bytes value + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_DOUBLE_EQ(value.as(), 3.14); + } + + TEST(LuaSerializationTest, Boolean) + { + sol::state lua; + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, true)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_TRUE(value.as()); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, false)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_FALSE(value.as()); + } + } + + TEST(LuaSerializationTest, String) + { + sol::state lua; + std::string_view emptyString = ""; + std::string_view shortString = "abc"; + std::string_view longString = "It is a string with more than 32 characters..........................."; + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, emptyString)); + EXPECT_EQ(serialized.size(), 2); // version, type + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), emptyString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, shortString)); + EXPECT_EQ(serialized.size(), 2 + shortString.size()); // version, type, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), shortString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, longString)); + EXPECT_EQ(serialized.size(), 6 + longString.size()); // version, type, size, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), longString); + } + } + + TEST(LuaSerializationTest, Vector) + { + sol::state lua; + osg::Vec2f vec2(1, 2); + osg::Vec3f vec3(1, 2, 3); + osg::Vec4f vec4(1, 2, 3, 4); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec2)); + EXPECT_EQ(serialized.size(), 18); // version, type, 2x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec2); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec3)); + EXPECT_EQ(serialized.size(), 26); // version, type, 3x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec3); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec4)); + EXPECT_EQ(serialized.size(), 34); // version, type, 4x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec4); + } + } + + TEST(LuaSerializationTest, Color) + { + sol::state lua; + Misc::Color color(1, 1, 1, 1); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, color)); + EXPECT_EQ(serialized.size(), 18); // version, type, 4x float + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), color); + } + } + + TEST(LuaSerializationTest, Transform) { + sol::state lua; + osg::Matrixf matrix(1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16); + LuaUtil::TransformM transM = LuaUtil::asTransform(matrix); + osg::Quat quat(1, 2, 3, 4); + LuaUtil::TransformQ transQ = LuaUtil::asTransform(quat); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, transM)); + EXPECT_EQ(serialized.size(), 130); // version, type, 16x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as().mM, transM.mM); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, transQ)); + EXPECT_EQ(serialized.size(), 34); // version, type, 4x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as().mQ, transQ.mQ); + } + + } + + TEST(LuaSerializationTest, Table) + { + sol::state lua; + sol::table table(lua, sol::create); + table["aa"] = 1; + table["ab"] = true; + table["nested"] = sol::table(lua, sol::create); + table["nested"]["aa"] = 2; + table["nested"]["bb"] = "something"; + table["nested"][5] = -0.5; + table["nested_empty"] = sol::table(lua, sol::create); + table[1] = osg::Vec2f(1, 2); + table[2] = osg::Vec2f(2, 1); + + std::string serialized = LuaUtil::serialize(table); + EXPECT_EQ(serialized.size(), 139); + sol::table res_table = LuaUtil::deserialize(lua, serialized); + sol::table res_readonly_table = LuaUtil::deserialize(lua, serialized, nullptr, true); + + for (auto t : {res_table, res_readonly_table}) + { + EXPECT_EQ(t.get("aa"), 1); + EXPECT_EQ(t.get("ab"), true); + EXPECT_EQ(t.get("nested").get("aa"), 2); + EXPECT_EQ(t.get("nested").get("bb"), "something"); + EXPECT_DOUBLE_EQ(t.get("nested").get(5), -0.5); + EXPECT_EQ(t.get(1), osg::Vec2f(1, 2)); + EXPECT_EQ(t.get(2), osg::Vec2f(2, 1)); + } + + lua["t"] = res_table; + lua["ro_t"] = res_readonly_table; + EXPECT_NO_THROW(lua.safe_script("t.x = 5")); + EXPECT_NO_THROW(lua.safe_script("t.nested.x = 5")); + EXPECT_ERROR(lua.safe_script("ro_t.x = 5"), "userdata value"); + EXPECT_ERROR(lua.safe_script("ro_t.nested.x = 5"), "userdata value"); + } + + struct TestStruct1 { double a, b; }; + struct TestStruct2 { int a, b; }; + + class TestSerializer final : public LuaUtil::UserdataSerializer + { + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is()) + { + TestStruct1 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "ts1", &t, sizeof(t)); + return true; + } + if (data.is()) + { + TestStruct2 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "test_struct2", &t, sizeof(t)); + return true; + } + return false; + } + + bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override + { + if (typeName == "ts1") + { + if (sizeof(TestStruct1) != binaryData.size()) + throw std::runtime_error("Incorrect binaryData.size() for TestStruct1: " + std::to_string(binaryData.size())); + TestStruct1 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + if (typeName == "test_struct2") + { + if (sizeof(TestStruct2) != binaryData.size()) + throw std::runtime_error("Incorrect binaryData.size() for TestStruct2: " + std::to_string(binaryData.size())); + TestStruct2 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + return false; + } + }; + + TEST(LuaSerializationTest, UserdataSerializer) + { + sol::state lua; + sol::table table(lua, sol::create); + table["x"] = TestStruct1{1.5, 2.5}; + table["y"] = TestStruct2{4, 3}; + TestSerializer serializer; + + EXPECT_ERROR(LuaUtil::serialize(table), "Value is not serializable."); + std::string serialized = LuaUtil::serialize(table, &serializer); + EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type in serialized data:"); + sol::table res = LuaUtil::deserialize(lua, serialized, &serializer); + + TestStruct1 rx = res.get("x"); + TestStruct2 ry = res.get("y"); + EXPECT_EQ(rx.a, 1.5); + EXPECT_EQ(rx.b, 2.5); + EXPECT_EQ(ry.a, 4); + EXPECT_EQ(ry.b, 3); + } + +} diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp new file mode 100644 index 0000000000..c1d8cc4c4f --- /dev/null +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -0,0 +1,115 @@ +#include +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + + template + T get(sol::state& lua, std::string luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + TEST(LuaUtilStorageTest, Basic) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + + std::vector callbackCalls; + LuaUtil::Callback callback{ + sol::make_object(mLua, [&](const std::string& section, const sol::optional& key) + { + if (key) + callbackCalls.push_back(section + "_" + *key); + else + callbackCalls.push_back(section + "_*"); + }), + sol::table(mLua, sol::create) + }; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = "fakeId"; + + mLua["mutable"] = storage.getMutableSection("test"); + mLua["ro"] = storage.getReadOnlySection("test"); + mLua["ro"]["subscribe"](mLua["ro"], callback); + + mLua.safe_script("mutable:set('x', 5)"); + EXPECT_EQ(get(mLua, "mutable:get('x')"), 5); + EXPECT_EQ(get(mLua, "ro:get('x')"), 5); + + EXPECT_THROW(mLua.safe_script("ro:set('y', 3)"), std::exception); + + mLua.safe_script("t1 = mutable:asTable()"); + mLua.safe_script("t2 = ro:asTable()"); + EXPECT_EQ(get(mLua, "t1.x"), 5); + EXPECT_EQ(get(mLua, "t2.x"), 5); + + mLua.safe_script("mutable:reset()"); + EXPECT_TRUE(get(mLua, "ro:get('x') == nil")); + + mLua.safe_script("mutable:reset({x=4, y=7})"); + EXPECT_EQ(get(mLua, "ro:get('x')"), 4); + EXPECT_EQ(get(mLua, "ro:get('y')"), 7); + + EXPECT_THAT(callbackCalls, ::testing::ElementsAre("test_x", "test_*", "test_*")); + } + + TEST(LuaUtilStorageTest, Table) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + mLua["mutable"] = storage.getMutableSection("test"); + mLua["ro"] = storage.getReadOnlySection("test"); + + mLua.safe_script("mutable:set('x', { y = 'abc', z = 7 })"); + EXPECT_EQ(get(mLua, "mutable:get('x').z"), 7); + EXPECT_THROW(mLua.safe_script("mutable:get('x').z = 3"), std::exception); + EXPECT_NO_THROW(mLua.safe_script("mutable:getCopy('x').z = 3")); + EXPECT_EQ(get(mLua, "mutable:get('x').z"), 7); + EXPECT_EQ(get(mLua, "ro:get('x').z"), 7); + EXPECT_EQ(get(mLua, "ro:get('x').y"), "abc"); + } + + TEST(LuaUtilStorageTest, Saving) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + + mLua["permanent"] = storage.getMutableSection("permanent"); + mLua["temporary"] = storage.getMutableSection("temporary"); + mLua.safe_script("temporary:removeOnExit()"); + mLua.safe_script("permanent:set('x', 1)"); + mLua.safe_script("temporary:set('y', 2)"); + + std::string tmpFile = (std::filesystem::temp_directory_path() / "test_storage.bin").string(); + storage.save(tmpFile); + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_EQ(get(mLua, "temporary:get('y')"), 2); + + storage.clearTemporaryAndRemoveCallbacks(); + mLua["permanent"] = storage.getMutableSection("permanent"); + mLua["temporary"] = storage.getMutableSection("temporary"); + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(mLua, "temporary:get('y') == nil")); + + mLua.safe_script("permanent:set('x', 3)"); + mLua.safe_script("permanent:set('z', 4)"); + + LuaUtil::LuaStorage storage2(mLua); + storage2.load(tmpFile); + mLua["permanent"] = storage2.getMutableSection("permanent"); + mLua["temporary"] = storage2.getMutableSection("temporary"); + + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(mLua, "permanent:get('z') == nil")); + EXPECT_TRUE(get(mLua, "temporary:get('y') == nil")); + } + +} diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/openmw_test_suite/lua/test_ui_content.cpp new file mode 100644 index 0000000000..f478c618dc --- /dev/null +++ b/apps/openmw_test_suite/lua/test_ui_content.cpp @@ -0,0 +1,97 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + + sol::state state; + + sol::table makeTable() + { + return sol::table(state, sol::create); + } + + sol::table makeTable(std::string name) + { + auto result = makeTable(); + result["name"] = name; + return result; + } + + TEST(LuaUiContentTest, Create) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + EXPECT_EQ(content.size(), 3); + } + + TEST(LuaUiContentTest, CreateWithHole) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table[4] = makeTable(); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, WrongType) + { + auto table = makeTable(); + table.add(makeTable()); + table.add("a"); + table.add(makeTable()); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, NameAccess) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable("a")); + LuaUi::Content content(table); + EXPECT_NO_THROW(content.at("a")); + content.remove("a"); + content.assign(content.size(), makeTable("b")); + content.assign("b", makeTable()); + EXPECT_ANY_THROW(content.at("b")); + EXPECT_EQ(content.size(), 2); + content.assign(content.size(), makeTable("c")); + content.assign(content.size(), makeTable("c")); + content.remove("c"); + EXPECT_ANY_THROW(content.at("c")); + } + + TEST(LuaUiContentTest, IndexOf) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + auto child = makeTable(); + content.assign(2, child); + EXPECT_EQ(content.indexOf(child), 2); + EXPECT_EQ(content.indexOf(makeTable()), content.size()); + } + + TEST(LuaUiContentTest, BoundsChecks) + { + auto table = makeTable(); + LuaUi::Content content(table); + EXPECT_ANY_THROW(content.at(0)); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + EXPECT_ANY_THROW(content.at(3)); + EXPECT_ANY_THROW(content.remove(3)); + EXPECT_NO_THROW(content.remove(1)); + EXPECT_NO_THROW(content.at(1)); + EXPECT_EQ(content.size(), 2); + } +} diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp new file mode 100644 index 0000000000..2e8734a2df --- /dev/null +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -0,0 +1,186 @@ +#include "gmock/gmock.h" +#include + +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + + template + T get(sol::state& lua, const std::string& luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + std::string getAsString(sol::state& lua, std::string luaCode) + { + return LuaUtil::toString(lua.safe_script("return " + luaCode)); + } + + TEST(LuaUtilPackageTest, Vector2) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector2(3, 4)"); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 3); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 4); + EXPECT_EQ(get(lua, "tostring(v)"), "(3, 4)"); + EXPECT_FLOAT_EQ(get(lua, "v:length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "v:length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector2(1, 2) == util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) == util.vector2(2, 4) / 2")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) * 2 == util.vector2(2, 4)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2) * v"), 17); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2):dot(v)"), 17); + EXPECT_ERROR(lua.safe_script("v2, len = v.normalize()"), "value is not a valid userdata"); // checks that it doesn't segfault + lua.safe_script("v2, len = v:normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector2(3/5, 4/5)")); + lua.safe_script("_, len = util.vector2(0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector2(1, 0):ediv(util.vector2(0, 0))"); + 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)")); + } + + TEST(LuaUtilPackageTest, Vector3) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector3(5, 12, 13)"); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 5); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); + EXPECT_FLOAT_EQ(get(lua, "v.z"), 13); + EXPECT_EQ(get(lua, "tostring(v)"), "(5, 12, 13)"); + EXPECT_EQ(getAsString(lua, "v"), "(5, 12, 13)"); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector3(1, 2, 3) == util.vector3(1, 3, 2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1) * v"), 5*3 + 12*2 + 13*1); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1):dot(v)"), 5*3 + 12*2 + 13*1); + EXPECT_TRUE(get(lua, "util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)")); + EXPECT_ERROR(lua.safe_script("v2, len = util.vector3(3, 4, 0).normalize()"), "value is not a valid userdata"); + lua.safe_script("v2, len = util.vector3(3, 4, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector3(3/5, 4/5, 0)")); + lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector3(1, 1, 1):ediv(util.vector3(0, 0, 0))"); + 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)")); + } + + TEST(LuaUtilPackageTest, Vector4) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector4(5, 12, 13, 15)"); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 5); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); + EXPECT_FLOAT_EQ(get(lua, "v.z"), 13); + EXPECT_FLOAT_EQ(get(lua, "v.w"), 15); + EXPECT_EQ(get(lua, "tostring(v)"), "(5, 12, 13, 15)"); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(4, 0, 0, 3):length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(4, 0, 0, 3):length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector4(1, 2, 3, 4) == util.vector4(1, 3, 2, 4)")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) + util.vector4(2, 5, 1, 2) == util.vector4(3, 7, 4, 6)")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) - util.vector4(2, 5, 1, 7) == -util.vector4(1, 3, -2, 3)")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) == util.vector4(2, 4, 6, 8) / 2")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) * 2 == util.vector4(2, 4, 6, 8)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(3, 2, 1, 4) * v"), 5 * 3 + 12 * 2 + 13 * 1 + 15 * 4); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(3, 2, 1, 4):dot(v)"), 5 * 3 + 12 * 2 + 13 * 1 + 15 * 4); + lua.safe_script("v2, len = util.vector4(3, 0, 0, 4):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector4(3/5, 0, 0, 4/5)")); + lua.safe_script("_, len = util.vector4(0, 0, 0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector4(1, 1, 1, -1):ediv(util.vector4(0, 0, 0, 0))"); + EXPECT_TRUE(get(lua, "ediv0.w == -math.huge")); + EXPECT_TRUE(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)")); + } + + TEST(LuaUtilPackageTest, Color) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("brown = util.color.rgba(0.75, 0.25, 0, 1)"); + EXPECT_EQ(get(lua, "tostring(brown)"), "(0.75, 0.25, 0, 1)"); + lua.safe_script("blue = util.color.rgb(0, 1, 0, 1)"); + EXPECT_EQ(get(lua, "tostring(blue)"), "(0, 1, 0, 1)"); + lua.safe_script("red = util.color.hex('ff0000')"); + EXPECT_EQ(get(lua, "tostring(red)"), "(1, 0, 0, 1)"); + lua.safe_script("green = util.color.hex('00FF00')"); + EXPECT_EQ(get(lua, "tostring(green)"), "(0, 1, 0, 1)"); + lua.safe_script("darkRed = util.color.hex('a01112')"); + EXPECT_EQ(get(lua, "darkRed:asHex()"), "a01112"); + EXPECT_TRUE(get(lua, "green:asRgba() == util.vector4(0, 1, 0, 1)")); + EXPECT_TRUE(get(lua, "red:asRgb() == util.vector3(1, 0, 0)")); + } + + TEST(LuaUtilPackageTest, Transform) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua["T"] = lua["util"]["transform"]; + lua["v"] = lua["util"]["vector3"]; + EXPECT_ERROR(lua.safe_script("T.identity = nil"), "attempt to index"); + EXPECT_EQ(getAsString(lua, "T.identity * v(3, 4, 5)"), "(3, 4, 5)"); + EXPECT_EQ(getAsString(lua, "T.move(1, 2, 3) * v(3, 4, 5)"), "(4, 6, 8)"); + EXPECT_EQ(getAsString(lua, "T.scale(1, -2, 3) * v(3, 4, 5)"), "(3, -8, 15)"); + EXPECT_EQ(getAsString(lua, "T.scale(v(1, 2, 3)) * v(3, 4, 5)"), "(3, 8, 15)"); + lua.safe_script("moveAndScale = T.move(v(1, 2, 3)) * T.scale(0.5, 1, 0.5) * T.move(10, 20, 30)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(0, 0, 0)"), "(6, 22, 18)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)"); + EXPECT_THAT(getAsString(lua, "moveAndScale"), AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }"))); + EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }"); + lua.safe_script("rx = T.rotateX(-math.pi / 2)"); + lua.safe_script("ry = T.rotateY(-math.pi / 2)"); + lua.safe_script("rz = T.rotateZ(-math.pi / 2)"); + EXPECT_LT(get(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6); + lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(math.pi / 4)"); + EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ")); + EXPECT_LT(get(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6); + lua.safe_script("rz_move_rx = rz * T.move(0, 3, 0) * rx"); + EXPECT_LT(get(lua, "(rz_move_rx * v(1, 2, 3) - v(0, 1, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz_move_rx:inverse() * v(0, 1, 2) - v(1, 2, 3)):length()"), 1e-6); + } + + TEST(LuaUtilPackageTest, UtilityFunctions) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))"); + EXPECT_FLOAT_EQ(get(lua, "v.x"), -0.5f); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 0.86602539f); + EXPECT_FLOAT_EQ(get(lua, "util.normalizeAngle(math.pi * 10 + 0.1)"), 0.1f); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(0.1, 0, 1.5)"), 0.1f); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(-0.1, 0, 1.5)"), 0); + EXPECT_FLOAT_EQ(get(lua, "util.clamp(2.1, 0, 1.5)"), 1.5f); + lua.safe_script("t = util.makeReadOnly({x = 1})"); + EXPECT_FLOAT_EQ(get(lua, "t.x"), 1); + EXPECT_ERROR(lua.safe_script("t.y = 2"), "userdata value"); + } + +} diff --git a/apps/openmw_test_suite/misc/compression.cpp b/apps/openmw_test_suite/misc/compression.cpp new file mode 100644 index 0000000000..e062599f4a --- /dev/null +++ b/apps/openmw_test_suite/misc/compression.cpp @@ -0,0 +1,31 @@ +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + TEST(MiscCompressionTest, compressShouldAddPrefixWithDataSize) + { + const std::vector data(1234); + const std::vector compressed = compress(data); + int size = 0; + std::memcpy(&size, compressed.data(), sizeof(size)); + EXPECT_EQ(size, data.size()); + } + + TEST(MiscCompressionTest, decompressIsInverseToCompress) + { + const std::vector data(1024); + const std::vector compressed = compress(data); + EXPECT_LT(compressed.size(), data.size()); + const std::vector decompressed = decompress(compressed); + EXPECT_EQ(decompressed, data); + } +} diff --git a/apps/openmw_test_suite/misc/progressreporter.cpp b/apps/openmw_test_suite/misc/progressreporter.cpp new file mode 100644 index 0000000000..cd5449acf7 --- /dev/null +++ b/apps/openmw_test_suite/misc/progressreporter.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + struct ReportMock + { + MOCK_METHOD(void, call, (std::size_t, std::size_t), ()); + }; + + struct Report + { + StrictMock* mImpl; + + void operator()(std::size_t provided, std::size_t expected) + { + mImpl->call(provided, expected); + } + }; + + TEST(MiscProgressReporterTest, shouldCallReportWhenPassedInterval) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).WillOnce(Return()); + ProgressReporter reporter(std::chrono::steady_clock::duration(0), Report {&report}); + reporter(13, 42); + } + + TEST(MiscProgressReporterTest, shouldNotCallReportWhenIntervalIsNotPassed) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).Times(0); + ProgressReporter reporter(std::chrono::seconds(1000), Report {&report}); + reporter(13, 42); + } +} diff --git a/apps/openmw_test_suite/misc/test_endianness.cpp b/apps/openmw_test_suite/misc/test_endianness.cpp new file mode 100644 index 0000000000..9c3bbf61a6 --- /dev/null +++ b/apps/openmw_test_suite/misc/test_endianness.cpp @@ -0,0 +1,122 @@ +#include +#include "components/misc/endianness.hpp" + +struct EndiannessTest : public ::testing::Test {}; + +TEST_F(EndiannessTest, test_swap_endianness_inplace1) +{ + uint8_t zero=0x00; + uint8_t ff=0xFF; + uint8_t fortytwo=0x42; + uint8_t half=128; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00); + + Misc::swapEndiannessInplace(ff); + EXPECT_EQ(ff, 0xFF); + + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x42); + + Misc::swapEndiannessInplace(half); + EXPECT_EQ(half, 128); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace2) +{ + uint16_t zero = 0x0000; + uint16_t ffff = 0xFFFF; + uint16_t n12 = 0x0102; + uint16_t fortytwo = 0x0042; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFu); + + Misc::swapEndiannessInplace(n12); + EXPECT_EQ(n12, 0x0201u); + Misc::swapEndiannessInplace(n12); + EXPECT_EQ(n12, 0x0102u); + + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x4200u); + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x0042u); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace4) +{ + uint32_t zero = 0x00000000; + uint32_t n1234 = 0x01020304; + uint32_t ffff = 0xFFFFFFFF; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00000000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00000000u); + + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x04030201u); + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x01020304u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFFFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFFFFFu); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace8) +{ + uint64_t zero = 0x0000'0000'0000'0000; + uint64_t n1234 = 0x0102'0304'0506'0708; + uint64_t ffff = 0xFFFF'FFFF'FFFF'FFFF; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000'0000'0000'0000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000'0000'0000'0000u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFF'FFFF'FFFF'FFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFF'FFFF'FFFF'FFFFu); + + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x0807'0605'0403'0201u); + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x0102'0304'0506'0708u); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace_float) +{ + const uint32_t original = 0x4023d70a; + const uint32_t expected = 0x0ad72340; + + float number; + memcpy(&number, &original, sizeof(original)); + + Misc::swapEndiannessInplace(number); + + EXPECT_TRUE(!memcmp(&number, &expected, sizeof(expected))); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace_double) +{ + const uint64_t original = 0x040047ae147ae147ul; + const uint64_t expected = 0x47e17a14ae470004ul; + + double number; + memcpy(&number, &original, sizeof(original)); + + Misc::swapEndiannessInplace(number); + + EXPECT_TRUE(!memcmp(&number, &expected, sizeof(expected)) ); +} diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp new file mode 100644 index 0000000000..c4130b9cca --- /dev/null +++ b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp @@ -0,0 +1,80 @@ +#include +#include "components/misc/resourcehelpers.hpp" +#include "../testing_util.hpp" + +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"); + } + + 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"); + } + + 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"); + } + + namespace + { + std::string checkChangeExtensionToDds(std::string path) + { + changeExtensionToDds(path); + return path; + } + } + + TEST(ChangeExtensionToDds, original_extension_with_same_size_as_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.tga"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_greater_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.jpeg"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_smaller_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.xx"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, does_not_change_dds_extension) + { + std::string path = "texture/bar.dds"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, does_not_change_when_no_extension) + { + std::string path = "texture/bar"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, change_when_there_is_an_extension) + { + std::string path = "texture/bar.jpeg"; + EXPECT_TRUE(changeExtensionToDds(path)); + } +} diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/openmw_test_suite/misc/test_stringops.cpp index 086908692d..d21a74700c 100644 --- a/apps/openmw_test_suite/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/misc/test_stringops.cpp @@ -1,5 +1,10 @@ #include #include "components/misc/stringops.hpp" +#include "components/misc/algorithm.hpp" + +#include +#include +#include struct PartialBinarySearchTest : public ::testing::Test { @@ -12,13 +17,9 @@ struct PartialBinarySearchTest : public ::testing::Test std::sort(mDataVec.begin(), mDataVec.end(), Misc::StringUtils::ciLess); } - void TearDown() override - { - } - bool matches(const std::string& keyword) { - return Misc::StringUtils::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); + return Misc::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); } }; @@ -51,3 +52,97 @@ TEST_F (PartialBinarySearchTest, ci_test) std::string unicode1 = "\u04151 \u0418"; // CYRILLIC CAPITAL LETTER IE, CYRILLIC CAPITAL LETTER I EXPECT_TRUE( Misc::StringUtils::lowerCase(unicode1) == unicode1 ); } + +namespace +{ + using ::Misc::StringUtils; + using namespace ::testing; + + template + struct MiscStringUtilsCiEqualEmptyTest : Test {}; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest); + + TYPED_TEST_P(MiscStringUtilsCiEqualEmptyTest, empty_strings_should_be_equal) + { + EXPECT_TRUE(StringUtils::ciEqual(typename TypeParam::first_type {}, typename TypeParam::second_type {})); + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest, + empty_strings_should_be_equal + ); + + using EmptyStringTypePairsTypes = Types< + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair + >; + + INSTANTIATE_TYPED_TEST_SUITE_P(EmptyStringTypePairs, MiscStringUtilsCiEqualEmptyTest, EmptyStringTypePairsTypes); + + template + struct MiscStringUtilsCiEqualNotEmptyTest : Test {}; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest); + + using RawValue = const char[4]; + + constexpr RawValue foo = "f0#"; + constexpr RawValue fooUpper = "F0#"; + constexpr RawValue bar = "bar"; + + template + using Value = std::conditional_t, RawValue&, T>; + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_should_be_equal) + { + const Value a {foo}; + const Value b {foo}; + EXPECT_TRUE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_with_different_case_sensetivity_should_be_equal) + { + const Value a {foo}; + const Value b {fooUpper}; + EXPECT_TRUE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, different_strings_content_should_not_be_equal) + { + const Value a {foo}; + const Value b {bar}; + EXPECT_FALSE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest, + same_strings_should_be_equal, + same_strings_with_different_case_sensetivity_should_be_equal, + different_strings_content_should_not_be_equal + ); + + using NotEmptyStringTypePairsTypes = Types< + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair + >; + + INSTANTIATE_TYPED_TEST_SUITE_P(NotEmptyStringTypePairs, MiscStringUtilsCiEqualNotEmptyTest, NotEmptyStringTypePairsTypes); + + TEST(MiscStringUtilsCiEqualTest, string_with_different_length_should_not_be_equal) + { + EXPECT_FALSE(StringUtils::ciEqual(std::string("a"), std::string("aa"))); + } +} diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp index 431725be2c..6128c7e5dd 100644 --- a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp +++ b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp @@ -27,9 +27,9 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution) search.highlightKeywords(text.begin(), text.end(), matches); // Should contain: "foo bar", "lock switch" - ASSERT_TRUE (matches.size() == 2); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "foo bar"); - ASSERT_TRUE (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) == "lock switch"); + EXPECT_EQ (matches.size() , 2); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "foo bar"); + EXPECT_EQ (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) , "lock switch"); } TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) @@ -43,8 +43,8 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "dwemer language"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "dwemer language"); } @@ -62,6 +62,75 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "bar lock"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "bar lock"); } + + +TEST_F(KeywordSearchTest, keyword_test_utf8_word_begin) +{ + // We make sure that the search works well even if the character is not ASCII + MWDialogue::KeywordSearch search; + search.seed("états", 0); + search.seed("ïrradiés", 0); + search.seed("ça nous déçois", 0); + search.seed("ois", 0); + + std::string text = "les nations unis ont réunis le monde entier, états units inclus pour parler du problème des gens ïrradiés et ça nous déçois"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ (matches.size() , 3); + EXPECT_EQ (std::string( matches[0].mBeg, matches[0].mEnd) , "états"); + EXPECT_EQ (std::string( matches[1].mBeg, matches[1].mEnd) , "ïrradiés"); + EXPECT_EQ (std::string( matches[2].mBeg, matches[2].mEnd) , "ça nous déçois"); +} + +TEST_F(KeywordSearchTest, keyword_test_non_alpha_non_whitespace_word_begin) +{ + // We make sure that the search works well even if the separator is not a whitespace + MWDialogue::KeywordSearch search; + search.seed("Report to caius cosades", 0); + + + + std::string text = "I was told to \"Report to caius cosades\""; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Report to caius cosades"); +} + +TEST_F(KeywordSearchTest, keyword_test_russian_non_ascii_before) +{ + // We make sure that the search works well even if the separator is not a whitespace with russian chars + MWDialogue::KeywordSearch search; + search.seed("Доложить Каю Косадесу", 0); + + std::string text = "Что? Да. Я Кай Косадес. То есть как это, вам велели «Доложить Каю Косадесу»? О чем вы говорите?"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); +} + +TEST_F(KeywordSearchTest, keyword_test_russian_ascii_before) +{ + // We make sure that the search works well even if the separator is not a whitespace with russian chars + MWDialogue::KeywordSearch search; + search.seed("Доложить Каю Косадесу", 0); + + std::string text = "Что? Да. Я Кай Косадес. То есть как это, вам велели 'Доложить Каю Косадесу'? О чем вы говорите?"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); +} + diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp new file mode 100644 index 0000000000..dea2a0dc8a --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -0,0 +1,851 @@ +#include +#include + +#include "test_utils.hpp" + +namespace +{ + struct MWScriptTest : public ::testing::Test + { + MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} + + std::optional compile(const std::string& scriptBody, bool shouldFail = false) + { + mParser.reset(); + mErrorHandler.reset(); + std::istringstream input(scriptBody); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); + scanner.scan(mParser); + if(mErrorHandler.isGood()) + { + std::vector code; + mParser.getCode(code); + return CompiledScript(code, mParser.getLocals()); + } + else if(!shouldFail) + logErrors(); + return {}; + } + + void logErrors() + { + for(const auto& [error, loc] : mErrorHandler.getErrors()) + { + std::cout << error; + if(loc.mLine) + std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; + std::cout << "\n"; + } + } + + void registerExtensions() + { + Compiler::registerExtensions(mExtensions); + mCompilerContext.setExtensions(&mExtensions); + } + + void run(const CompiledScript& script, TestInterpreterContext& context) + { + mInterpreter.run(&script.mByteCode[0], static_cast(script.mByteCode.size()), context); + } + + template + void installOpcode(int code, TArgs&& ...args) + { + mInterpreter.installSegment5(code, std::forward(args)...); + } + + protected: + void SetUp() override + { + Interpreter::installOpcodes(mInterpreter); + } + + void TearDown() override {} + private: + TestErrorHandler mErrorHandler; + TestCompilerContext mCompilerContext; + Compiler::FileParser mParser; + Compiler::Extensions mExtensions; + Interpreter::Interpreter mInterpreter; + }; + + const std::string sScript1 = R"mwscript(Begin basic_logic +; Comment +short one +short two + +set one to two + +if ( one == two ) + set one to 1 +elseif ( two == 1 ) + set one to 2 +else + set one to 3 +endif + +while ( one < two ) + set one to ( one + 1 ) +endwhile + +End)mwscript"; + + const std::string sScript2 = R"mwscript(Begin addtopic + +AddTopic "OpenMW Unit Test" + +End)mwscript"; + + const std::string sScript3 = R"mwscript(Begin math + +short a +short b +short c +short d +short e + +set b to ( a + 1 ) +set c to ( a - 1 ) +set d to ( b * c ) +set e to ( d / a ) + +End)mwscript"; + +// https://forum.openmw.org/viewtopic.php?f=6&t=2262 + const std::string sScript4 = R"mwscript(Begin scripting_once_again + +player -> addSpell "fire_bite", 645 + +PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 + +End)mwscript"; + + const std::string sIssue587 = R"mwscript(Begin stalresetScript + +End stalreset Script)mwscript"; + + const std::string sIssue677 = R"mwscript(Begin _ase_dtree_dtree-owls + +End)mwscript"; + + const std::string sIssue685 = R"mwscript(Begin issue685 + +Choice: "Sicher. Hier, nehmt." 1 "Nein, ich denke nicht. Tut mir Leid." 2 +StartScript GetPCGold + +End)mwscript"; + + const std::string sIssue694 = R"mwscript(Begin issue694 + +float timer + +if ( timer < .1 ) +endif + +End)mwscript"; + + const std::string sIssue1062 = R"mwscript(Begin issue1026 + +short end + +End)mwscript"; + + const std::string sIssue1430 = R"mwscript(Begin issue1430 + +short var +If ( menumode == 1 ) + Player->AddItem "fur_boots", 1 + Player->Equip "iron battle axe", 1 + player->addspell "fire bite", 645 + player->additem "ring_keley", 1, +endif + +End)mwscript"; + + const std::string sIssue1593 = R"mwscript(Begin changeWater_-550_400 + +End)mwscript"; + + const std::string sIssue1730 = R"mwscript(Begin 4LOM_Corprusarium_Guards + +End)mwscript"; + + const std::string sIssue1767 = R"mwscript(Begin issue1767 + +player->GetPcRank "temple" + +End)mwscript"; + + const std::string sIssue2185 = R"mwscript(Begin issue2185 + +short a +short b +short eq +short gte +short lte +short ne + +set eq to 0 +if ( a == b ) + set eq to ( eq + 1 ) +endif +if ( a = = b ) + set eq to ( eq + 1 ) +endif + +set gte to 0 +if ( a >= b ) + set gte to ( gte + 1 ) +endif +if ( a > = b ) + set gte to ( gte + 1 ) +endif + +set lte to 0 +if ( a <= b ) + set lte to ( lte + 1 ) +endif +if ( a < = b ) + set lte to ( lte + 1 ) +endif + +set ne to 0 +if ( a != b ) + set ne to ( ne + 1 ) +endif +if ( a ! = b ) + set ne to ( ne + 1 ) +endif + +End)mwscript"; + + const std::string sIssue2206 = R"mwscript(Begin issue2206 + +Choice ."Sklavin kaufen." 1 "Lebt wohl." 2 +Choice Choice "Insister pour qu’il vous réponde." 6 "Le prier de vous accorder un peu de son temps." 6 " Le menacer de révéler qu'il prélève sa part sur les bénéfices de la mine d’ébonite." 7 + +End)mwscript"; + + const std::string sIssue2207 = R"mwscript(Begin issue2207 + +PositionCell -35 –473 -248 0 "Skaal-Dorf, Die Große Halle" + +End)mwscript"; + + const std::string sIssue2794 = R"mwscript(Begin issue2794 + +if ( player->"getlevel" == 1 ) + ; do something +endif + +End)mwscript"; + + const std::string sIssue2830 = R"mwscript(Begin issue2830 + +AddItem "if" 1 +AddItem "endif" 1 +GetItemCount "begin" + +End)mwscript"; + + const std::string sIssue2991 = R"mwscript(Begin issue2991 + +MessageBox "OnActivate" +messagebox "messagebox" +messagebox "if" +messagebox "tcl" + +End)mwscript"; + + const std::string sIssue3006 = R"mwscript(Begin issue3006 + +short a + +if ( a == 1 ) + set a to 2 +else set a to 3 +endif + +End)mwscript"; + + const std::string sIssue3725 = R"mwscript(Begin issue3725 + +onactivate + +if onactivate + ; do something +endif + +End)mwscript"; + + const std::string sIssue3744 = R"mwscript(Begin issue3744 + +short a +short b +short c + +set c to 0 + +if ( a => b ) + set c to ( c + 1 ) +endif +if ( a =< b ) + set c to ( c + 1 ) +endif +if ( a = b ) + set c to ( c + 1 ) +endif +if ( a == b ) + set c to ( c + 1 ) +endif + +End)mwscript"; + + const std::string sIssue3836 = R"mwscript(Begin issue3836 + +MessageBox " Membership Level: %.0f +Account Balance: %.0f +Your Gold: %.0f +Interest Rate: %.3f +Service Charge Rate: %.3f +Total Service Charges: %.0f +Total Interest Earned: %.0f " Membership BankAccount YourGold InterestRate ServiceRate TotalServiceCharges TotalInterestEarned + +End)mwscript"; + + const std::string sIssue3846 = R"mwscript(Begin issue3846 + +Addtopic -spells... +Addtopic -magicka... + +End)mwscript"; + + const std::string sIssue4061 = R"mwscript(Begin 01_Rz_neuvazhay-koryto2 + +End)mwscript"; + + const std::string sIssue4451 = R"mwscript(Begin, GlassDisplayScript + +;[Script body] + +End, GlassDisplayScript)mwscript"; + + const std::string sIssue4597 = R"mwscript(Begin issue4597 + +short a +short b +short c +short d + +set c to 0 +set d to 0 + +if ( a <> b ) + set c to ( c + 1 ) +endif +if ( a << b ) + set c to ( c + 1 ) +endif +if ( a < b ) + set c to ( c + 1 ) +endif + +if ( a >< b ) + set d to ( d + 1 ) +endif +if ( a >> b ) + set d to ( d + 1 ) +endif +if ( a > b ) + set d to ( d + 1 ) +endif + +End)mwscript"; + + const std::string sIssue4598 = R"mwscript(Begin issue4598 + +StartScript kal_S_Pub_Jejubãr_Faraminos + +End)mwscript"; + + const std::string sIssue4803 = R"mwscript( +-- ++-Begin issue4803 + +End)mwscript"; + + const std::string sIssue4867 = R"mwscript(Begin issue4867 + +float PcMagickaMult : The gameplay setting fPcBaseMagickaMult - 1.0000 + +End)mwscript"; + + const std::string sIssue4888 = R"mwscript(Begin issue4888 + +if (player->GameHour == 10) +set player->GameHour to 20 +endif + +End)mwscript"; + + const std::string sIssue5087 = R"mwscript(Begin Begin + +player->sethealth 0 +stopscript Begin + +End Begin)mwscript"; + + const std::string sIssue5097 = R"mwscript(Begin issue5097 + +setscale "0.3" + +End)mwscript"; + + const std::string sIssue5345 = R"mwscript(Begin issue5345 + +StartScript DN_MinionDrain_s" + +End)mwscript"; + + const std::string sIssue6066 = R"mwscript(Begin issue6066 +addtopic "return" + +End)mwscript"; + + const std::string sIssue6282 = R"mwscript(Begin 11AA_LauraScript7.5 + +End)mwscript"; + + const std::string sIssue6363 = R"mwscript(Begin issue6363 + +short 1 + +if ( "1" == 1 ) + PositionCell 0 1 2 3 4 5 "Morrowland" +endif + +set 1 to 42 + +End)mwscript"; + + const std::string sIssue6380 = R"mwscript(,Begin,issue6380, + +,short,a + +,set,a,to,,,,(a,+1) + +messagebox,"this is a %g",a + +,End,)mwscript"; + + TEST_F(MWScriptTest, mwscript_test_invalid) + { + EXPECT_THROW(compile("this is not a valid script", true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_compilation) + { + EXPECT_FALSE(!compile(sScript1)); + } + + TEST_F(MWScriptTest, mwscript_test_no_extensions) + { + EXPECT_THROW(compile(sScript2, true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_function) + { + registerExtensions(); + bool failed = true; + if(const auto script = compile(sScript2)) + { + class AddTopic : public Interpreter::Opcode0 + { + bool& mFailed; + public: + AddTopic(bool& failed) : mFailed(failed) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + mFailed = false; + EXPECT_EQ(topic, "OpenMW Unit Test"); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, failed); + TestInterpreterContext context; + run(*script, context); + } + if(failed) + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_math) + { + if(const auto script = compile(sScript3)) + { + struct Algorithm + { + int a; + int b; + int c; + int d; + int e; + + void run(int input) + { + a = input; + b = a + 1; + c = a - 1; + d = b * c; + e = d / a; + } + + void test(const TestInterpreterContext& context) const + { + EXPECT_EQ(a, context.getLocalShort(0)); + EXPECT_EQ(b, context.getLocalShort(1)); + EXPECT_EQ(c, context.getLocalShort(2)); + EXPECT_EQ(d, context.getLocalShort(3)); + EXPECT_EQ(e, context.getLocalShort(4)); + } + } algorithm; + TestInterpreterContext context; + for(int i = 1; i < 1000; ++i) + { + context.setLocalShort(0, i); + run(*script, context); + algorithm.run(i); + algorithm.test(context); + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_forum_thread) + { + registerExtensions(); + EXPECT_FALSE(!compile(sScript4)); + } + + TEST_F(MWScriptTest, mwscript_test_587) + { + EXPECT_FALSE(!compile(sIssue587)); + } + + TEST_F(MWScriptTest, mwscript_test_677) + { + EXPECT_FALSE(!compile(sIssue677)); + } + + TEST_F(MWScriptTest, mwscript_test_685) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue685)); + } + + TEST_F(MWScriptTest, mwscript_test_694) + { + EXPECT_FALSE(!compile(sIssue694)); + } + + TEST_F(MWScriptTest, mwscript_test_1062) + { + if(const auto script = compile(sIssue1062)) + { + EXPECT_EQ(script->mLocals.getIndex("end"), 0); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_1430) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1430)); + } + + TEST_F(MWScriptTest, mwscript_test_1593) + { + EXPECT_FALSE(!compile(sIssue1593)); + } + + TEST_F(MWScriptTest, mwscript_test_1730) + { + EXPECT_FALSE(!compile(sIssue1730)); + } + + TEST_F(MWScriptTest, mwscript_test_1767) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1767)); + } + + TEST_F(MWScriptTest, mwscript_test_2185) + { + if(const auto script = compile(sIssue2185)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(3), a >= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(4), a <= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(5), a != b ? 2 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_2206) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2206)); + } + + TEST_F(MWScriptTest, mwscript_test_2207) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2207)); + } + + TEST_F(MWScriptTest, mwscript_test_2794) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2794)); + } + + TEST_F(MWScriptTest, mwscript_test_2830) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2830)); + } + + TEST_F(MWScriptTest, mwscript_test_2991) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2991)); + } + + TEST_F(MWScriptTest, mwscript_test_3006) + { + if(const auto script = compile(sIssue3006)) + { + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 0); + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 2); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3725) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3725)); + } + + TEST_F(MWScriptTest, mwscript_test_3744) + { + if(const auto script = compile(sIssue3744)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 4 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3836) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3836)); + } + + TEST_F(MWScriptTest, mwscript_test_3846) + { + registerExtensions(); + if(const auto script = compile(sIssue3846)) + { + std::vector topics = { "-spells...", "-magicka..." }; + class AddTopic : public Interpreter::Opcode0 + { + std::vector& mTopics; + public: + AddTopic(std::vector& topics) : mTopics(topics) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + EXPECT_EQ(topic, mTopics[0]); + mTopics.erase(mTopics.begin()); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, topics); + TestInterpreterContext context; + run(*script, context); + EXPECT_TRUE(topics.empty()); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4061) + { + EXPECT_FALSE(!compile(sIssue4061)); + } + + TEST_F(MWScriptTest, mwscript_test_4451) + { + EXPECT_FALSE(!compile(sIssue4451)); + } + + TEST_F(MWScriptTest, mwscript_test_4597) + { + if(const auto script = compile(sIssue4597)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a < b ? 3 : 0); + EXPECT_EQ(context.getLocalShort(3), a > b ? 3 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4598) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue4598)); + } + + TEST_F(MWScriptTest, mwscript_test_4803) + { + EXPECT_FALSE(!compile(sIssue4803)); + } + + TEST_F(MWScriptTest, mwscript_test_4867) + { + EXPECT_FALSE(!compile(sIssue4867)); + } + + TEST_F(MWScriptTest, mwscript_test_4888) + { + EXPECT_FALSE(!compile(sIssue4888)); + } + + TEST_F(MWScriptTest, mwscript_test_5087) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5087)); + } + + TEST_F(MWScriptTest, mwscript_test_5097) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5097)); + } + + TEST_F(MWScriptTest, mwscript_test_5345) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5345)); + } + + TEST_F(MWScriptTest, mwscript_test_6066) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue6066)); + } + + TEST_F(MWScriptTest, mwscript_test_6282) + { + EXPECT_FALSE(!compile(sIssue6282)); + } + + TEST_F(MWScriptTest, mwscript_test_6363) + { + registerExtensions(); + if(const auto script = compile(sIssue6363)) + { + class PositionCell : public Interpreter::Opcode0 + { + bool& mRan; + public: + PositionCell(bool& ran) : mRan(ran) {} + + void execute(Interpreter::Runtime& runtime) + { + mRan = true; + } + }; + bool ran = false; + installOpcode(Compiler::Transformation::opcodePositionCell, ran); + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_FALSE(ran); + ran = false; + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_TRUE(ran); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_6380) + { + EXPECT_FALSE(!compile(sIssue6380)); + } +} \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp new file mode 100644 index 0000000000..15f2fae661 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -0,0 +1,243 @@ +#ifndef MWSCRIPT_TESTING_UTIL_H +#define MWSCRIPT_TESTING_UTIL_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return Misc::StringUtils::ciEqual(name, "player"); } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + class LocalVariables + { + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + template + T getLocal(std::size_t index, const std::vector& vector) const + { + if(index < vector.size()) + return vector[index]; + return {}; + } + + template + void setLocal(T value, std::size_t index, std::vector& vector) + { + if(index >= vector.size()) + vector.resize(index + 1); + vector[index] = value; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(std::size_t index) const { return getLocal(index, mShorts); }; + + int getLong(std::size_t index) const { return getLocal(index, mLongs); }; + + float getFloat(std::size_t index) const { return getLocal(index, mFloats); }; + + void setShort(std::size_t index, int value) { setLocal(value, index, mShorts); }; + + void setLong(std::size_t index, int value) { setLocal(value, index, mLongs); }; + + void setFloat(std::size_t index, float value) { setLocal(value, index, mFloats); }; + }; + + class GlobalVariables + { + std::map> mShorts; + std::map> mLongs; + std::map> mFloats; + + template + T getGlobal(std::string_view name, const std::map>& map) const + { + auto it = map.find(name); + if(it != map.end()) + return it->second; + return {}; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(std::string_view name) const { return getGlobal(name, mShorts); }; + + int getLong(std::string_view name) const { return getGlobal(name, mLongs); }; + + float getFloat(std::string_view name) const { return getGlobal(name, mFloats); }; + + void setShort(std::string_view name, int value) { mShorts[std::string(name)] = value; }; + + void setLong(std::string_view name, int value) { mLongs[std::string(name)] = value; }; + + void setFloat(std::string_view name, float value) { mFloats[std::string(name)] = value; }; + }; + + class TestInterpreterContext : public Interpreter::Context + { + LocalVariables mLocals; + std::map> mMembers; + public: + std::string getTarget() const override { return {}; }; + + int getLocalShort(int index) const override { return mLocals.getShort(index); }; + + int getLocalLong(int index) const override { return mLocals.getLong(index); }; + + float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; + + void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; + + void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; + + void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; + + void messageBox(const std::string& message, const std::vector& buttons) override {}; + + void report(const std::string& message) override {}; + + int getGlobalShort(std::string_view name) const override { return {}; }; + + int getGlobalLong(std::string_view name) const override { return {}; }; + + float getGlobalFloat(std::string_view name) const override { return {}; }; + + void setGlobalShort(std::string_view name, int value) override {}; + + void setGlobalLong(std::string_view name, int value) override {}; + + void setGlobalFloat(std::string_view name, float value) override {}; + + std::vector getGlobals() const override { return {}; }; + + char getGlobalType(std::string_view name) const override { return ' '; }; + + std::string getActionBinding(std::string_view action) const override { return {}; }; + + std::string getActorName() const override { return {}; }; + + std::string getNPCRace() const override { return {}; }; + + std::string getNPCClass() const override { return {}; }; + + std::string getNPCFaction() const override { return {}; }; + + std::string getNPCRank() const override { return {}; }; + + std::string getPCName() const override { return {}; }; + + std::string getPCRace() const override { return {}; }; + + std::string getPCClass() const override { return {}; }; + + std::string getPCRank() const override { return {}; }; + + std::string getPCNextRank() const override { return {}; }; + + int getPCBounty() const override { return {}; }; + + std::string getCurrentCellName() const override { return {}; }; + + int getMemberShort(std::string_view id, std::string_view name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getShort(name); + return {}; + }; + + int getMemberLong(std::string_view id, std::string_view name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getLong(name); + return {}; + }; + + float getMemberFloat(std::string_view id, std::string_view name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getFloat(name); + return {}; + }; + + void setMemberShort(std::string_view id, std::string_view name, int value, bool global) override { mMembers[std::string(id)].setShort(name, value); }; + + void setMemberLong(std::string_view id, std::string_view name, int value, bool global) override { mMembers[std::string(id)].setLong(name, value); }; + + void setMemberFloat(std::string_view id, std::string_view name, float value, bool global) override { mMembers[std::string(id)].setFloat(name, value); }; + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; +} + +#endif \ No newline at end of file diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index 77aaccfdd0..34079a8172 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -1,15 +1,21 @@ #include -#include +#include + +#include +#include #include -#include -#include +#include +#include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwmechanics/spelllist.hpp" +#include "../testing_util.hpp" + namespace MWMechanics { SpellList::SpellList(const std::string& id, int type) : mId(id), mType(type) {} @@ -27,19 +33,15 @@ struct ContentFileTest : public ::testing::Test readContentFiles(); // load the content files - std::vector readerList; - readerList.resize(mContentFiles.size()); - int index=0; + ESM::Dialogue* dialogue = nullptr; for (const auto & mContentFile : mContentFiles) { ESM::ESMReader lEsm; lEsm.setEncoder(nullptr); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&readerList); lEsm.open(mContentFile.string()); - readerList[index] = lEsm; - mEsmStore.load(readerList[index], &dummyListener); + mEsmStore.load(lEsm, &dummyListener, dialogue); ++index; } @@ -58,36 +60,37 @@ struct ContentFileTest : public ::testing::Test boost::program_options::options_description desc("Allowed options"); desc.add_options() - ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()->composing()) - ("content", boost::program_options::value >()->default_value(std::vector(), "") - ->multitoken(), "content file(s): esm/esp, or omwgame/omwaddon") - ("data-local", boost::program_options::value()->default_value("")); - - boost::program_options::notify(variables); + ("data", boost::program_options::value()->default_value(Files::MaybeQuotedPathContainer(), "data")->multitoken()->composing()) + ("content", boost::program_options::value>()->default_value(std::vector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + ("data-local", boost::program_options::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), "")); + Files::ConfigurationManager::addCommonOptions(desc); mConfigurationManager.readConfiguration(variables, desc, true); Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { - dataDirs = Files::PathContainer(variables["data"].as()); + dataDirs = asPathContainer(variables["data"].as()); } - std::string local = variables["data-local"].as(); - if (!local.empty()) { - dataLocal.push_back(Files::PathContainer::value_type(local)); - } + Files::PathContainer::value_type local(variables["data-local"].as()); + if (!local.empty()) + dataLocal.push_back(local); - mConfigurationManager.processPaths (dataDirs); - mConfigurationManager.processPaths (dataLocal, true); + mConfigurationManager.filterOutNonExistingPaths(dataDirs); + mConfigurationManager.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); Files::Collections collections (dataDirs, true); - std::vector contentFiles = variables["content"].as >(); + std::vector contentFiles = variables["content"].as>(); for (auto & contentFile : contentFiles) - mContentFiles.push_back(collections.getPath(contentFile)); + { + if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts")) + mContentFiles.push_back(collections.getPath(contentFile)); + } } protected: @@ -105,10 +108,8 @@ TEST_F(ContentFileTest, dialogue_merging_test) return; } - const std::string file = "test_dialogue_merging.txt"; - - boost::filesystem::ofstream stream; - stream.open(file); + const std::string file = TestingOpenMW::outputFilePath("test_dialogue_merging.txt"); + std::ofstream stream(file); const MWWorld::Store& dialStore = mEsmStore.get(); for (const auto & dial : dialStore) @@ -187,10 +188,8 @@ TEST_F(ContentFileTest, content_diagnostics_test) return; } - const std::string file = "test_content_diagnostics.txt"; - - boost::filesystem::ofstream stream; - stream.open(file); + const std::string file = TestingOpenMW::outputFilePath("test_content_diagnostics.txt"); + std::ofstream stream(file); RUN_TEST_FOR_TYPES(printRecords, mEsmStore, stream); @@ -224,17 +223,17 @@ protected: /// Create an ESM file in-memory containing the specified record. /// @param deleted Write record with deleted flag? template -Files::IStreamPtr getEsmFile(T record, bool deleted) +std::unique_ptr getEsmFile(T record, bool deleted) { ESM::ESMWriter writer; - auto* stream = new std::stringstream; + auto stream = std::make_unique(); writer.setFormat(0); writer.save(*stream); writer.startRecord(T::sRecordId); record.save(writer, deleted); writer.endRecord(T::sRecordId); - return Files::IStreamPtr(stream); + return stream; } /// Tests deletion of records. @@ -249,31 +248,26 @@ TEST_F(StoreTest, delete_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); + ESM::Dialogue* dialogue = nullptr; // master file inserts a record - Files::IStreamPtr file = getEsmFile(record, false); - reader.open(file, "filename"); - mEsmStore.load(reader, &dummyListener); + reader.open(getEsmFile(record, false), "filename"); + mEsmStore.load(reader, &dummyListener, dialogue); mEsmStore.setUp(); ASSERT_TRUE (mEsmStore.get().getSize() == 1); // now a plugin deletes it - file = getEsmFile(record, true); - reader.open(file, "filename"); - mEsmStore.load(reader, &dummyListener); + reader.open(getEsmFile(record, true), "filename"); + mEsmStore.load(reader, &dummyListener, dialogue); mEsmStore.setUp(); ASSERT_TRUE (mEsmStore.get().getSize() == 0); // now another plugin inserts it again // expected behaviour is the record to reappear rather than staying deleted - file = getEsmFile(record, false); - reader.open(file, "filename"); - mEsmStore.load(reader, &dummyListener); + reader.open(getEsmFile(record, false), "filename"); + mEsmStore.load(reader, &dummyListener, dialogue); mEsmStore.setUp(); ASSERT_TRUE (mEsmStore.get().getSize() == 1); @@ -292,22 +286,18 @@ TEST_F(StoreTest, overwrite_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); + ESM::Dialogue* dialogue = nullptr; // master file inserts a record - Files::IStreamPtr file = getEsmFile(record, false); - reader.open(file, "filename"); - mEsmStore.load(reader, &dummyListener); + reader.open(getEsmFile(record, false), "filename"); + mEsmStore.load(reader, &dummyListener, dialogue); mEsmStore.setUp(); // now a plugin overwrites it with changed data record.mId = recordIdUpper; // change id to uppercase, to test case smashing while we're at it record.mModel = "the_new_model"; - file = getEsmFile(record, false); - reader.open(file, "filename"); - mEsmStore.load(reader, &dummyListener); + reader.open(getEsmFile(record, false), "filename"); + mEsmStore.load(reader, &dummyListener, dialogue); mEsmStore.setUp(); // verify that changes were actually applied diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index 72dcd30664..21b9e9df3a 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -60,6 +60,19 @@ namespace { return isNear(lhs.getOrigin(), rhs.getOrigin()) && isNear(lhs.getBasis(), rhs.getBasis()); } + + struct WriteVec3f + { + osg::Vec3f mValue; + + friend std::ostream& operator <<(std::ostream& stream, const WriteVec3f& value) + { + return stream << "osg::Vec3f {" + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.x() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.y() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.z() << "}"; + } + }; } static std::ostream& operator <<(std::ostream& stream, const btVector3& value) @@ -122,6 +135,17 @@ static std::ostream& operator <<(std::ostream& stream, const TriangleMeshShape& return stream << "}}"; } +static bool operator ==(const BulletShape::CollisionBox& l, const BulletShape::CollisionBox& r) +{ + const auto tie = [] (const BulletShape::CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; + return tie(l) == tie(r); +} + +static std::ostream& operator <<(std::ostream& stream, const BulletShape::CollisionBox& value) +{ + return stream << "CollisionBox {" << WriteVec3f {value.mExtents} << ", " << WriteVec3f {value.mCenter} << "}"; +} + } static std::ostream& operator <<(std::ostream& stream, const btCollisionShape& value) @@ -160,20 +184,21 @@ namespace Resource { static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs) { - return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) - && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) - && lhs.mCollisionBoxHalfExtents == rhs.mCollisionBoxHalfExtents - && lhs.mCollisionBoxTranslate == rhs.mCollisionBoxTranslate + return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) + && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) + && lhs.mCollisionBox == rhs.mCollisionBox + && lhs.mCollisionType == rhs.mCollisionType && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } static std::ostream& operator <<(std::ostream& stream, const Resource::BulletShape& value) { return stream << "Resource::BulletShape {" - << value.mCollisionShape << ", " - << value.mAvoidCollisionShape << ", " - << "osg::Vec3f {" << value.mCollisionBoxHalfExtents << "}" << ", " + << value.mCollisionShape.get() << ", " + << value.mAvoidCollisionShape.get() << ", " + << value.mCollisionBox << ", " << value.mAnimatedShapes + << ", collisionType=" << value.mCollisionType << "}"; } } @@ -254,18 +279,29 @@ namespace value.flags = 0; init(value.trafo); value.hasBounds = false; - value.parent = nullptr; + value.parents.push_back(nullptr); value.isBone = false; } - void init(Nif::NiTriShape& value) + void init(Nif::NiGeometry& value) { init(static_cast(value)); - value.recType = Nif::RC_NiTriShape; - value.data = Nif::NiTriShapeDataPtr(nullptr); + value.data = Nif::NiGeometryDataPtr(nullptr); value.skin = Nif::NiSkinInstancePtr(nullptr); } + void init(Nif::NiTriShape& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriShape; + } + + void init(Nif::NiTriStrips& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriStrips; + } + void init(Nif::NiSkinInstance& value) { value.data = Nif::NiSkinDataPtr(nullptr); @@ -293,22 +329,23 @@ namespace struct NifFileMock : Nif::File { - MOCK_CONST_METHOD1(getRecord, Nif::Record* (std::size_t)); - MOCK_CONST_METHOD0(numRecords, std::size_t ()); - MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t)); - MOCK_CONST_METHOD0(numRoots, std::size_t ()); - MOCK_CONST_METHOD1(getString, std::string (uint32_t)); - MOCK_METHOD1(setUseSkinning, void (bool)); - MOCK_CONST_METHOD0(getUseSkinning, bool ()); - MOCK_CONST_METHOD0(getFilename, std::string ()); - MOCK_CONST_METHOD0(getVersion, unsigned int ()); - MOCK_CONST_METHOD0(getUserVersion, unsigned int ()); - MOCK_CONST_METHOD0(getBethVersion, unsigned int ()); + MOCK_METHOD(Nif::Record*, getRecord, (std::size_t), (const, override)); + MOCK_METHOD(std::size_t, numRecords, (), (const, override)); + MOCK_METHOD(Nif::Record*, getRoot, (std::size_t), (const, override)); + MOCK_METHOD(std::size_t, numRoots, (), (const, override)); + MOCK_METHOD(std::string, getString, (uint32_t), (const, override)); + MOCK_METHOD(void, setUseSkinning, (bool), (override)); + MOCK_METHOD(bool, getUseSkinning, (), (const, override)); + MOCK_METHOD(std::string, getFilename, (), (const, override)); + MOCK_METHOD(std::string, getHash, (), (const, override)); + MOCK_METHOD(unsigned int, getVersion, (), (const, override)); + MOCK_METHOD(unsigned int, getUserVersion, (), (const, override)); + MOCK_METHOD(unsigned int, getBethVersion, (), (const, override)); }; struct RecordMock : Nif::Record { - MOCK_METHOD1(read, void (Nif::NIFStream *nif)); + MOCK_METHOD(void, read, (Nif::NIFStream *nif), (override)); }; struct TestBulletNifLoader : Test @@ -324,6 +361,8 @@ namespace Nif::NiTriShape mNiTriShape; Nif::NiTriShapeData mNiTriShapeData2; Nif::NiTriShape mNiTriShape2; + Nif::NiTriStripsData mNiTriStripsData; + Nif::NiTriStrips mNiTriStrips; Nif::NiSkinInstance mNiSkinInstance; Nif::NiStringExtraData mNiStringExtraData; Nif::NiStringExtraData mNiStringExtraData2; @@ -345,6 +384,7 @@ namespace ), btVector3(4, 8, 12) }; + const std::string mHash = "hash"; TestBulletNifLoader() { @@ -355,24 +395,49 @@ namespace init(mNiNode3); init(mNiTriShape); init(mNiTriShape2); + init(mNiTriStrips); init(mNiSkinInstance); init(mNiStringExtraData); init(mNiStringExtraData2); 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.triangles = {0, 1, 2}; - mNiTriShape.data = Nif::NiTriShapeDataPtr(&mNiTriShapeData); + mNiTriShape.data = 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.triangles = {0, 1, 2}; - mNiTriShape2.data = Nif::NiTriShapeDataPtr(&mNiTriShapeData2); + mNiTriShape2.data = Nif::NiGeometryDataPtr(&mNiTriShapeData2); + + mNiTriStripsData.recType = Nif::RC_NiTriStripsData; + mNiTriStripsData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0)}; + mNiTriStripsData.strips = {{0, 1, 2, 3}}; + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, getHash()).WillOnce(Return(mHash)); } }; TEST_F(TestBulletNifLoader, for_zero_num_roots_should_return_default) { EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(0)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + EXPECT_EQ(result->mFileName, "test.nif"); + EXPECT_EQ(result->mFileHash, mHash); + } + + TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(nullptr)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; @@ -421,21 +486,23 @@ namespace TEST_F(TestBulletNifLoader, for_root_nif_node_with_bounding_box_should_return_shape_with_compound_shape_and_box_inside) { mNode.hasBounds = true; - mNode.flags |= Nif::NiNode::Flag_BBoxCollision; - mNode.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + 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 = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -443,22 +510,25 @@ namespace TEST_F(TestBulletNifLoader, for_child_nif_node_with_bounding_box) { mNode.hasBounds = true; - mNode.flags |= Nif::NiNode::Flag_BBoxCollision; - mNode.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + 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 = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -466,26 +536,30 @@ namespace TEST_F(TestBulletNifLoader, for_root_and_child_nif_node_with_bounding_box_but_root_without_flag_should_use_child_bounds) { mNode.hasBounds = true; - mNode.flags |= Nif::NiNode::Flag_BBoxCollision; - mNode.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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.hasBounds = true; - mNiNode.boundXYZ = osg::Vec3f(4, 5, 6); - mNiNode.boundPos = osg::Vec3f(-4, -5, -6); + 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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + 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 = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -493,30 +567,36 @@ namespace 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::NiNode::Flag_BBoxCollision; - mNode.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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.boundXYZ = osg::Vec3f(4, 5, 6); - mNode2.boundPos = osg::Vec3f(-4, -5, -6); + 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.boundXYZ = osg::Vec3f(7, 8, 9); - mNiNode.boundPos = osg::Vec3f(-7, -8, -9); + 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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + 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 = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -524,30 +604,36 @@ namespace 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.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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::NiNode::Flag_BBoxCollision; - mNode2.boundXYZ = osg::Vec3f(4, 5, 6); - mNode2.boundPos = osg::Vec3f(-4, -5, -6); + 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.boundXYZ = osg::Vec3f(7, 8, 9); - mNiNode.boundPos = osg::Vec3f(-7, -8, -9); + 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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(4, 5, 6); - expected.mCollisionBoxTranslate = osg::Vec3f(-4, -5, -6); + 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 = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -555,8 +641,9 @@ namespace TEST_F(TestBulletNifLoader, for_root_nif_node_with_bounds_but_without_flag_should_return_shape_with_bounds_but_with_null_collision_shape) { mNode.hasBounds = true; - mNode.boundXYZ = osg::Vec3f(1, 2, 3); - mNode.boundPos = osg::Vec3f(-1, -2, -3); + 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); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNode)); @@ -564,8 +651,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -580,7 +667,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -588,8 +675,9 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_root_node_with_bounds_should_return_shape_with_bounds_but_with_null_collision_shape) { mNiTriShape.hasBounds = true; - mNiTriShape.boundXYZ = osg::Vec3f(1, 2, 3); - mNiTriShape.boundPos = osg::Vec3f(-1, -2, -3); + 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); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriShape)); @@ -597,14 +685,15 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + 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_child_node_should_return_shape_with_triangle_mesh_shape) { + mNiTriShape.parents.push_back(&mNiNode); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -615,7 +704,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -623,7 +712,9 @@ namespace TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_shape_with_triangle_mesh_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); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); @@ -633,13 +724,15 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_shape_with_triangle_mesh_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) @@ -654,7 +747,7 @@ namespace triangles->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -662,6 +755,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_shape_with_triangle_mesh_shape) { mNiTriShape.skin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.parents.push_back(&mNiNode); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -672,7 +766,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -694,7 +788,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -704,7 +798,7 @@ namespace { copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; - mNiTriShape.parent = &mNiNode; + mNiTriShape.parents.push_back(&mNiNode); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.trafo.scale = 4; @@ -720,7 +814,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -730,9 +824,11 @@ namespace { 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); mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), @@ -758,7 +854,7 @@ namespace shape->addChildShape(mResultTransform, mesh.release()); shape->addChildShape(mResultTransform, mesh2.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -767,10 +863,10 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_controller_should_return_shape_with_compound_shape) { mController.recType = Nif::RC_NiKeyframeController; - mController.flags |= Nif::NiNode::ControllerFlag_Active; + mController.flags |= Nif::Controller::Flag_Active; copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; - mNiTriShape.parent = &mNiNode; + mNiTriShape.parents.push_back(&mNiNode); mNiTriShape.controller = Nif::ControllerPtr(&mController); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.trafo.scale = 4; @@ -787,7 +883,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -796,12 +892,13 @@ namespace TEST_F(TestBulletNifLoader, for_two_tri_shape_children_nodes_where_one_with_controller_should_return_shape_with_compound_shape) { mController.recType = Nif::RC_NiKeyframeController; - mController.flags |= Nif::NiNode::ControllerFlag_Active; + 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.parent = &mNiNode; + mNiTriShape2.parents.push_back(&mNiNode); mNiTriShape2.controller = Nif::ControllerPtr(&mController); mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), @@ -815,7 +912,7 @@ namespace const auto result = mLoader.load(mNifFile); std::unique_ptr triangles(new btTriangleMesh(false)); - triangles->addTriangle(btVector3(1, 2, 3), btVector3(4, 2, 3), btVector3(4, 4.632747650146484375, 1.56172335147857666015625)); + triangles->addTriangle(btVector3(4, 8, 12), btVector3(16, 8, 12), btVector3(16, 18.5309906005859375, 6.246893405914306640625)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); mesh->setLocalScaling(btVector3(1, 1, 1)); @@ -828,7 +925,35 @@ namespace shape->addChildShape(mResultTransform2, mesh2.release()); shape->addChildShape(btTransform::getIdentity(), mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } + + 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)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriShape2)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + 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)); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -836,6 +961,7 @@ 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)})); mNiNode.recType = Nif::RC_AvoidNode; @@ -847,14 +973,15 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false); + expected.mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), false)); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { - mNiTriShape.data = Nif::NiTriShapeDataPtr(nullptr); + mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.parents.push_back(&mNiNode); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -869,7 +996,27 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_triangles_should_return_shape_with_null_collision_shape) { - mNiTriShape.data->triangles.clear(); + auto data = static_cast(mNiTriShape.data.getPtr()); + data->triangles.clear(); + mNiTriShape.parents.push_back(&mNiNode); + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + { + mNiStringExtraData.string = "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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -877,16 +1024,44 @@ namespace EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + expected.mCollisionType = Resource::BulletShape::CollisionType::Camera; EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_null_collision_shape) + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + { + mNiStringExtraData.next = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.string = "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)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + expected.mCollisionType = Resource::BulletShape::CollisionType::Camera; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) { mNiStringExtraData.string = "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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -894,17 +1069,22 @@ namespace EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + expected.mCollisionType = Resource::BulletShape::CollisionType::None; EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_null_collision_shape) + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) { mNiStringExtraData.next = Nif::ExtraPtr(&mNiStringExtraData2); mNiStringExtraData2.string = "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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -912,7 +1092,41 @@ namespace EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); const auto result = mLoader.load(mNifFile); + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + expected.mCollisionType = Resource::BulletShape::CollisionType::None; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_empty_root_collision_node_without_nc_should_return_shape_with_cameraonly_collision) + { + Nif::NiTriShape niTriShape; + Nif::NiNode emptyCollisionNode; + init(niTriShape); + init(emptyCollisionNode); + + niTriShape.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + niTriShape.parents.push_back(&mNiNode); + + emptyCollisionNode.recType = Nif::RC_RootCollisionNode; + emptyCollisionNode.parents.push_back(&mNiNode); + + mNiNode.children = Nif::NodeList(std::vector( + {Nif::NodePtr(&niTriShape), Nif::NodePtr(&emptyCollisionNode)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + expected.mCollisionType = Resource::BulletShape::CollisionType::Camera; EXPECT_EQ(*result, expected); } @@ -922,6 +1136,7 @@ namespace 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)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -939,8 +1154,10 @@ namespace mNiStringExtraData.string = "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; @@ -952,7 +1169,187 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_shape_data_with_mismatching_data_rec_type) + { + mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriShape)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_shape_with_triangle_mesh_shape) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + 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)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_mismatching_data_rec_type) + { + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) + { + mNiTriStripsData.strips.clear(); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + 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)})); + mNiNode.recType = Nif::RC_AvoidNode; + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + 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)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + 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)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + 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()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } + + 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; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiNode2)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + 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); + const btTransform transform1 { + btMatrix3x3( + 1, 0, 0, + 0, 0.8004512795493964327775415767973754555, 0.59939782204119995689950428641168400645, + 0, -0.59939782204119995689950428641168400645, 0.8004512795493964327775415767973754555 + ), + btVector3(2, 4, 6) + }; + const btTransform transform2 { + btMatrix3x3( + 1, 0, 0, + 0, 0.79515431915808965079861536651151254773, 0.60640713116208888600056070572463795543, + 0, -0.60640713116208888600056070572463795543, 0.79515431915808965079861536651151254773 + ), + btVector3(3, 6, 9) + }; + shape->addChildShape(transform1, mesh1.release()); + shape->addChildShape(transform2, mesh2.release()); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); } diff --git a/apps/openmw_test_suite/openmw/options.cpp b/apps/openmw_test_suite/openmw/options.cpp new file mode 100644 index 0000000000..33c38da5df --- /dev/null +++ b/apps/openmw_test_suite/openmw/options.cpp @@ -0,0 +1,398 @@ +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace OpenMW; + + namespace bpo = boost::program_options; + + template + std::string makeString(const T (&range)[size]) + { + static_assert(size > 0); + return std::string(std::begin(range), std::end(range) - 1); + } + + template + std::vector generateSupportedCharacters(Args&& ... args) + { + std::vector result; + (result.emplace_back(makeString(args)) , ...); + for (int i = 1; i <= std::numeric_limits::max(); ++i) + if (i != '&' && i != '"' && i != ' ' && i != '\n') + result.push_back(std::string(1, i)); + return result; + } + + MATCHER_P(IsPath, v, "") { return arg.string() == v; } + + template + void parseArgs(const T& arguments, bpo::variables_map& variables, bpo::options_description& description) + { + Files::parseArgs(static_cast(arguments.size()), arguments.data(), variables, description); + } + + TEST(OpenMWOptionsFromArguments, should_support_equality_to_separate_flag_and_value) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame=save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_single_word_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "/home/user/openmw/save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "/home/user/openmw/save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_windows_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(C:\OpenMW\save.omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(C:\OpenMW\save.omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_spaces) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_octothorpe) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my#save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my#save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_at_sign) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "my@save.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my@save.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_quote) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(my"save.omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(my"save.omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save".omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save)"); + } + + TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save&".omwsave")"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"("save.omwsave&&")"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave&"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "save&.omwsave"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save&.omwsave"); + } + + TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_multiple_quotes) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", R"(my"save".omwsave)"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(my"save".omwsave)"); + } + + TEST(OpenMWOptionsFromArguments, should_compose_data) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--data", "1", "--data", "2"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromArguments, should_compose_data_from_single_flag) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--data", "1", "2"}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromArguments, should_throw_on_multiple_load_savegame) + { + bpo::options_description description = makeOptionsDescription(); + const std::array arguments {"openmw", "--load-savegame", "1.omwsave", "--load-savegame", "2.omwsave"}; + bpo::variables_map variables; + EXPECT_THROW(parseArgs(arguments, variables, description), std::exception); + } + + struct OpenMWOptionsFromArgumentsStrings : TestWithParam {}; + + TEST_P(OpenMWOptionsFromArgumentsStrings, should_support_paths_with_certain_characters_in_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::string path = "save_" + std::string(GetParam()) + ".omwsave"; + const std::string pathArgument = "\"" + path + "\""; + const std::array arguments {"openmw", "--load-savegame", pathArgument.c_str()}; + bpo::variables_map variables; + parseArgs(arguments, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), path); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedCharacters, + OpenMWOptionsFromArgumentsStrings, + ValuesIn(generateSupportedCharacters(u8"👍", u8"Ъ", u8"Ǽ", "\n")) + ); + + TEST(OpenMWOptionsFromConfig, should_support_single_word_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save.omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_strip_outer_quotes_from_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=""save".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), ""); + } + + TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path_with_space) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="my save.omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "my save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_octothorpe) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save#.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save#.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_at_sign) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save@.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save@.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_quote) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=save".omwsave)"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_support_confusing_savegame_path_with_lots_going_on) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="one &"two"three".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(one "two)"); + } + + TEST(OpenMWOptionsFromConfig, should_support_confusing_savegame_path_with_even_more_going_on) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="one &"two"three ".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(one "two)"); + } + + TEST(OpenMWOptionsFromConfig, should_ignore_commented_option) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("#load-savegame=save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), ""); + } + + TEST(OpenMWOptionsFromConfig, should_ignore_whitespace_prefixed_commented_option) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(" \t#load-savegame=save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), ""); + } + + TEST(OpenMWOptionsFromConfig, should_support_whitespace_around_option) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(" load-savegame = save.omwsave "); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_throw_on_multiple_load_savegame) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=1.omwsave\nload-savegame=2.omwsave"); + bpo::variables_map variables; + EXPECT_THROW(Files::parseConfig(stream, variables, description), std::exception); + } + + TEST(OpenMWOptionsFromConfig, should_support_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=/home/user/openmw/save.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "/home/user/openmw/save.omwsave"); + } + + TEST(OpenMWOptionsFromConfig, should_support_windows_multi_component_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame=C:\OpenMW\save.omwsave)"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(C:\OpenMW\save.omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_compose_data) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("data=1\ndata=2"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_THAT(variables["data"].as(), ElementsAre(IsPath("1"), IsPath("2"))); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save&".omwsave")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), R"(save".omwsave)"); + } + + TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream(R"(load-savegame="save.omwsave&&")"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save.omwsave&"); + } + + TEST(OpenMWOptionsFromConfig, should_support_load_savegame_path_with_ampersand) + { + bpo::options_description description = makeOptionsDescription(); + std::istringstream stream("load-savegame=save&.omwsave"); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), "save&.omwsave"); + } + + struct OpenMWOptionsFromConfigStrings : TestWithParam {}; + + TEST_P(OpenMWOptionsFromConfigStrings, should_support_paths_with_certain_characters_in_load_savegame_path) + { + bpo::options_description description = makeOptionsDescription(); + const std::string path = "save_" + std::string(GetParam()) + ".omwsave"; + std::istringstream stream("load-savegame=\"" + path + "\""); + bpo::variables_map variables; + Files::parseConfig(stream, variables, description); + EXPECT_EQ(variables["load-savegame"].as().string(), path); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedCharacters, + OpenMWOptionsFromConfigStrings, + ValuesIn(generateSupportedCharacters(u8"👍", u8"Ъ", u8"Ǽ")) + ); +} diff --git a/apps/openmw_test_suite/serialization/binaryreader.cpp b/apps/openmw_test_suite/serialization/binaryreader.cpp new file mode 100644 index 0000000000..b78c673d8f --- /dev/null +++ b/apps/openmw_test_suite/serialization/binaryreader.cpp @@ -0,0 +1,90 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue) + { + std::uint32_t value = 42; + std::vector data(sizeof(value)); + std::memcpy(data.data(), &value, sizeof(value)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + binaryReader(format, result); + EXPECT_EQ(result, 42u); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeRangeValue) + { + const std::size_t count = 3; + std::vector data(sizeof(std::size_t) + count * sizeof(std::uint32_t)); + std::memcpy(data.data(), &count, sizeof(count)); + const std::uint32_t value1 = 960900021; + std::memcpy(data.data() + sizeof(count), &value1, sizeof(std::uint32_t)); + const std::uint32_t value2 = 1235496234; + std::memcpy(data.data() + sizeof(count) + sizeof(std::uint32_t), &value2, sizeof(std::uint32_t)); + const std::uint32_t value3 = 2342038092; + std::memcpy(data.data() + sizeof(count) + 2 * sizeof(std::uint32_t), &value3, sizeof(std::uint32_t)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::size_t resultCount = 0; + const TestFormat format; + binaryReader(format, resultCount); + std::vector result(resultCount); + binaryReader(format, result.data(), result.size()); + EXPECT_THAT(result, ElementsAre(value1, value2, value3)); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeShouldThrowException) + { + std::vector data(3); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + EXPECT_THROW(binaryReader(format, result), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeRangeShouldThrowException) + { + std::vector data(7); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::vector values(2); + const TestFormat format; + EXPECT_THROW(binaryReader(format, values.data(), values.size()), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldSetPointerToCurrentBufferPosition) + { + std::vector data(8); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + const std::byte* ptr = nullptr; + const TestFormat format; + binaryReader(format, ptr); + EXPECT_EQ(ptr, data.data()); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldNotAdvanceAfterPointer) + { + std::vector data(8); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + const std::byte* ptr1 = nullptr; + const std::byte* ptr2 = nullptr; + const TestFormat format; + binaryReader(format, ptr1); + binaryReader(format, ptr2); + EXPECT_EQ(ptr1, data.data()); + EXPECT_EQ(ptr2, data.data()); + } +} diff --git a/apps/openmw_test_suite/serialization/binarywriter.cpp b/apps/openmw_test_suite/serialization/binarywriter.cpp new file mode 100644 index 0000000000..cb0f29ba85 --- /dev/null +++ b/apps/openmw_test_suite/serialization/binarywriter.cpp @@ -0,0 +1,57 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue) + { + std::vector result(4); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + binaryWriter(format, std::uint32_t(42)); + EXPECT_THAT(result, ElementsAre(std::byte(42), std::byte(0), std::byte(0), std::byte(0))); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeRangeValue) + { + std::vector result(8); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + binaryWriter(format, values.data(), values.size()); + constexpr std::array expected { + std::byte(42), std::byte(0), std::byte(0), std::byte(0), + std::byte(13), std::byte(0), std::byte(0), std::byte(0), + }; + EXPECT_THAT(result, ElementsAreArray(expected)); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeShouldThrowException) + { + std::vector result(3); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, std::uint32_t(42)), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeRangeShouldThrowException) + { + std::vector result(7); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/serialization/format.hpp b/apps/openmw_test_suite/serialization/format.hpp new file mode 100644 index 0000000000..603d2790e0 --- /dev/null +++ b/apps/openmw_test_suite/serialization/format.hpp @@ -0,0 +1,76 @@ +#ifndef OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H +#define OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H + +#include + +#include +#include +#include + +namespace SerializationTesting +{ + struct Pod + { + int mInt = 42; + double mDouble = 3.14; + + friend bool operator==(const Pod& l, const Pod& r) + { + const auto tuple = [] (const Pod& v) { return std::tuple(v.mInt, v.mDouble); }; + return tuple(l) == tuple(r); + } + }; + + enum Enum : std::int32_t + { + A, + B, + C, + }; + + struct Composite + { + short mFloatArray[3] = {0}; + std::vector mIntVector; + std::vector mEnumVector; + std::vector mPodVector; + std::size_t mPodDataSize = 0; + std::vector mPodBuffer; + std::size_t mCharDataSize = 0; + std::vector mCharBuffer; + }; + + template + struct TestFormat : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Pod>> + { + visitor(*this, value.mInt); + visitor(*this, value.mDouble); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Composite>> + { + visitor(*this, value.mFloatArray); + visitor(*this, value.mIntVector); + visitor(*this, value.mEnumVector); + visitor(*this, value.mPodVector); + visitor(*this, value.mPodDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mPodBuffer.resize(value.mPodDataSize); + visitor(*this, value.mPodBuffer.data(), value.mPodDataSize); + visitor(*this, value.mCharDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mCharBuffer.resize(value.mCharDataSize); + visitor(*this, value.mCharBuffer.data(), value.mCharDataSize); + } + }; +} + +#endif diff --git a/apps/openmw_test_suite/serialization/integration.cpp b/apps/openmw_test_suite/serialization/integration.cpp new file mode 100644 index 0000000000..cb8c711c67 --- /dev/null +++ b/apps/openmw_test_suite/serialization/integration.cpp @@ -0,0 +1,56 @@ +#include "format.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + struct DetourNavigatorSerializationIntegrationTest : Test + { + Composite mComposite; + + DetourNavigatorSerializationIntegrationTest() + { + mComposite.mIntVector = {4, 5, 6}; + mComposite.mEnumVector = {Enum::A, Enum::B, Enum::C}; + mComposite.mPodVector = {Pod {4, 23.87}, Pod {5, -31.76}, Pod {6, 65.12}}; + mComposite.mPodBuffer = {Pod {7, 456.123}, Pod {8, -628.346}}; + mComposite.mPodDataSize = mComposite.mPodBuffer.size(); + std::string charData = "serialization"; + mComposite.mCharBuffer = {charData.begin(), charData.end()}; + mComposite.mCharDataSize = charData.size(); + } + }; + + TEST_F(DetourNavigatorSerializationIntegrationTest, sizeAccumulatorShouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + TestFormat{}(sizeAccumulator, mComposite); + EXPECT_EQ(sizeAccumulator.value(), 143); + } + + TEST_F(DetourNavigatorSerializationIntegrationTest, binaryReaderShouldDeserializeDataWrittenByBinaryWriter) + { + std::vector data(143); + TestFormat{}(BinaryWriter(data.data(), data.data() + data.size()), mComposite); + Composite result; + TestFormat{}(BinaryReader(data.data(), data.data() + data.size()), result); + EXPECT_EQ(result.mIntVector, mComposite.mIntVector); + EXPECT_EQ(result.mEnumVector, mComposite.mEnumVector); + EXPECT_EQ(result.mPodVector, mComposite.mPodVector); + EXPECT_EQ(result.mPodDataSize, mComposite.mPodDataSize); + EXPECT_EQ(result.mPodBuffer, mComposite.mPodBuffer); + EXPECT_EQ(result.mCharDataSize, mComposite.mCharDataSize); + EXPECT_EQ(result.mCharBuffer, mComposite.mCharBuffer); + } +} diff --git a/apps/openmw_test_suite/serialization/sizeaccumulator.cpp b/apps/openmw_test_suite/serialization/sizeaccumulator.cpp new file mode 100644 index 0000000000..dce148468a --- /dev/null +++ b/apps/openmw_test_suite/serialization/sizeaccumulator.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType) + { + SizeAccumulator sizeAccumulator; + constexpr std::monostate format; + sizeAccumulator(format, std::uint32_t()); + EXPECT_EQ(sizeAccumulator.value(), 4); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticTypeRange) + { + SizeAccumulator sizeAccumulator; + const std::uint64_t* const data = nullptr; + const std::size_t count = 3; + const std::monostate format; + sizeAccumulator(format, data, count); + EXPECT_EQ(sizeAccumulator.value(), 24); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + const TestFormat format; + sizeAccumulator(format, Pod {}); + EXPECT_EQ(sizeAccumulator.value(), 12); + } +} diff --git a/apps/openmw_test_suite/settings/parser.cpp b/apps/openmw_test_suite/settings/parser.cpp index d54360fc28..7e250b43aa 100644 --- a/apps/openmw_test_suite/settings/parser.cpp +++ b/apps/openmw_test_suite/settings/parser.cpp @@ -1,9 +1,11 @@ #include -#include +#include #include +#include "../testing_util.hpp" + namespace { using namespace testing; @@ -17,11 +19,11 @@ namespace template void withSettingsFile( const std::string& content, F&& f) { - const auto path = std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".cfg"; + std::string path = TestingOpenMW::outputFilePath( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".cfg"); { - boost::filesystem::ofstream stream; - stream.open(path); + std::ofstream stream(path); stream << content; stream.close(); } diff --git a/apps/openmw_test_suite/settings/shadermanager.cpp b/apps/openmw_test_suite/settings/shadermanager.cpp new file mode 100644 index 0000000000..e4f40aaa03 --- /dev/null +++ b/apps/openmw_test_suite/settings/shadermanager.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include + +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + using namespace Settings; + + struct ShaderSettingsTest : Test + { + template + void withSettingsFile( const std::string& content, F&& f) + { + std::string path = TestingOpenMW::outputFilePath( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".yaml"); + + { + std::ofstream stream; + stream.open(path); + stream << content; + stream.close(); + } + + f(path); + } + }; + + TEST_F(ShaderSettingsTest, fail_to_fetch_then_set_and_succeed) + { + const std::string content = +R"YAML( +config: + shader: + vec3_uniform: [1.0, 2.0] +)YAML"; + + withSettingsFile(content, [] (const auto& path) + { + EXPECT_TRUE(ShaderManager::get().load(path)); + EXPECT_FALSE(ShaderManager::get().getValue("shader", "vec3_uniform").has_value()); + EXPECT_TRUE(ShaderManager::get().setValue("shader", "vec3_uniform", osg::Vec3f(1, 2, 3))); + EXPECT_TRUE(ShaderManager::get().getValue("shader", "vec3_uniform").has_value()); + EXPECT_EQ(ShaderManager::get().getValue("shader", "vec3_uniform").value(), osg::Vec3f(1, 2, 3)); + EXPECT_TRUE(ShaderManager::get().save()); + }); + } + + TEST_F(ShaderSettingsTest, fail_to_load_file_then_fail_to_set_and_get) + { + const std::string content = +R"YAML( +config: + shader: + uniform: 12.0 + >Defeated by a sideways carrot +)YAML"; + + withSettingsFile(content, [] (const auto& path) + { + EXPECT_FALSE(ShaderManager::get().load(path)); + EXPECT_FALSE(ShaderManager::get().setValue("shader", "uniform", 12.0)); + EXPECT_FALSE(ShaderManager::get().getValue("shader", "uniform").has_value()); + EXPECT_FALSE(ShaderManager::get().save()); + }); + } +} \ No newline at end of file diff --git a/apps/openmw_test_suite/shader/parsefors.cpp b/apps/openmw_test_suite/shader/parsefors.cpp index 330feb172d..4e64042365 100644 --- a/apps/openmw_test_suite/shader/parsefors.cpp +++ b/apps/openmw_test_suite/shader/parsefors.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace { @@ -16,6 +17,12 @@ namespace const std::string mName = "shader"; }; + static bool parseFors(std::string& source, const std::string& templateName) + { + std::vector dummy; + return parseDirectives(source, dummy, {}, {}, templateName); + } + TEST_F(ShaderParseForsTest, empty_should_succeed) { ASSERT_TRUE(parseFors(mSource, mName)); diff --git a/apps/openmw_test_suite/shader/parselinks.cpp b/apps/openmw_test_suite/shader/parselinks.cpp new file mode 100644 index 0000000000..2e3697ba50 --- /dev/null +++ b/apps/openmw_test_suite/shader/parselinks.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + using DefineMap = ShaderManager::DefineMap; + + struct ShaderParseLinksTest : Test + { + std::string mSource; + std::vector mLinkTargets; + ShaderManager::DefineMap mDefines; + const std::string mName = "my_shader.glsl"; + + bool parseLinks() + { + return parseDirectives(mSource, mLinkTargets, mDefines, {}, mName); + } + }; + + TEST_F(ShaderParseLinksTest, empty_should_succeed) + { + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, ""); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_for_single_escape_symbol) + { + mSource = "$"; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$"); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_on_first_found_escaped_not_valid_directive) + { + mSource = "$foo "; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$foo "); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_on_absent_link_target) + { + mSource = "$link "; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$link "); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_not_require_newline) + { + mSource = "$link \"foo.glsl\""; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, ""); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } + + TEST_F(ShaderParseLinksTest, should_require_quotes) + { + mSource = "$link foo.glsl"; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$link foo.glsl"); + EXPECT_EQ(mLinkTargets.size(), 0); + } + + TEST_F(ShaderParseLinksTest, should_be_replaced_with_empty_line) + { + mSource = "$link \"foo.glsl\"\nbar"; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, "\nbar"); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } + + TEST_F(ShaderParseLinksTest, should_only_accept_on_true_condition) + { + mSource = +R"glsl( +$link "foo.glsl" if 1 +$link "bar.glsl" if 0 +)glsl"; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, +R"glsl( + + +)glsl"); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } +} diff --git a/apps/openmw_test_suite/shader/shadermanager.cpp b/apps/openmw_test_suite/shader/shadermanager.cpp index a25e5e9ba6..58ca3b6f3f 100644 --- a/apps/openmw_test_suite/shader/shadermanager.cpp +++ b/apps/openmw_test_suite/shader/shadermanager.cpp @@ -1,9 +1,11 @@ #include -#include +#include #include +#include "../testing_util.hpp" + namespace { using namespace testing; @@ -28,11 +30,11 @@ namespace template void withShaderFile(const std::string& suffix, const std::string& content, F&& f) { - const auto path = UnitTest::GetInstance()->current_test_info()->name() + suffix + ".glsl"; + std::string path = TestingOpenMW::outputFilePath( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + suffix + ".glsl"); { - boost::filesystem::ofstream stream; - stream.open(path); + std::ofstream stream(path); stream << content; stream.close(); } diff --git a/apps/openmw_test_suite/sqlite3/db.cpp b/apps/openmw_test_suite/sqlite3/db.cpp new file mode 100644 index 0000000000..d2c0cd8674 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/db.cpp @@ -0,0 +1,15 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + TEST(Sqlite3DbTest, makeDbShouldCreateInMemoryDbWithSchema) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + EXPECT_NE(db, nullptr); + } +} diff --git a/apps/openmw_test_suite/sqlite3/request.cpp b/apps/openmw_test_suite/sqlite3/request.cpp new file mode 100644 index 0000000000..3bcf658c0d --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/request.cpp @@ -0,0 +1,269 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + template + struct InsertInt + { + static std::string_view text() noexcept { return "INSERT INTO ints (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, T value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertReal + { + static std::string_view text() noexcept { return "INSERT INTO reals (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, double value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertText + { + static std::string_view text() noexcept { return "INSERT INTO texts (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertBlob + { + static std::string_view text() noexcept { return "INSERT INTO blobs (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::vector& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetAll + { + std::string mQuery; + + explicit GetAll(const std::string& table) : mQuery("SELECT value FROM " + table + " ORDER BY value") {} + + std::string_view text() noexcept { return mQuery; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + template + struct GetExact + { + std::string mQuery; + + explicit GetExact(const std::string& table) : mQuery("SELECT value FROM " + table + " WHERE value = :value") {} + + std::string_view text() noexcept { return mQuery; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const T& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetInt64 + { + static std::string_view text() noexcept { return "SELECT value FROM ints WHERE value = :value"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::int64_t value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetNull + { + static std::string_view text() noexcept { return "SELECT NULL"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Int + { + int mValue = 0; + + Int() = default; + + explicit Int(int value) : mValue(value) {} + + Int& operator=(int value) + { + mValue = value; + return *this; + } + + friend bool operator==(const Int& l, const Int& r) + { + return l.mValue == r.mValue; + } + }; + + constexpr const char schema[] = R"( + CREATE TABLE ints ( value INTEGER ); + CREATE TABLE reals ( value REAL ); + CREATE TABLE texts ( value TEXT ); + CREATE TABLE blobs ( value BLOB ); + )"; + + struct Sqlite3RequestTest : Test + { + const Db mDb = makeDb(":memory:", schema); + }; + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(13), std::tuple(42))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + EXPECT_EQ(execute(*mDb, insert, 3.14), 1); + Statement select(*mDb, GetAll("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(3.14))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetAll("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetAll("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + const double value = 3.14; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetExact("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), text); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetExact>("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), blob); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportNull) + { + Statement select(*mDb, GetNull {}); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(nullptr))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportConstructibleFromInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(Int(value)))); + } + + TEST_F(Sqlite3RequestTest, requestShouldLimitOutput) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), 1); + EXPECT_THAT(result, ElementsAre(std::tuple(13))); + } +} diff --git a/apps/openmw_test_suite/sqlite3/statement.cpp b/apps/openmw_test_suite/sqlite3/statement.cpp new file mode 100644 index 0000000000..fb53ceebb2 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/statement.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct Query + { + static std::string_view text() noexcept { return "SELECT 1"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + TEST(Sqlite3StatementTest, makeStatementShouldCreateStatementWithPreparedQuery) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + const Statement statement(*db, Query {}); + EXPECT_FALSE(statement.mNeedReset); + EXPECT_NE(statement.mHandle, nullptr); + EXPECT_EQ(statement.mQuery.text(), "SELECT 1"); + } +} diff --git a/apps/openmw_test_suite/sqlite3/transaction.cpp b/apps/openmw_test_suite/sqlite3/transaction.cpp new file mode 100644 index 0000000000..913fd34bce --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/transaction.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct InsertId + { + static std::string_view text() noexcept { return "INSERT INTO test (id) VALUES (42)"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct GetIds + { + static std::string_view text() noexcept { return "SELECT id FROM test"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Sqlite3TransactionTest : Test + { + const Db mDb = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + + void insertId() const + { + Statement insertId(*mDb, InsertId {}); + EXPECT_EQ(execute(*mDb, insertId), 1); + } + + std::vector> getIds() const + { + Statement getIds(*mDb, GetIds {}); + std::vector> result; + request(*mDb, getIds, std::back_inserter(result), std::numeric_limits::max()); + return result; + } + }; + + TEST_F(Sqlite3TransactionTest, shouldRollbackOnDestruction) + { + { + const Transaction transaction(*mDb); + insertId(); + } + EXPECT_THAT(getIds(), IsEmpty()); + } + + TEST_F(Sqlite3TransactionTest, commitShouldCommitTransaction) + { + { + Transaction transaction(*mDb); + insertId(); + transaction.commit(); + } + EXPECT_THAT(getIds(), ElementsAre(std::tuple(42))); + } +} diff --git a/apps/openmw_test_suite/testing_util.hpp b/apps/openmw_test_suite/testing_util.hpp new file mode 100644 index 0000000000..125d2505ed --- /dev/null +++ b/apps/openmw_test_suite/testing_util.hpp @@ -0,0 +1,77 @@ +#ifndef TESTING_UTIL_H +#define TESTING_UTIL_H + +#include +#include + +#include +#include + +namespace TestingOpenMW +{ + + inline std::string outputFilePath(const std::string name) + { + std::filesystem::path dir("tests_output"); + std::filesystem::create_directory(dir); + return (dir / name).string(); + } + + inline std::string temporaryFilePath(const std::string name) + { + return (std::filesystem::temp_directory_path() / name).string(); + } + + class VFSTestFile : public VFS::File + { + public: + explicit VFSTestFile(std::string content) : mContent(std::move(content)) {} + + Files::IStreamPtr open() override + { + return std::make_unique(mContent, std::ios_base::in); + } + + std::string getPath() override + { + return "TestFile"; + } + + private: + const std::string mContent; + }; + + struct VFSTestData : public VFS::Archive + { + std::map mFiles; + + VFSTestData(std::map files) : mFiles(std::move(files)) {} + + void listResources(std::map& out, char (*normalize_function) (char)) override + { + out = mFiles; + } + + bool contains(const std::string& file, char (*normalize_function) (char)) const override + { + return mFiles.count(file) != 0; + } + + std::string getDescription() const override { return "TestData"; } + + }; + + inline std::unique_ptr createTestVFS(std::map files) + { + auto vfs = std::make_unique(true); + vfs->addArchive(std::make_unique(std::move(files))); + vfs->buildIndex(); + return vfs; + } + + #define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \ + catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); } + +} + +#endif // TESTING_UTIL_H diff --git a/components/to_utf8/tests/test_data/french-utf8.txt b/apps/openmw_test_suite/toutf8/data/french-utf8.txt similarity index 100% rename from components/to_utf8/tests/test_data/french-utf8.txt rename to apps/openmw_test_suite/toutf8/data/french-utf8.txt diff --git a/components/to_utf8/tests/test_data/french-win1252.txt b/apps/openmw_test_suite/toutf8/data/french-win1252.txt similarity index 100% rename from components/to_utf8/tests/test_data/french-win1252.txt rename to apps/openmw_test_suite/toutf8/data/french-win1252.txt diff --git a/components/to_utf8/tests/test_data/russian-utf8.txt b/apps/openmw_test_suite/toutf8/data/russian-utf8.txt similarity index 100% rename from components/to_utf8/tests/test_data/russian-utf8.txt rename to apps/openmw_test_suite/toutf8/data/russian-utf8.txt diff --git a/components/to_utf8/tests/test_data/russian-win1251.txt b/apps/openmw_test_suite/toutf8/data/russian-win1251.txt similarity index 100% rename from components/to_utf8/tests/test_data/russian-win1251.txt rename to apps/openmw_test_suite/toutf8/data/russian-win1251.txt diff --git a/apps/openmw_test_suite/toutf8/toutf8.cpp b/apps/openmw_test_suite/toutf8/toutf8.cpp new file mode 100644 index 0000000000..d1cf6b2851 --- /dev/null +++ b/apps/openmw_test_suite/toutf8/toutf8.cpp @@ -0,0 +1,159 @@ +#include + +#include + +#include + +#ifndef OPENMW_TEST_SUITE_SOURCE_DIR +#define OPENMW_TEST_SUITE_SOURCE_DIR "" +#endif + +namespace +{ + using namespace testing; + using namespace ToUTF8; + + struct Params + { + FromType mLegacyEncoding; + std::string mLegacyEncodingFileName; + std::string mUtf8FileName; + }; + + std::string readContent(const std::string& fileName) + { + std::ifstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(std::string(OPENMW_TEST_SUITE_SOURCE_DIR) + "/toutf8/data/" + fileName); + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + struct Utf8EncoderTest : TestWithParam {}; + + TEST(Utf8EncoderTest, getUtf8ShouldReturnEmptyAsIs) + { + Utf8Encoder encoder(FromType::CP437); + EXPECT_EQ(encoder.getUtf8(std::string_view()), std::string_view()); + } + + TEST(Utf8EncoderTest, getUtf8ShouldReturnAsciiOnlyAsIs) + { + std::string input; + for (int c = 1; c <= std::numeric_limits::max(); ++c) + input.push_back(c); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result.data(), input.data()); + EXPECT_EQ(result.size(), input.size()); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilZero) + { + const std::string input("a\0b"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result, "a"); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilEndOfInputForAscii) + { + const std::string input("abc"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "ab"); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilEndOfInputForNonAscii) + { + const std::string input("a\x92" "b"); + Utf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view result = encoder.getUtf8(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "a\xE2\x80\x99"); + } + + TEST_P(Utf8EncoderTest, getUtf8ShouldConvertFromLegacyEncodingToUtf8) + { + const std::string input(readContent(GetParam().mLegacyEncodingFileName)); + const std::string expected(readContent(GetParam().mUtf8FileName)); + Utf8Encoder encoder(GetParam().mLegacyEncoding); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result, expected); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldReturnEmptyAsIs) + { + Utf8Encoder encoder(FromType::CP437); + EXPECT_EQ(encoder.getLegacyEnc(std::string_view()), std::string_view()); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldReturnAsciiOnlyAsIs) + { + std::string input; + for (int c = 1; c <= std::numeric_limits::max(); ++c) + input.push_back(c); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result.data(), input.data()); + EXPECT_EQ(result.size(), input.size()); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldLookUpUntilZero) + { + const std::string input("a\0b"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result, "a"); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldLookUpUntilEndOfInputForAscii) + { + const std::string input("abc"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "ab"); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldStripIncompleteCharacters) + { + const std::string input("a\xc3\xa2\xe2\x80\x99"); + Utf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view result = encoder.getLegacyEnc(std::string_view(input.data(), 5)); + EXPECT_EQ(result, "a\xe2"); + } + + TEST_P(Utf8EncoderTest, getLegacyEncShouldConvertFromUtf8ToLegacyEncoding) + { + const std::string input(readContent(GetParam().mUtf8FileName)); + const std::string expected(readContent(GetParam().mLegacyEncodingFileName)); + Utf8Encoder encoder(GetParam().mLegacyEncoding); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result, expected); + } + + INSTANTIATE_TEST_SUITE_P(Files, Utf8EncoderTest, Values( + Params {ToUTF8::WINDOWS_1251, "russian-win1251.txt", "russian-utf8.txt"}, + Params {ToUTF8::WINDOWS_1252, "french-win1252.txt", "french-utf8.txt"} + )); + + TEST(StatelessUtf8EncoderTest, shouldCleanupBuffer) + { + std::string buffer; + StatelessUtf8Encoder encoder(FromType::WINDOWS_1252); + encoder.getUtf8(std::string_view("long string\x92"), BufferAllocationPolicy::UseGrowFactor, buffer); + const std::string shortString("short\x92"); + ASSERT_GT(buffer.size(), shortString.size()); + const std::string_view shortUtf8 = encoder.getUtf8(shortString, BufferAllocationPolicy::UseGrowFactor, buffer); + ASSERT_GE(buffer.size(), shortUtf8.size()); + EXPECT_EQ(buffer[shortUtf8.size()], '\0') << buffer; + } + + TEST(StatelessUtf8EncoderTest, withFitToRequiredSizeShouldResizeBuffer) + { + std::string buffer; + StatelessUtf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view utf8 = encoder.getUtf8(std::string_view("long string\x92"), BufferAllocationPolicy::FitToRequiredSize, buffer); + EXPECT_EQ(buffer.size(), utf8.size()); + } +} diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 10e06d1ff0..03cd63480a 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,4 +1,3 @@ - set(WIZARD componentselectionpage.cpp conclusionpage.cpp @@ -34,21 +33,6 @@ set(WIZARD_HEADER utils/componentlistwidget.hpp ) -# Headers that must be pre-processed -set(WIZARD_HEADER_MOC - componentselectionpage.hpp - conclusionpage.hpp - existinginstallationpage.hpp - importpage.hpp - installationtargetpage.hpp - intropage.hpp - languageselectionpage.hpp - mainwizard.hpp - methodselectionpage.hpp - - utils/componentlistwidget.hpp -) - set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui @@ -63,7 +47,6 @@ set(WIZARD_UI if (OPENMW_USE_UNSHIELD) set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) - set (WIZARD_HEADER_MOC ${WIZARD_HEADER_MOC} installationpage.hpp unshield/unshieldworker.hpp) set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) add_definitions(-DOPENMW_USE_UNSHIELD) endif (OPENMW_USE_UNSHIELD) @@ -80,7 +63,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) -QT5_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${WIZARD_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -94,12 +76,11 @@ openmw_add_executable(openmw-wizard ${WIZARD} ${WIZARD_HEADER} ${RCC_SRCS} - ${MOC_SRCS} ${UI_HDRS} ) target_link_libraries(openmw-wizard - components + components_qt ) target_link_libraries(openmw-wizard Qt5::Widgets Qt5::Core) @@ -125,3 +106,7 @@ endif() if (WIN32) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION ".") endif(WIN32) + +if(USE_QT) + set_property(TARGET openmw-wizard PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp index 2509b9f5ed..961669ab57 100644 --- a/apps/wizard/componentselectionpage.hpp +++ b/apps/wizard/componentselectionpage.hpp @@ -1,8 +1,6 @@ #ifndef COMPONENTSELECTIONPAGE_HPP #define COMPONENTSELECTIONPAGE_HPP -#include - #include "ui_componentselectionpage.h" namespace Wizard diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp index f5f27dfcab..72af2cceb1 100644 --- a/apps/wizard/conclusionpage.hpp +++ b/apps/wizard/conclusionpage.hpp @@ -1,8 +1,6 @@ #ifndef CONCLUSIONPAGE_HPP #define CONCLUSIONPAGE_HPP -#include - #include "ui_conclusionpage.h" namespace Wizard diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 2434f42cfe..35f3c8c257 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -1,6 +1,5 @@ #include "existinginstallationpage.hpp" -#include #include #include #include @@ -95,9 +94,9 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() { QString selectedFile = QFileDialog::getOpenFileName( this, - tr("Select master file"), + tr("Select Morrowind.esm (located in Data Files)"), QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)")), + QString(tr("Morrowind master file (Morrowind.esm)")), nullptr, QFileDialog::DontResolveSymlinks); @@ -110,7 +109,23 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() return; if (!mWizard->findFiles(QLatin1String("Morrowind"), info.absolutePath())) - return; // No valid Morrowind installation found + { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error detecting Morrowind files")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(QObject::tr( + "Morrowind.bsa is missing!
\ + Make sure your Morrowind installation is complete." + )); + msgBox.exec(); + return; + } + + if (!versionIsOK(info.absolutePath())) + { + return; + } QString path(QDir::toNativeSeparators(info.absolutePath())); QList items = installationsList->findItems(path, Qt::MatchExactly); @@ -154,3 +169,36 @@ int Wizard::ExistingInstallationPage::nextId() const { return MainWizard::Page_LanguageSelection; } + +bool Wizard::ExistingInstallationPage::versionIsOK(QString directory_name) +{ + QDir directory = QDir(directory_name); + QFileInfoList infoList = directory.entryInfoList(QStringList(QString("Morrowind.bsa"))); + if (infoList.size() == 1) + { + qint64 actualFileSize = infoList.at(0).size(); + const qint64 expectedFileSize = 310459500; // Size of Morrowind.bsa in Steam and GOG editions. + + if (actualFileSize == expectedFileSize) + { + return true; + } + + QMessageBox msgBox; + msgBox.setWindowTitle(QObject::tr("Most recent Morrowind not detected")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + msgBox.setText(QObject::tr("
There may be a more recent version of Morrowind available.

\ + Do you wish to continue anyway?
")); + int ret = msgBox.exec(); + if (ret == QMessageBox::Yes) + { + return true; + } + + return false; + } + + return false; +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp index bb229e2498..6bdc5df134 100644 --- a/apps/wizard/existinginstallationpage.hpp +++ b/apps/wizard/existinginstallationpage.hpp @@ -1,10 +1,10 @@ #ifndef EXISTINGINSTALLATIONPAGE_HPP #define EXISTINGINSTALLATIONPAGE_HPP -#include - #include "ui_existinginstallationpage.h" +#include + namespace Wizard { class MainWizard; @@ -27,9 +27,10 @@ namespace Wizard private: MainWizard *mWizard; + bool versionIsOK(QString directory_name); + protected: void initializePage() override; - }; } diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp index 412d39ac18..b1f26409a3 100644 --- a/apps/wizard/importpage.hpp +++ b/apps/wizard/importpage.hpp @@ -1,8 +1,6 @@ #ifndef IMPORTPAGE_HPP #define IMPORTPAGE_HPP -#include - #include "ui_importpage.h" namespace Wizard diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index d4a63c6761..198d1df6e7 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -1,7 +1,5 @@ #include "inisettings.hpp" -#include - #include #include #include @@ -124,7 +122,7 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) QString section(fullKey.at(0)); section.prepend(QLatin1Char('[')); section.append(QLatin1Char(']')); - QString key(fullKey.at(1)); + const QString& key(fullKey.at(1)); int index = buffer.lastIndexOf(section); if (index == -1) { diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index 9c90b0bbf7..0df0dbb57a 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,15 +1,15 @@ #include "installationpage.hpp" -#include #include -#include #include #include +#include #include "mainwizard.hpp" -Wizard::InstallationPage::InstallationPage(QWidget *parent) : - QWizardPage(parent) +Wizard::InstallationPage::InstallationPage(QWidget *parent, Config::GameSettings &gameSettings) : + QWizardPage(parent), + mGameSettings(gameSettings) { mWizard = qobject_cast(parent); @@ -17,36 +17,40 @@ Wizard::InstallationPage::InstallationPage(QWidget *parent) : mFinished = false; - mThread = new QThread(); - mUnshield = new UnshieldWorker(); - mUnshield->moveToThread(mThread); + mThread = std::make_unique(); + mUnshield = std::make_unique(mGameSettings.value("morrowind-bsa-filesize").toLongLong()); + mUnshield->moveToThread(mThread.get()); - connect(mThread, SIGNAL(started()), - mUnshield, SLOT(extract())); + connect(mThread.get(), SIGNAL(started()), + mUnshield.get(), SLOT(extract())); - connect(mUnshield, SIGNAL(finished()), - mThread, SLOT(quit())); + connect(mUnshield.get(), SIGNAL(finished()), + mThread.get(), SLOT(quit())); - connect(mUnshield, SIGNAL(finished()), + connect(mUnshield.get(), SIGNAL(finished()), this, SLOT(installationFinished()), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(error(QString, QString)), + connect(mUnshield.get(), SIGNAL(error(QString, QString)), this, SLOT(installationError(QString, QString)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(textChanged(QString)), + connect(mUnshield.get(), SIGNAL(textChanged(QString)), installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(textChanged(QString)), + connect(mUnshield.get(), SIGNAL(textChanged(QString)), logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(textChanged(QString)), + connect(mUnshield.get(), SIGNAL(textChanged(QString)), mWizard, SLOT(addLogText(QString)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(progressChanged(int)), + connect(mUnshield.get(), SIGNAL(progressChanged(int)), installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); - connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), + connect(mUnshield.get(), SIGNAL(requestFileDialog(Wizard::Component)), this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); + + connect(mUnshield.get(), SIGNAL(requestOldVersionDialog()), + this, SLOT(showOldVersionDialog()) + , Qt::QueuedConnection); } Wizard::InstallationPage::~InstallationPage() @@ -56,9 +60,6 @@ Wizard::InstallationPage::~InstallationPage() mThread->quit(); mThread->wait(); } - - delete mUnshield; - delete mThread; } void Wizard::InstallationPage::initializePage() @@ -181,6 +182,34 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) mUnshield->setDiskPath(path); } +void Wizard::InstallationPage::showOldVersionDialog() +{ + logTextEdit->appendHtml(tr("

Detected old version of component Morrowind.

")); + mWizard->addLogText(tr("Detected old version of component Morrowind.")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Morrowind Installation")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setText(QObject::tr("There may be a more recent version of Morrowind available.

Do you wish to continue anyway?")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + + int ret = msgBox.exec(); + if (ret == QMessageBox::No) + { + logTextEdit->appendHtml(tr("


\ + Error: The installation was aborted by the user

")); + + mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); + mWizard->mError = true; + + emit completeChanged(); + return; + } + + mUnshield->wakeAll(); +} + void Wizard::InstallationPage::installationFinished() { QMessageBox msgBox; diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp index cc1ccf5599..d2380956cc 100644 --- a/apps/wizard/installationpage.hpp +++ b/apps/wizard/installationpage.hpp @@ -1,11 +1,14 @@ #ifndef INSTALLATIONPAGE_HPP #define INSTALLATIONPAGE_HPP +#include + #include #include "unshield/unshieldworker.hpp" #include "ui_installationpage.h" #include "inisettings.hpp" +#include class QThread; @@ -19,8 +22,8 @@ namespace Wizard { Q_OBJECT public: - InstallationPage(QWidget *parent); - ~InstallationPage(); + InstallationPage(QWidget *parent, Config::GameSettings &gameSettings); + ~InstallationPage() override; int nextId() const override; bool isComplete() const override; @@ -29,13 +32,16 @@ namespace Wizard MainWizard *mWizard; bool mFinished; - QThread* mThread; - UnshieldWorker *mUnshield; + std::unique_ptr mThread; + std::unique_ptr mUnshield; void startInstallation(); + Config::GameSettings &mGameSettings; + private slots: void showFileDialog(Wizard::Component component); + void showOldVersionDialog(); void installationFinished(); void installationError(const QString &text, const QString &details); diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp index 7cba295733..1f1e952280 100644 --- a/apps/wizard/installationtargetpage.hpp +++ b/apps/wizard/installationtargetpage.hpp @@ -1,8 +1,6 @@ #ifndef INSTALLATIONTARGETPAGE_HPP #define INSTALLATIONTARGETPAGE_HPP -#include - #include "ui_installationtargetpage.h" namespace Files diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp index c8cd690167..3fae6a747f 100644 --- a/apps/wizard/intropage.hpp +++ b/apps/wizard/intropage.hpp @@ -1,8 +1,6 @@ #ifndef INTROPAGE_HPP #define INTROPAGE_HPP -#include - #include "ui_intropage.h" namespace Wizard diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp index cc86ba9b37..81f8faa485 100644 --- a/apps/wizard/languageselectionpage.hpp +++ b/apps/wizard/languageselectionpage.hpp @@ -1,8 +1,6 @@ #ifndef LANGUAGESELECTIONPAGE_HPP #define LANGUAGESELECTIONPAGE_HPP -#include - #include "ui_languageselectionpage.h" namespace Wizard diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index e3624742a0..67410df147 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -1,7 +1,5 @@ #include -#include #include -#include #include "mainwizard.hpp" diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 160c6f7217..840027323a 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -1,9 +1,7 @@ #include "mainwizard.hpp" #include - -#include -#include +#include #include #include #include @@ -56,6 +54,9 @@ Wizard::MainWizard::MainWizard(QWidget *parent) :

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

"); + boost::filesystem::create_directories(mCfgMgr.getUserConfigPath()); + boost::filesystem::create_directories(mCfgMgr.getUserDataPath()); + setupLog(); setupGameSettings(); setupLauncherSettings(); @@ -88,8 +89,9 @@ void Wizard::MainWizard::setupLog() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(mLogError.arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } addLogText(QString("Started OpenMW Wizard on %1").arg(QDateTime::currentDateTime().toString())); @@ -110,8 +112,9 @@ void Wizard::MainWizard::addLogText(const QString &text) msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(mLogError.arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } if (!file.isSequential()) @@ -148,8 +151,9 @@ void Wizard::MainWizard::setupGameSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(message.arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); @@ -157,6 +161,8 @@ void Wizard::MainWizard::setupGameSettings() mGameSettings.readUserFile(stream); } + file.close(); + // Now the rest QStringList paths; paths.append(userPath + QLatin1String("openmw.cfg")); @@ -175,8 +181,9 @@ void Wizard::MainWizard::setupGameSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(message.arg(file.fileName())); - - return qApp->quit(); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); + msgBox.exec(); + return; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); @@ -208,8 +215,9 @@ void Wizard::MainWizard::setupLauncherSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(message.arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); @@ -319,7 +327,7 @@ void Wizard::MainWizard::setupPages() setPage(Page_InstallationTarget, new InstallationTargetPage(this, mCfgMgr)); setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); #ifdef OPENMW_USE_UNSHIELD - setPage(Page_Installation, new InstallationPage(this)); + setPage(Page_Installation, new InstallationPage(this, mGameSettings)); #endif setPage(Page_Import, new ImportPage(this)); setPage(Page_Conclusion, new ConclusionPage(this)); @@ -392,8 +400,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setText(tr("

Could not create %1

\

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

").arg(userPath)); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } } @@ -409,8 +418,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setText(tr("

Could not open %1 for writing

\

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

").arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } QTextStream stream(&file); @@ -431,8 +441,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setText(tr("

Could not open %1 for writing

\

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

").arg(file.fileName())); + connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); - return qApp->quit(); + return; } stream.setDevice(&file); diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp index 8d9623baa1..a8dec32da7 100644 --- a/apps/wizard/mainwizard.hpp +++ b/apps/wizard/mainwizard.hpp @@ -1,9 +1,7 @@ #ifndef MAINWIZARD_HPP #define MAINWIZARD_HPP -#include #include -#include #include @@ -41,8 +39,8 @@ namespace Wizard Page_Conclusion }; - MainWizard(QWidget *parent = 0); - ~MainWizard(); + MainWizard(QWidget *parent = nullptr); + ~MainWizard() override; bool findFiles(const QString &name, const QString &path); void addInstallation(const QString &path); diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index e00344af93..37234468b9 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -1,6 +1,9 @@ #include "methodselectionpage.hpp" #include "mainwizard.hpp" +#include +#include + Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : QWizardPage(parent) { @@ -11,9 +14,12 @@ Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : #ifndef OPENMW_USE_UNSHIELD retailDiscRadioButton->setEnabled(false); existingLocationRadioButton->setChecked(true); + buyLinkButton->released(); #endif - + registerField(QLatin1String("installation.retailDisc"), retailDiscRadioButton); + + connect(buyLinkButton, SIGNAL(released()), this, SLOT(handleBuyButton())); } int Wizard::MethodSelectionPage::nextId() const @@ -24,3 +30,8 @@ int Wizard::MethodSelectionPage::nextId() const return MainWizard::Page_ExistingInstallation; } } + +void Wizard::MethodSelectionPage::handleBuyButton() +{ + QDesktopServices::openUrl(QUrl("https://openmw.org/faq/#do_i_need_morrowind")); +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp index c189ea171f..e844024b48 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -1,8 +1,6 @@ #ifndef METHODSELECTIONPAGE_HPP #define METHODSELECTIONPAGE_HPP -#include - #include "ui_methodselectionpage.h" namespace Wizard @@ -17,6 +15,9 @@ namespace Wizard int nextId() const override; + private slots: + void handleBuyButton(); + private: MainWizard *mWizard; diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index f84658bf35..8ff0690667 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -1,20 +1,17 @@ #include "unshieldworker.hpp" #include - #include -#include -#include #include #include #include #include #include #include -#include -Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : +Wizard::UnshieldWorker::UnshieldWorker(qint64 expectedMorrowindBsaSize, QObject *parent) : QObject(parent), + mExpectedMorrowindBsaSize(expectedMorrowindBsaSize), mIniSettings() { unshield_set_log_level(0); @@ -160,6 +157,11 @@ void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) mIniCodec = codec; } +void Wizard::UnshieldWorker::wakeAll() +{ + mWait.wakeAll(); +} + bool Wizard::UnshieldWorker::setupSettings() { // Create Morrowind.ini settings map @@ -479,6 +481,18 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) // Check if we have correct archive, other archives have Morrowind.bsa too if (tribunalFound == bloodmoonFound) { + qint64 actualFileSize = getMorrowindBsaFileSize(file); + if (actualFileSize != mExpectedMorrowindBsaSize) + { + QReadLocker readLock(&mLock); + emit requestOldVersionDialog(); + mWait.wait(&mLock); + if (mStopped) + { + qDebug() << "We are asked to stop !!"; + break; + } + } cabFile = file; found = true; // We have a GoTY disk or a Morrowind-only disk } @@ -493,6 +507,11 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) } + if (cabFile.isEmpty()) + { + break; + } + if (!found) { emit textChanged(tr("Failed to find a valid archive containing %1.bsa! Retrying.").arg(name)); @@ -940,3 +959,42 @@ bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &c unshield_close(unshield); return false; } + +size_t Wizard::UnshieldWorker::getMorrowindBsaFileSize(const QString &cabFile) +{ + QString fileName = QString("Morrowind.bsa"); + QByteArray array(cabFile.toUtf8()); + + Unshield *unshield; + unshield = unshield_open(array.constData()); + + if (!unshield) + { + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + unshield_close(unshield); + return false; + } + + for (int i = 0; i < unshield_file_group_count(unshield); ++i) + { + UnshieldFileGroup *group = unshield_file_group_get(unshield, i); + + for (size_t j = group->first_file; j <= group->last_file; ++j) + { + + if (unshield_file_is_valid(unshield, j)) + { + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); + if (current.toLower() == fileName.toLower()) + { + size_t fileSize = unshield_file_size(unshield, j); + unshield_close(unshield); + return fileSize; // File is found! + } + } + } + } + + unshield_close(unshield); + return 0; +} diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp index 3f922ad78a..6e37e734d6 100644 --- a/apps/wizard/unshield/unshieldworker.hpp +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -2,8 +2,6 @@ #define UNSHIELDWORKER_HPP #include -#include -#include #include #include #include @@ -12,6 +10,7 @@ #include "../inisettings.hpp" +#include namespace Wizard { @@ -26,8 +25,8 @@ namespace Wizard Q_OBJECT public: - UnshieldWorker(QObject *parent = 0); - ~UnshieldWorker(); + UnshieldWorker(qint64 expectedMorrowindBsaSize, QObject *parent = nullptr); + ~UnshieldWorker() override; void stopWorker(); @@ -38,6 +37,8 @@ namespace Wizard void setPath(const QString &path); void setIniPath(const QString &path); + void wakeAll(); + QString getPath(); QString getIniPath(); @@ -45,8 +46,9 @@ namespace Wizard bool setupSettings(); - private: + size_t getMorrowindBsaFileSize(const QString &cabFile); + private: bool writeSettings(); bool getInstallComponent(Component component); @@ -95,6 +97,8 @@ namespace Wizard bool mStopped; + qint64 mExpectedMorrowindBsaSize; + QString mPath; QString mIniPath; QString mDiskPath; @@ -113,6 +117,7 @@ namespace Wizard signals: void finished(); void requestFileDialog(Wizard::Component component); + void requestOldVersionDialog(); void textChanged(const QString &text); diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp index 6a5d019b5e..ff37e5d8ca 100644 --- a/apps/wizard/utils/componentlistwidget.cpp +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -1,7 +1,6 @@ #include "componentlistwidget.hpp" #include -#include ComponentListWidget::ComponentListWidget(QWidget *parent) : QListWidget(parent) diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp index 23965f8a6b..be15ea49f7 100644 --- a/apps/wizard/utils/componentlistwidget.hpp +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -10,7 +10,7 @@ class ComponentListWidget : public QListWidget Q_PROPERTY(QStringList mCheckedItems READ checkedItems) public: - ComponentListWidget(QWidget *parent = 0); + ComponentListWidget(QWidget *parent = nullptr); QStringList mCheckedItems; QStringList checkedItems(); diff --git a/appveyor.yml b/appveyor.yml index e2c13ed948..95f070a662 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,6 @@ environment: APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 platform: -# - Win32 - x64 configuration: @@ -22,26 +21,20 @@ configuration: - Release # - RelWithDebInfo -# For the Qt, Boost, CMake, etc installs -#os: Visual Studio 2017 - # We want the git revision for versioning, # so shallow clones don't work. clone_depth: 1 cache: - - C:\projects\openmw\deps\Bullet-2.87-msvc2015-win32.7z - - C:\projects\openmw\deps\Bullet-2.87-msvc2015-win64.7z - - C:\projects\openmw\deps\MyGUI-3.2.2-msvc2015-win32.7z - - C:\projects\openmw\deps\MyGUI-3.2.2-msvc2015-win64.7z - - C:\projects\openmw\deps\OSG-3.4.1-scrawl-msvc2015-win32.7z - - C:\projects\openmw\deps\OSG-3.4.1-scrawl-msvc2015-win64.7z - - C:\projects\openmw\deps\ffmpeg-3.2.4-dev-win32.zip - - C:\projects\openmw\deps\ffmpeg-3.2.4-dev-win64.zip - - C:\projects\openmw\deps\ffmpeg-3.2.4-win32.zip - - C:\projects\openmw\deps\ffmpeg-3.2.4-win64.zip - - C:\projects\openmw\deps\OpenAL-Soft-1.19.1.zip - - C:\projects\openmw\deps\SDL2-2.0.7.zip + - C:\projects\openmw\deps\Bullet-2.89-msvc2017-win64-double.7z + - C:\projects\openmw\deps\MyGUI-3.4.1-msvc2017-win64.7z + - C:\projects\openmw\deps\MyGUI-3.4.1-msvc2019-win64.7z + - C:\projects\openmw\deps\OSGoS-3.6.5-b02abe2-msvc2017-win64.7z + - C:\projects\openmw\deps\OSGoS-3.6.5-b02abe2-msvc2019-win64.7z + - C:\projects\openmw\deps\ffmpeg-4.2.2-dev-win64.zip + - C:\projects\openmw\deps\ffmpeg-4.2.2-win64.zip + - C:\projects\openmw\deps\OpenAL-Soft-1.20.1.zip + - C:\projects\openmw\deps\SDL2-2.0.18.zip clone_folder: C:\projects\openmw @@ -53,7 +46,6 @@ before_build: - cmd: sh %APPVEYOR_BUILD_FOLDER%\CI\before_script.msvc.sh -c %configuration% -p %PLATFORM% -v %msvc% -V -i %APPVEYOR_BUILD_FOLDER%\install build_script: - - cmd: if %PLATFORM%==Win32 set build=MSVC%msvc%_32 - cmd: if %PLATFORM%==x64 set build=MSVC%msvc%_64 - cmd: msbuild %build%\OpenMW.sln /t:Build /p:Configuration=%configuration% /m:2 /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - cmd: cmake --install %build% --config %configuration% diff --git a/cmake/CheckBulletPrecision.cmake b/cmake/CheckBulletPrecision.cmake new file mode 100644 index 0000000000..77409f9326 --- /dev/null +++ b/cmake/CheckBulletPrecision.cmake @@ -0,0 +1,26 @@ +set(TMP_ROOT ${CMAKE_BINARY_DIR}/try-compile) +file(MAKE_DIRECTORY ${TMP_ROOT}) + +file(WRITE ${TMP_ROOT}/checkbullet.cpp +" +#include +int main(int argc, char** argv) +{ + btSphereShape shape(1.0); + btScalar mass(1.0); + btVector3 inertia; + shape.calculateLocalInertia(mass, inertia); + return 0; +} +") + +message(STATUS "Checking if Bullet uses double precision") + +try_compile(RESULT_VAR + ${TMP_ROOT}/temp + ${TMP_ROOT}/checkbullet.cpp + COMPILE_DEFINITIONS "-DBT_USE_DOUBLE_PRECISION" + LINK_LIBRARIES ${BULLET_LIBRARIES} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${BULLET_INCLUDE_DIRS}" + ) +set(HAS_DOUBLE_PRECISION_BULLET ${RESULT_VAR}) diff --git a/cmake/CheckOsgMultiview.cmake b/cmake/CheckOsgMultiview.cmake new file mode 100644 index 0000000000..1dcea31a32 --- /dev/null +++ b/cmake/CheckOsgMultiview.cmake @@ -0,0 +1,26 @@ +set(TMP_ROOT ${CMAKE_BINARY_DIR}/try-compile) +file(MAKE_DIRECTORY ${TMP_ROOT}) + +file(WRITE ${TMP_ROOT}/checkmultiview.cpp +" +#include +int main(void) +{ + (void)osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER; + return 0; +} +") + +message(STATUS "Checking if OSG supports multiview") + +try_compile(RESULT_VAR + ${TMP_ROOT}/temp + ${TMP_ROOT}/checkmultiview.cpp + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${OPENSCENEGRAPH_INCLUDE_DIRS}" + ) +set(HAVE_MULTIVIEW ${RESULT_VAR}) +if(HAVE_MULTIVIEW) + message(STATUS "Osg supports multiview") +else(HAVE_MULTIVIEW) + message(NOTICE "Osg does not support multiview, disabling use of GL_OVR_multiview") +endif(HAVE_MULTIVIEW) diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake index 8d73242423..bc3410969f 100644 --- a/cmake/FindGMock.cmake +++ b/cmake/FindGMock.cmake @@ -1,15 +1,11 @@ # Get the Google C++ Mocking Framework. -# (This file is almost an copy of the original FindGTest.cmake file, -# altered to download and compile GMock and GTest if not found -# in GMOCK_ROOT or GTEST_ROOT respectively, +# (This file is almost an copy of the original FindGTest.cmake file for GMock, # feel free to use it as it is or modify it for your own needs.) # # Defines the following variables: # # GMOCK_FOUND - Found or got the Google Mocking framework -# GTEST_FOUND - Found or got the Google Testing framework # GMOCK_INCLUDE_DIRS - GMock include directory -# GTEST_INCLUDE_DIRS - GTest include direcotry # # Also defines the library variables below as normal variables # @@ -17,14 +13,8 @@ # GMOCK_LIBRARIES - libgmock # GMOCK_MAIN_LIBRARIES - libgmock-main # -# GTEST_BOTH_LIBRARIES - Both libgtest & libgtest_main -# GTEST_LIBRARIES - libgtest -# GTEST_MAIN_LIBRARIES - libgtest_main -# # Accepts the following variables as input: # -# GMOCK_ROOT - The root directory of the gmock install prefix -# GTEST_ROOT - The root directory of the gtest install prefix # GMOCK_SRC_DIR -The directory of the gmock sources # GMOCK_VER - The version of the gmock sources to be downloaded # @@ -101,48 +91,6 @@ # # * Kitware, Inc. #============================================================================= -# Thanks to Daniel Blezek for the GTEST_ADD_TESTS code - -function(gtest_add_tests executable extra_args) - if(NOT ARGN) - message(FATAL_ERROR "Missing ARGN: Read the documentation for GTEST_ADD_TESTS") - endif() - if(ARGN STREQUAL "AUTO") - # obtain sources used for building that executable - get_property(ARGN TARGET ${executable} PROPERTY SOURCES) - endif() - set(gtest_case_name_regex ".*\\( *([A-Za-z_0-9]+) *, *([A-Za-z_0-9]+) *\\).*") - set(gtest_test_type_regex "(TYPED_TEST|TEST_?[FP]?)") - foreach(source ${ARGN}) - file(READ "${source}" contents) - string(REGEX MATCHALL "${gtest_test_type_regex} *\\(([A-Za-z_0-9 ,]+)\\)" found_tests ${contents}) - foreach(hit ${found_tests}) - string(REGEX MATCH "${gtest_test_type_regex}" test_type ${hit}) - - # Parameterized tests have a different signature for the filter - if("x${test_type}" STREQUAL "xTEST_P") - string(REGEX REPLACE ${gtest_case_name_regex} "*/\\1.\\2/*" test_name ${hit}) - elseif("x${test_type}" STREQUAL "xTEST_F" OR "x${test_type}" STREQUAL "xTEST") - string(REGEX REPLACE ${gtest_case_name_regex} "\\1.\\2" test_name ${hit}) - elseif("x${test_type}" STREQUAL "xTYPED_TEST") - string(REGEX REPLACE ${gtest_case_name_regex} "\\1/*.\\2" test_name ${hit}) - else() - message(WARNING "Could not parse GTest ${hit} for adding to CTest.") - continue() - endif() - add_test(NAME ${test_name} COMMAND ${executable} --gtest_filter=${test_name} ${extra_args}) - endforeach() - endforeach() -endfunction() - -function(_append_debugs _endvar _library) - if(${_library} AND ${_library}_DEBUG) - set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) - else() - set(_output ${${_library}}) - endif() - set(${_endvar} ${_output} PARENT_SCOPE) -endfunction() function(_gmock_find_library _name) find_library(${_name} @@ -155,38 +103,20 @@ function(_gmock_find_library _name) mark_as_advanced(${_name}) endfunction() -function(_gtest_find_library _name) - find_library(${_name} - NAMES ${ARGN} - HINTS - ENV GTEST_ROOT - ${GTEST_ROOT} - PATH_SUFFIXES ${_gtest_libpath_suffixes} - ) - mark_as_advanced(${_name}) -endfunction() - if(NOT DEFINED GMOCK_MSVC_SEARCH) set(GMOCK_MSVC_SEARCH MD) endif() set(_gmock_libpath_suffixes lib) -set(_gtest_libpath_suffixes lib) if(MSVC) if(GMOCK_MSVC_SEARCH STREQUAL "MD") list(APPEND _gmock_libpath_suffixes msvc/gmock-md/Debug msvc/gmock-md/Release) - list(APPEND _gtest_libpath_suffixes - msvc/gtest-md/Debug - msvc/gtest-md/Release) elseif(GMOCK_MSVC_SEARCH STREQUAL "MT") list(APPEND _gmock_libpath_suffixes msvc/gmock/Debug msvc/gmock/Release) - list(APPEND _gtest_libpath_suffixes - msvc/gtest/Debug - msvc/gtest/Release) endif() endif() @@ -197,13 +127,6 @@ find_path(GMOCK_INCLUDE_DIR gmock/gmock.h ) mark_as_advanced(GMOCK_INCLUDE_DIR) -find_path(GTEST_INCLUDE_DIR gtest/gtest.h - HINTS - $ENV{GTEST_ROOT}/include - ${GTEST_ROOT}/include - ) -mark_as_advanced(GTEST_INCLUDE_DIR) - if(MSVC AND GMOCK_MSVC_SEARCH STREQUAL "MD") # The provided /MD project files for Google Mock add -md suffixes to the # library names. @@ -211,28 +134,12 @@ if(MSVC AND GMOCK_MSVC_SEARCH STREQUAL "MD") _gmock_find_library(GMOCK_LIBRARY_DEBUG gmock-mdd gmockd) _gmock_find_library(GMOCK_MAIN_LIBRARY gmock_main-md gmock_main) _gmock_find_library(GMOCK_MAIN_LIBRARY_DEBUG gmock_main-mdd gmock_maind) - - _gtest_find_library(GTEST_LIBRARY gtest-md gtest) - _gtest_find_library(GTEST_LIBRARY_DEBUG gtest-mdd gtestd) - _gtest_find_library(GTEST_MAIN_LIBRARY gtest_main-md gtest_main) - _gtest_find_library(GTEST_MAIN_LIBRARY_DEBUG gtest_main-mdd gtest_maind) else() _gmock_find_library(GMOCK_LIBRARY gmock) _gmock_find_library(GMOCK_LIBRARY_DEBUG gmockd) _gmock_find_library(GMOCK_MAIN_LIBRARY gmock_main) _gmock_find_library(GMOCK_MAIN_LIBRARY_DEBUG gmock_maind) - _gtest_find_library(GTEST_LIBRARY gtest) - _gtest_find_library(GTEST_LIBRARY_DEBUG gtestd) - _gtest_find_library(GTEST_MAIN_LIBRARY gtest_main) - _gtest_find_library(GTEST_MAIN_LIBRARY_DEBUG gtest_maind) -endif() - -if(NOT TARGET GTest::GTest) - add_library(GTest::GTest UNKNOWN IMPORTED) -endif() -if(NOT TARGET GTest::Main) - add_library(GTest::Main UNKNOWN IMPORTED) endif() if(NOT TARGET GMock::GMock) @@ -244,228 +151,29 @@ if(NOT TARGET GMock::Main) endif() set(GMOCK_LIBRARY_EXISTS OFF) -set(GTEST_LIBRARY_EXISTS OFF) if(EXISTS "${GMOCK_LIBRARY}" OR EXISTS "${GMOCK_LIBRARY_DEBUG}" AND GMOCK_INCLUDE_DIR) set(GMOCK_LIBRARY_EXISTS ON) endif() -if(EXISTS "${GTEST_LIBRARY}" OR EXISTS "${GTEST_LIBRARY_DEBUG}" AND GTEST_INCLUDE_DIR) - set(GTEST_LIBRARY_EXISTS ON) -endif() - -if(NOT (${GMOCK_LIBRARY_EXISTS} AND ${GTEST_LIBRARY_EXISTS})) - - include(ExternalProject) - - if(GTEST_USE_STATIC_LIBS) - set(GTEST_CMAKE_ARGS -Dgtest_force_shared_crt:BOOL=ON -DBUILD_SHARED_LIBS=OFF) - if(BUILD_SHARED_LIBS) - list(APPEND GTEST_CMAKE_ARGS - -DCMAKE_POSITION_INDEPENDENT_CODE=ON - -Dgtest_hide_internal_symbols=ON - -DCMAKE_CXX_VISIBILITY_PRESET=hidden - -DCMAKE_VISIBILITY_INLINES_HIDDEN=ON - -DCMAKE_POLICY_DEFAULT_CMP0063=NEW - ) - endif() - set(GTEST_LIBRARY_PREFIX ${CMAKE_STATIC_LIBRARY_PREFIX}) - else() - set(GTEST_CMAKE_ARGS -DBUILD_SHARED_LIBS=ON) - set(GTEST_LIBRARY_PREFIX ${CMAKE_SHARED_LIBRARY_PREFIX}) - endif() - if(WIN32) - list(APPEND GTEST_CMAKE_ARGS -Dgtest_disable_pthreads=ON) - endif() - - if("${GMOCK_SRC_DIR}" STREQUAL "") - message(STATUS "Downloading GMock / GTest version ${GMOCK_VER} from git") - if("${GMOCK_VER}" STREQUAL "1.6.0" OR "${GMOCK_VER}" STREQUAL "1.7.0") - set(GTEST_BIN_DIR "${GMOCK_ROOT}/src/gtest-build") - set(GTEST_LIBRARY "${GTEST_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GTEST_MAIN_LIBRARY "${GTEST_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - mark_as_advanced(GTEST_LIBRARY) - mark_as_advanced(GTEST_MAIN_LIBRARY) - - externalproject_add( - gtest - GIT_REPOSITORY "https://github.com/google/googletest.git" - GIT_TAG "release-${GMOCK_VER}" - PREFIX ${GMOCK_ROOT} - INSTALL_COMMAND "" - LOG_DOWNLOAD ON - LOG_CONFIGURE ON - LOG_BUILD ON - CMAKE_ARGS - ${GTEST_CMAKE_ARGS} - BINARY_DIR ${GTEST_BIN_DIR} - BUILD_BYPRODUCTS - "${GTEST_LIBRARY}" - "${GTEST_MAIN_LIBRARY}" - ) - - set(GMOCK_BIN_DIR "${GMOCK_ROOT}/src/gmock-build") - set(GMOCK_LIBRARY "${GMOCK_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GMOCK_MAIN_LIBRARY "${GMOCK_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - mark_as_advanced(GMOCK_LIBRARY) - mark_as_advanced(GMOCK_MAIN_LIBRARY) - - externalproject_add( - gmock - GIT_REPOSITORY "https://github.com/google/googlemock.git" - GIT_TAG "release-${GMOCK_VER}" - PREFIX ${GMOCK_ROOT} - INSTALL_COMMAND "" - LOG_DOWNLOAD ON - LOG_CONFIGURE ON - LOG_BUILD ON - CMAKE_ARGS - ${GTEST_CMAKE_ARGS} - BINARY_DIR ${GMOCK_BIN_DIR} - BUILD_BYPRODUCTS - "${GMOCK_LIBRARY}" - "${GMOCK_MAIN_LIBRARY}" - ) - - add_dependencies(gmock gtest) - - add_dependencies(GTest::GTest gtest) - add_dependencies(GTest::Main gtest) - add_dependencies(GMock::GMock gmock) - add_dependencies(GMock::Main gmock) - - externalproject_get_property(gtest source_dir) - set(GTEST_INCLUDE_DIR "${source_dir}/include") - mark_as_advanced(GTEST_INCLUDE_DIR) - externalproject_get_property(gmock source_dir) - set(GMOCK_INCLUDE_DIR "${source_dir}/include") - mark_as_advanced(GMOCK_INCLUDE_DIR) - else() #1.8.0 - set(GMOCK_BIN_DIR "${GMOCK_ROOT}/src/gmock-build") - set(GTEST_LIBRARY "${GMOCK_BIN_DIR}/googlemock/gtest/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GTEST_MAIN_LIBRARY "${GMOCK_BIN_DIR}/googlemock/gtest/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GMOCK_LIBRARY "${GMOCK_BIN_DIR}/googlemock/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GMOCK_MAIN_LIBRARY "${GMOCK_BIN_DIR}/googlemock/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - mark_as_advanced(GTEST_LIBRARY) - mark_as_advanced(GTEST_MAIN_LIBRARY) - mark_as_advanced(GMOCK_LIBRARY) - mark_as_advanced(GMOCK_MAIN_LIBRARY) - - externalproject_add( - gmock - GIT_REPOSITORY "https://github.com/google/googletest.git" - GIT_TAG "release-${GMOCK_VER}" - PREFIX ${GMOCK_ROOT} - INSTALL_COMMAND "" - LOG_DOWNLOAD ON - LOG_CONFIGURE ON - LOG_BUILD ON - CMAKE_ARGS - ${GTEST_CMAKE_ARGS} - BINARY_DIR "${GMOCK_BIN_DIR}" - BUILD_BYPRODUCTS - "${GTEST_LIBRARY}" - "${GTEST_MAIN_LIBRARY}" - "${GMOCK_LIBRARY}" - "${GMOCK_MAIN_LIBRARY}" - ) - - add_dependencies(GTest::GTest gmock) - add_dependencies(GTest::Main gmock) - add_dependencies(GMock::GMock gmock) - add_dependencies(GMock::Main gmock) - - externalproject_get_property(gmock source_dir) - set(GTEST_INCLUDE_DIR "${source_dir}/googletest/include") - set(GMOCK_INCLUDE_DIR "${source_dir}/googlemock/include") - mark_as_advanced(GMOCK_INCLUDE_DIR) - mark_as_advanced(GTEST_INCLUDE_DIR) - endif() - - # Prevent CMake from complaining about these directories missing when the libgtest/libgmock targets get used as dependencies - file(MAKE_DIRECTORY ${GTEST_INCLUDE_DIR} ${GMOCK_INCLUDE_DIR}) - else() - message(STATUS "Building Gmock / Gtest from dir ${GMOCK_SRC_DIR}") - - set(GMOCK_BIN_DIR "${GMOCK_ROOT}/src/gmock-build") - set(GTEST_LIBRARY "${GMOCK_BIN_DIR}/gtest/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GTEST_MAIN_LIBRARY "${GMOCK_BIN_DIR}/gtest/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gtest_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GMOCK_LIBRARY "${GMOCK_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock${CMAKE_STATIC_LIBRARY_SUFFIX}") - set(GMOCK_MAIN_LIBRARY "${GMOCK_BIN_DIR}/${CMAKE_CFG_INTDIR}/${GTEST_LIBRARY_PREFIX}gmock_main${CMAKE_STATIC_LIBRARY_SUFFIX}") - mark_as_advanced(GTEST_LIBRARY) - mark_as_advanced(GTEST_MAIN_LIBRARY) - mark_as_advanced(GMOCK_LIBRARY) - mark_as_advanced(GMOCK_MAIN_LIBRARY) - - if(EXISTS "${GMOCK_SRC_DIR}/gtest/include/gtest/gtest.h") - set(GTEST_INCLUDE_DIR "${GMOCK_SRC_DIR}/gtest/include") - mark_as_advanced(GTEST_INCLUDE_DIR) - endif() - if(EXISTS "${GMOCK_SRC_DIR}/include/gmock/gmock.h") - set(GMOCK_INCLUDE_DIR "${GMOCK_SRC_DIR}/include") - mark_as_advanced(GMOCK_INCLUDE_DIR) - elseif(EXISTS "${GMOCK_SRC_DIR}/../../include/gmock/gmock.h") - set(GMOCK_INCLUDE_DIR "${GMOCK_SRC_DIR}/../../include") - if(IS_ABSOLUTE "${GMOCK_INCLUDE_DIR}") - get_filename_component(GMOCK_INCLUDE_DIR "${GMOCK_INCLUDE_DIR}" ABSOLUTE) - endif() - mark_as_advanced(GMOCK_INCLUDE_DIR) - endif() - - externalproject_add( - gmock - SOURCE_DIR ${GMOCK_SRC_DIR} - PREFIX ${GMOCK_ROOT} - INSTALL_COMMAND "" - LOG_DOWNLOAD ON - LOG_CONFIGURE ON - LOG_BUILD ON - CMAKE_ARGS - ${GTEST_CMAKE_ARGS} - BINARY_DIR "${GMOCK_BIN_DIR}" - BUILD_BYPRODUCTS - "${GTEST_LIBRARY}" - "${GTEST_MAIN_LIBRARY}" - "${GMOCK_LIBRARY}" - "${GMOCK_MAIN_LIBRARY}" - ) - - add_dependencies(GTest::GTest gmock) - add_dependencies(GTest::Main gmock) - add_dependencies(GMock::GMock gmock) - add_dependencies(GMock::Main gmock) - endif() -endif() - include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(GTest DEFAULT_MSG GTEST_LIBRARY GTEST_INCLUDE_DIR GTEST_MAIN_LIBRARY) find_package_handle_standard_args(GMock DEFAULT_MSG GMOCK_LIBRARY GMOCK_INCLUDE_DIR GMOCK_MAIN_LIBRARY) include(CMakeFindDependencyMacro) find_dependency(Threads) -set_target_properties(GTest::GTest PROPERTIES +set_target_properties(GMock::GMock PROPERTIES INTERFACE_LINK_LIBRARIES "Threads::Threads" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GTEST_LIBRARY}" - ) + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX") -if(GTEST_INCLUDE_DIR) - set_target_properties(GTest::GTest PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" - INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" - ) +if(EXISTS "${GMOCK_LIBRARY}") + set_target_properties(GMock::GMock PROPERTIES + IMPORTED_LOCATION "${GMOCK_LIBRARY}") +endif() +if(EXISTS "${GMOCK_LIBRARY_DEBUG}") + set_target_properties(GMock::GMock PROPERTIES + IMPORTED_LOCATION_DEBUG "${GMOCK_LIBRARY_DEBUG}") endif() - -set_target_properties(GTest::Main PROPERTIES - INTERFACE_LINK_LIBRARIES "GTest::GTest" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GTEST_MAIN_LIBRARY}") - -set_target_properties(GMock::GMock PROPERTIES - INTERFACE_LINK_LIBRARIES "Threads::Threads" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GMOCK_LIBRARY}") if(GMOCK_INCLUDE_DIR) set_target_properties(GMock::GMock PROPERTIES @@ -477,30 +185,20 @@ if(GMOCK_INCLUDE_DIR) # so just specify it on the link interface. set_property(TARGET GMock::GMock APPEND PROPERTY INTERFACE_LINK_LIBRARIES GTest::GTest) - elseif(GTEST_INCLUDE_DIR) - # GMock 1.7 and beyond doesn't have it as a link-time dependency anymore, - # so merge it's compile-time interface (include dirs) with ours. - set_property(TARGET GMock::GMock APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}") - set_property(TARGET GMock::GMock APPEND PROPERTY - INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}") endif() endif() set_target_properties(GMock::Main PROPERTIES INTERFACE_LINK_LIBRARIES "GMock::GMock" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX") -if(GTEST_FOUND) - set(GTEST_INCLUDE_DIRS ${GTEST_INCLUDE_DIR}) - set(GTEST_LIBRARIES GTest::GTest) - set(GTEST_MAIN_LIBRARIES GTest::Main) - set(GTEST_BOTH_LIBRARIES ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES}) - if(VERBOSE) - message(STATUS "GTest includes: ${GTEST_INCLUDE_DIRS}") - message(STATUS "GTest libs: ${GTEST_BOTH_LIBRARIES}") - endif() +if(EXISTS "${GMOCK_MAIN_LIBRARY}") + set_target_properties(GMock::Main PROPERTIES + IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") +endif() +if(EXISTS "${GMOCK_MAIN_LIBRARY_DEBUG}") + set_target_properties(GMock::Main PROPERTIES + IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY_DEBUG}") endif() if(GMOCK_FOUND) diff --git a/cmake/FindLZ4.cmake b/cmake/FindLZ4.cmake index ec854c6b18..5b148cb64e 100644 --- a/cmake/FindLZ4.cmake +++ b/cmake/FindLZ4.cmake @@ -95,6 +95,8 @@ include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LZ4 DEFAULT_MSG LZ4_LIBRARY LZ4_INCLUDE_DIR LZ4_VERSION) +set(LZ4_INCLUDE_DIR ${LZ4_INCLUDE_DIR} CACHE PATH "LZ4 include dir hint") +set(LZ4_LIBRARY ${LZ4_LIBRARY} CACHE FILEPATH "LZ4 library path hint") mark_as_advanced(LZ4_INCLUDE_DIR LZ4_LIBRARY) set(LZ4_LIBRARIES ${LZ4_LIBRARY}) diff --git a/cmake/FindLuaJit.cmake b/cmake/FindLuaJit.cmake new file mode 100644 index 0000000000..0f38da9b4b --- /dev/null +++ b/cmake/FindLuaJit.cmake @@ -0,0 +1,14 @@ +# Once found, defines: +# LuaJit_FOUND +# LuaJit_INCLUDE_DIR +# LuaJit_LIBRARIES + +include(LibFindMacros) + +libfind_pkg_detect(LuaJit luajit + FIND_PATH luajit.h PATH_SUFFIXES luajit luajit-2.1 + FIND_LIBRARY luajit-5.1 luajit + ) + +libfind_process(LuaJit) + diff --git a/cmake/FindOSGPlugins.cmake b/cmake/FindOSGPlugins.cmake index c210466c08..f7ebb0fa00 100644 --- a/cmake/FindOSGPlugins.cmake +++ b/cmake/FindOSGPlugins.cmake @@ -15,6 +15,17 @@ include(LibFindMacros) include(Findosg_functions) +if (NOT OSGPlugins_LIB_DIR) + unset(OSGPlugins_LIB_DIR) + foreach(OSGDB_LIB ${OSGDB_LIBRARY}) + # Skip library type names + if(EXISTS ${OSGDB_LIB} AND NOT IS_DIRECTORY ${OSGDB_LIB}) + get_filename_component(OSG_LIB_DIR ${OSGDB_LIB} DIRECTORY) + list(APPEND OSGPlugins_LIB_DIR "${OSG_LIB_DIR}/osgPlugins-${OPENSCENEGRAPH_VERSION}") + endif() + endforeach(OSGDB_LIB) +endif() + if (NOT OSGPlugins_LIB_DIR) set(_mode WARNING) if (OSGPlugins_FIND_REQUIRED) @@ -27,9 +38,12 @@ foreach(_library ${OSGPlugins_FIND_COMPONENTS}) string(TOUPPER ${_library} _library_uc) set(_component OSGPlugins_${_library}) - set(${_library_uc}_DIR ${OSGPlugins_LIB_DIR}) # to help function osg_find_library + # On some systems, notably Debian and Ubuntu, the OSG plugins do not have + # the usual "lib" prefix. We temporarily add the empty string to the list + # of prefixes CMake searches for (via osg_find_library) to support these systems. set(_saved_lib_prefix ${CMAKE_FIND_LIBRARY_PREFIXES}) # save CMAKE_FIND_LIBRARY_PREFIXES - set(CMAKE_FIND_LIBRARY_PREFIXES "") # search libraries with no prefix + list(APPEND CMAKE_FIND_LIBRARY_PREFIXES "") # search libraries with no prefix + set(${_library_uc}_DIR ${OSGPlugins_LIB_DIR}) # to help function osg_find_library osg_find_library(${_library_uc} ${_library}) # find it into ${_library_uc}_LIBRARIES set(CMAKE_FIND_LIBRARY_PREFIXES ${_saved_lib_prefix}) # restore prefix diff --git a/cmake/FindRecastNavigation.cmake b/cmake/FindRecastNavigation.cmake new file mode 100644 index 0000000000..8728a5e949 --- /dev/null +++ b/cmake/FindRecastNavigation.cmake @@ -0,0 +1,211 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. +# Copyright 2021 Bret Curtis for OpenMW +#[=======================================================================[.rst: +FindRecastNavigation +------- + +Find the RecastNavigation include directory and library. + +Use this module by invoking find_package with the form:: + +.. code-block:: cmake + + find_package(RecastNavigation + [version] # Minimum version e.g. 1.8.0 + [REQUIRED] # Fail with error if RECAST is not found + ) + +Imported targets +^^^^^^^^^^^^^^^^ + +This module defines the following :prop_tgt:`IMPORTED` targets: + +.. variable:: RecastNavigation::Recast + + Imported target for using the RECAST library, if found. + +Result variables +^^^^^^^^^^^^^^^^ + +.. variable:: RECAST_FOUND + + Set to true if RECAST library found, otherwise false or undefined. + +.. variable:: RECAST_INCLUDE_DIRS + + Paths to include directories listed in one variable for use by RECAST client. + +.. variable:: RECAST_LIBRARIES + + Paths to libraries to linked against to use RECAST. + +.. variable:: RECAST_VERSION + + The version string of RECAST found. + +Cache variables +^^^^^^^^^^^^^^^ + +For users who wish to edit and control the module behavior, this module +reads hints about search locations from the following variables:: + +.. variable:: RECAST_INCLUDE_DIR + + Path to RECAST include directory with ``Recast.h`` header. + +.. variable:: RECAST_LIBRARY + + Path to RECAST library to be linked. + +NOTE: The variables above should not usually be used in CMakeLists.txt files! + +#]=======================================================================] + +### Find libraries ############################################################## + +if(NOT RECAST_LIBRARY) + find_library(RECAST_LIBRARY_RELEASE NAMES Recast HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + find_library(RECAST_LIBRARY_DEBUG NAMES Recast-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + include(SelectLibraryConfigurations) + select_library_configurations(RECAST) + mark_as_advanced(RECAST_LIBRARY_RELEASE RECAST_LIBRARY_DEBUG) +else() + file(TO_CMAKE_PATH "${RECAST_LIBRARY}" RECAST_LIBRARY) +endif() + +if(NOT DETOUR_LIBRARY) + find_library(DETOUR_LIBRARY_RELEASE NAMES Detour HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + find_library(DETOUR_LIBRARY_DEBUG NAMES Detour-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + include(SelectLibraryConfigurations) + select_library_configurations(DETOUR) + mark_as_advanced(DETOUR_LIBRARY_RELEASE DETOUR_LIBRARY_DEBUG) +else() + file(TO_CMAKE_PATH "${DETOUR_LIBRARY}" DETOUR_LIBRARY) +endif() + +if(NOT DEBUGUTILS_LIBRARY) + find_library(DEBUGUTILS_LIBRARY_RELEASE NAMES DebugUtils HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + find_library(DEBUGUTILS_LIBRARY_DEBUG NAMES DebugUtils-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib) + include(SelectLibraryConfigurations) + select_library_configurations(DEBUGUTILS) + mark_as_advanced(DEBUGUTILS_LIBRARY_RELEASE DEBUGUTILS_LIBRARY_DEBUG) +else() + file(TO_CMAKE_PATH "${DEBUGUTILS_LIBRARY}" DEBUGUTILS_LIBRARY) +endif() + +### Find include directory #################################################### +find_path(RECAST_INCLUDE_DIR NAMES Recast.h HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES include RECAST include/recastnavigation) +mark_as_advanced(RECAST_INCLUDE_DIR) + +if(RECAST_INCLUDE_DIR AND EXISTS "${RECAST_INCLUDE_DIR}/Recast.h") + file(STRINGS "${RECAST_INCLUDE_DIR}/Recast.h" _Recast_h_contents + REGEX "#define RECAST_VERSION_[A-Z]+[ ]+[0-9]+") + string(REGEX REPLACE "#define RECAST_VERSION_MAJOR[ ]+([0-9]+).+" "\\1" + RECAST_VERSION_MAJOR "${_Recast_h_contents}") + string(REGEX REPLACE ".+#define RECAST_VERSION_MINOR[ ]+([0-9]+).+" "\\1" + RECAST_VERSION_MINOR "${_Recast_h_contents}") + string(REGEX REPLACE ".+#define RECAST_VERSION_RELEASE[ ]+([0-9]+).*" "\\1" + RECAST_VERSION_RELEASE "${_Recast_h_contents}") + set(RECAST_VERSION "${RECAST_VERSION_MAJOR}.${RECAST_VERSION_MINOR}.${RECAST_VERSION_RELEASE}") + unset(_Recast_h_contents) +endif() + +#TODO: they don't include a version yet +set(RECAST_VERSION "1.5.1") + +### Set result variables ###################################################### +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(RecastNavigation DEFAULT_MSG + RECAST_LIBRARY RECAST_INCLUDE_DIR RECAST_VERSION) + +set(RECAST_LIBRARIES ${RECAST_LIBRARY}) +set(RECAST_INCLUDE_DIRS ${RECAST_INCLUDE_DIR}) + +set(DETOUR_LIBRARIES ${DETOUR_LIBRARY}) +set(DETOUR_INCLUDE_DIRS ${RECAST_INCLUDE_DIR}) + +set(DEBUGUTILS_LIBRARIES ${DEBUGUTILS_LIBRARY}) +set(DEBUGUTILS_INCLUDE_DIRS ${RECAST_INCLUDE_DIR}) + +### Import targets ############################################################ +if(RecastNavigation_FOUND) + if(NOT TARGET RecastNavigation::Recast) + add_library(RecastNavigation::Recast UNKNOWN IMPORTED) + set_target_properties(RecastNavigation::Recast PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + INTERFACE_INCLUDE_DIRECTORIES "${RECAST_INCLUDE_DIR}") + + if(RECAST_LIBRARY_RELEASE) + set_property(TARGET RecastNavigation::Recast APPEND PROPERTY + IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(RecastNavigation::Recast PROPERTIES + IMPORTED_LOCATION_RELEASE "${RECAST_LIBRARY_RELEASE}") + endif() + + if(RECAST_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::Recast APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(RecastNavigation::Recast PROPERTIES + IMPORTED_LOCATION_DEBUG "${RECAST_LIBRARY_DEBUG}") + endif() + + if(NOT RECAST_LIBRARY_RELEASE AND NOT RECAST_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::Recast APPEND PROPERTY + IMPORTED_LOCATION "${RECAST_LIBRARY}") + endif() + endif() + + if(NOT TARGET RecastNavigation::Detour) + add_library(RecastNavigation::Detour UNKNOWN IMPORTED) + set_target_properties(RecastNavigation::Detour PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + INTERFACE_INCLUDE_DIRECTORIES "${DETOUR_INCLUDE_DIR}") + + if(DETOUR_LIBRARY_RELEASE) + set_property(TARGET RecastNavigation::Detour APPEND PROPERTY + IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(RecastNavigation::Detour PROPERTIES + IMPORTED_LOCATION_RELEASE "${DETOUR_LIBRARY_RELEASE}") + endif() + + if(DETOUR_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::Detour APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(RecastNavigation::Detour PROPERTIES + IMPORTED_LOCATION_DEBUG "${DETOUR_LIBRARY_DEBUG}") + endif() + + if(NOT DETOUR_LIBRARY_RELEASE AND NOT DETOUR_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::Detour APPEND PROPERTY + IMPORTED_LOCATION "${DETOUR_LIBRARY}") + endif() + endif() + + if(NOT TARGET RecastNavigation::DebugUtils) + add_library(RecastNavigation::DebugUtils UNKNOWN IMPORTED) + set_target_properties(RecastNavigation::DebugUtils PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + INTERFACE_INCLUDE_DIRECTORIES "${DEBUGUTILS_INCLUDE_DIR}") + + if(DEBUGUTILS_LIBRARY_RELEASE) + set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY + IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(RecastNavigation::DebugUtils PROPERTIES + IMPORTED_LOCATION_RELEASE "${DEBUGUTILS_LIBRARY_RELEASE}") + endif() + + if(DEBUGUTILS_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(RecastNavigation::DebugUtils PROPERTIES + IMPORTED_LOCATION_DEBUG "${DEBUGUTILS_LIBRARY_DEBUG}") + endif() + + if(NOT DEBUGUTILS_LIBRARY_RELEASE AND NOT DEBUGUTILS_LIBRARY_DEBUG) + set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY + IMPORTED_LOCATION "${DEBUGUTILS_LIBRARY}") + endif() + endif() + +endif() diff --git a/cmake/LibFindMacros.cmake b/cmake/LibFindMacros.cmake index 2be27c5fcb..3044601f6b 100644 --- a/cmake/LibFindMacros.cmake +++ b/cmake/LibFindMacros.cmake @@ -19,11 +19,11 @@ macro (libfind_package PREFIX PKG) endmacro() # A simple wrapper to make pkg-config searches a bit easier. -# Works the same as CMake's internal pkg_check_modules but is always quiet. -macro (libfind_pkg_check_modules) +# Works the same as CMake's internal pkg_search_module but is always quiet. +macro (libfind_pkg_search_module) find_package(PkgConfig QUIET) if (PKG_CONFIG_FOUND) - pkg_check_modules(${ARGN} QUIET) + pkg_search_module(${ARGN} QUIET) endif() endmacro() @@ -47,7 +47,7 @@ function (libfind_pkg_detect PREFIX) message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.") endif() # Find library - libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs}) + libfind_pkg_search_module(${PREFIX}_PKGCONF ${pkgargs}) if (pathargs) find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS}) endif() diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index 2408cae2bd..0adc185b38 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -78,12 +78,8 @@ foreach (u ${ARGN}) file (GLOB ALL "${dir}/${u}.[ch]pp") foreach (f ${ALL}) list (APPEND files "${f}") -list (APPEND COMPONENT_FILES "${f}") +list (APPEND COMPONENT_QT_FILES "${f}") endforeach (f) -file (GLOB MOC_H "${dir}/${u}.hpp") -foreach (fi ${MOC_H}) -list (APPEND COMPONENT_MOC_FILES "${fi}") -endforeach (fi) endforeach (u) source_group ("components\\${dir}" FILES ${files}) endmacro (add_component_qt_dir) @@ -97,44 +93,21 @@ add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") endmacro (add_unit) -macro (add_qt_unit project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") -endmacro (add_qt_unit) - macro (add_hdr project dir unit) add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") endmacro (add_hdr) -macro (add_qt_hdr project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -endmacro (add_qt_hdr) - macro (opencs_units dir) foreach (u ${ARGN}) -add_qt_unit (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_units) - -macro (opencs_units_noqt dir) -foreach (u ${ARGN}) add_unit (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_units_noqt) +endmacro (opencs_units) macro (opencs_hdrs dir) foreach (u ${ARGN}) -add_qt_hdr (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_hdrs) - -macro (opencs_hdrs_noqt dir) -foreach (u ${ARGN}) add_hdr (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_hdrs_noqt) +endmacro (opencs_hdrs) include(CMakeParseArguments) @@ -199,6 +172,18 @@ macro (configure_resource_file source_path destination_dir_base dest_path_relati endif (multi_config) endmacro (configure_resource_file) +macro (pack_resource_file source_path destination_dir_base dest_path_relative) + get_generator_is_multi_config(multi_config) + if (multi_config) + foreach(cfgtype ${CMAKE_CONFIGURATION_TYPES}) + execute_process(COMMAND ${CMAKE_COMMAND} "-DINPUT_FILE=${source_path}" "-DOUTPUT_FILE=${destination_dir_base}/${cfgtype}/${dest_path_relative}" -P "${CMAKE_SOURCE_DIR}/cmake/base64.cmake") + endforeach(cfgtype) + else (multi_config) + execute_process(COMMAND ${CMAKE_COMMAND} "-DINPUT_FILE=${source_path}" "-DOUTPUT_FILE=${destination_dir_base}/${dest_path_relative}" -P "${CMAKE_SOURCE_DIR}/cmake/base64.cmake") + endif (multi_config) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${source_path}") +endmacro (pack_resource_file) + macro (copy_all_resource_files source_dir destination_dir_base destination_dir_relative files) foreach (f ${files}) get_filename_component(filename ${f} NAME) diff --git a/cmake/WholeArchive.cmake b/cmake/WholeArchive.cmake new file mode 100644 index 0000000000..0e4a09c796 --- /dev/null +++ b/cmake/WholeArchive.cmake @@ -0,0 +1,10 @@ +function (get_whole_archive_options OUT_VAR) + # We use --whole-archive because OSG plugins use registration. + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(${OUT_VAR} -Wl,--whole-archive ${ARGN} -Wl,--no-whole-archive PARENT_SCOPE) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + set(${OUT_VAR} -Wl,-all_load ${ARGN} -Wl,-noall_load PARENT_SCOPE) + else () + message(FATAL_ERROR "get_whole_archive_options not implemented for CMAKE_CXX_COMPILER_ID ${CMAKE_CXX_COMPILER_ID}") + endif() +endfunction () diff --git a/cmake/base64.cmake b/cmake/base64.cmake new file mode 100644 index 0000000000..7931758bbb --- /dev/null +++ b/cmake/base64.cmake @@ -0,0 +1,74 @@ +# math(EXPR "...") can't parse hex until 3.13 +cmake_minimum_required(VERSION 3.13) + +if (NOT DEFINED INPUT_FILE) + message(STATUS "Usage: cmake -DINPUT_FILE=\"infile.ext\" -DOUTPUT_FILE=\"out.txt\" -P base64.cmake") + message(FATAL_ERROR "INPUT_FILE not specified") +endif() + +if (NOT DEFINED OUTPUT_FILE) + message(STATUS "Usage: cmake -DINPUT_FILE=\"infile.ext\" -DOUTPUT_FILE=\"out.txt\" -P base64.cmake") + message(FATAL_ERROR "OUTPUT_FILE not specified") +endif() + +if (NOT EXISTS ${INPUT_FILE}) + message(FATAL_ERROR "INPUT_FILE ${INPUT_FILE} does not exist") +endif() + +set(lut "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + +file(READ "${INPUT_FILE}" hexContent HEX) + +set(base64Content "") +while(TRUE) + string(LENGTH "${hexContent}" tailLength) + if (tailLength LESS 1) + break() + endif() + + string(SUBSTRING "${hexContent}" 0 6 head) + # base64 works on three-byte chunks. Pad. + string(LENGTH "${head}" headLength) + if (headLength LESS 6) + set(hexContent "") + math(EXPR padSize "6 - ${headLength}") + set(pad "") + foreach(i RANGE 1 ${padSize}) + string(APPEND pad "0") + endforeach() + string(APPEND head "${pad}") + else() + string(SUBSTRING "${hexContent}" 6 -1 hexContent) + set(padSize 0) + endif() + + # get six-bit slices + math(EXPR first "0x${head} >> 18") + math(EXPR second "(0x${head} & 0x3F000) >> 12") + math(EXPR third "(0x${head} & 0xFC0) >> 6") + math(EXPR fourth "0x${head} & 0x3F") + + # first two characters are always needed to represent the first byte + string(SUBSTRING "${lut}" ${first} 1 char) + string(APPEND base64Content "${char}") + string(SUBSTRING "${lut}" ${second} 1 char) + string(APPEND base64Content "${char}") + + # if there's no second byte, pad with = + if (NOT padSize EQUAL 4) + string(SUBSTRING "${lut}" ${third} 1 char) + string(APPEND base64Content "${char}") + else() + string(APPEND base64Content "=") + endif() + + # if there's no third byte, pad with = + if (padSize EQUAL 0) + string(SUBSTRING "${lut}" ${fourth} 1 char) + string(APPEND base64Content "${char}") + else() + string(APPEND base64Content "=") + endif() +endwhile() + +file(WRITE "${OUTPUT_FILE}" "${base64Content}") \ No newline at end of file diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 08f183f5e8..8dcb1ce64f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -28,12 +28,20 @@ endif (GIT_CHECKOUT) # source files +add_component_dir (lua + luastate scriptscontainer utilpackage serialization configuration l10n storage + ) + +add_component_dir (l10n + messagebundles + ) + add_component_dir (settings settings parser ) add_component_dir (bsa - bsa_file compressedbsafile memorystream + bsa_file compressedbsafile ) add_component_dir (vfs @@ -41,21 +49,23 @@ add_component_dir (vfs ) add_component_dir (resource - scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem resourcemanager stats + scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem + resourcemanager stats animation foreachbulletobject ) add_component_dir (shader - shadermanager shadervisitor + shadermanager shadervisitor removedalphafunc ) add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller - lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer - actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin + lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer + actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt + screencapture depth color riggeometryosgaextension extradata ) add_component_dir (nif - controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream + controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream physics ) add_component_dir (nifosg @@ -70,23 +80,128 @@ add_component_dir (to_utf8 to_utf8 ) -add_component_dir (esm - attr defs esmcommon esmreader esmwriter loadacti loadalch loadappa loadarmo loadbody loadbook loadbsgn loadcell +add_component_dir(esm attr common defs esmcommon reader records util luascripts format) + +add_component_dir(fx pass technique lexer widgets stateupdater) + +add_component_dir(std140 ubo) + +add_component_dir (esm3 + esmreader esmwriter loadacti loadalch loadappa loadarmo loadbody loadbook loadbsgn loadcell loadclas loadclot loadcont loadcrea loaddial loaddoor loadench loadfact loadglob loadgmst loadinfo loadingr loadland loadlevlist loadligh loadlock loadprob loadrepa loadltex loadmgef loadmisc loadnpc loadpgrd loadrace loadregn loadscpt loadskil loadsndg loadsoun loadspel loadsscr loadstat - loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter - savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate - npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile - aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings + loadweap aipackage effectlist spelllist variant variantimp loadtes3 cellref filter + savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap + inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats + weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile + aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache ) -add_component_dir (esmterrain +add_component_dir (esm3terrain storage ) +add_component_dir (esm4 + acti + actor + common + dialogue + effect + formid + inventory + lighting + loadachr + loadacre + loadacti + loadalch + loadaloc + loadammo + loadanio + loadappa + loadarma + loadarmo + loadaspc + loadbook + loadbptd + loadcell + loadclas + loadclfm + loadclot + loadcont + loadcrea + loaddial + loaddobj + loaddoor + loadeyes + loadflor + loadflst + loadfurn + loadglob + loadgras + loadgrup + loadhair + loadhdpt + loadidle + loadidlm + loadimod + loadinfo + loadingr + loadkeym + loadland + loadlgtm + loadligh + loadltex + loadlvlc + loadlvli + loadlvln + loadmato + loadmisc + loadmset + loadmstt + loadmusc + loadnavi + loadnavm + loadnote + loadnpc + loadotft + loadpack + loadpgrd + loadpgre + loadpwat + loadqust + loadrace + loadrefr + loadregn + loadroad + loadsbsp + loadscol + loadscpt + loadscrl + loadsgst + loadslgm + loadsndr + loadsoun + loadstat + loadtact + loadterm + loadtes4 + loadtree + loadtxst + loadweap + loadwrld + reader + reference + script +) + add_component_dir (misc - constants utf8stream stringops resourcehelpers rng messageformatparser weakcache + constants utf8stream stringops resourcehelpers rng messageformatparser weakcache thread + compression osguservalues errorMarker color + ) + +add_component_dir (stereo + frustum multiview stereomanager types ) add_component_dir (debug @@ -98,8 +213,8 @@ IF(NOT WIN32 AND NOT APPLE) add_definitions(-DGLOBAL_CONFIG_PATH="${GLOBAL_CONFIG_PATH}") ENDIF() add_component_dir (files - linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager escape - lowlevelfile constrainedfilestream memorystream + linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager + constrainedfilestream memorystream hash configfileparser openfile constrainedfilestreambuf ) add_component_dir (compiler @@ -119,7 +234,8 @@ add_component_dir (translation ) add_component_dir (terrain - storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata cellborder + storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer + quadtreeworld quadtreenode viewdata cellborder view heightcull ) add_component_dir (loadinglistener @@ -139,7 +255,7 @@ add_component_dir (fontloader ) add_component_dir (sdlutil - sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager + gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager sdlmappings ) add_component_dir (version @@ -150,7 +266,20 @@ add_component_dir (fallback fallback validate ) -if(NOT WIN32 AND NOT ANDROID) +add_component_dir (lua_ui + registerscriptsettings scriptsettings + properties widget element util layers content alignment resources + adapter text textedit window image container flex + ) + + +if(WIN32) + add_component_dir (crashcatcher + windows_crashcatcher + windows_crashmonitor + windows_crashshm + ) +elseif(NOT ANDROID) add_component_dir (crashcatcher crashcatcher ) @@ -166,7 +295,6 @@ add_component_dir(detournavigator navmeshmanager navigatorimpl asyncnavmeshupdater - chunkytrimesh recastmesh tilecachedrecastmeshmanager recastmeshobject @@ -174,8 +302,59 @@ add_component_dir(detournavigator settings navigator findrandompointaroundcircle + raycast + navmeshtileview + oscillatingrecastmeshobject + offmeshconnectionsmanager + preparednavmeshdata + navmeshcacheitem + navigatorutils + generatenavmeshtile + navmeshdb + serialization + navmeshdbutils + recast + gettilespositions + ) + +add_component_dir(loadinglistener + reporter + ) + +add_component_dir(sqlite3 + db + statement + transaction +) + +add_component_dir(esmloader + load + esmdata +) + +add_component_dir(navmeshtool + protocol + ) + +add_component_dir(platform + platform + file ) +if (WIN32) + add_component_dir(platform + file.win32 + ) +elseif (UNIX) + add_component_dir(platform + file.posix + ) +else () + add_component_dir(platform + file.stdio + ) +endif() + set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) @@ -196,12 +375,11 @@ if (USE_QT) processinvoker ) - add_component_dir (misc + add_component_qt_dir (misc helpviewer ) QT5_WRAP_UI(ESM_UI_HDR ${ESM_UI}) - QT5_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) endif() if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -212,33 +390,53 @@ endif () include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) -add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) +find_package(SQLite3 REQUIRED) + +add_library(components STATIC ${COMPONENT_FILES}) target_link_libraries(components - ${Boost_SYSTEM_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} - ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_IOSTREAMS_LIBRARY} - ${OSG_LIBRARIES} - ${OPENTHREADS_LIBRARIES} + # 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`. + # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGPARTICLE_LIBRARIES} - ${OSGUTIL_LIBRARIES} - ${OSGDB_LIBRARIES} ${OSGVIEWER_LIBRARIES} - ${OSGTEXT_LIBRARIES} - ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} ${OSGANIMATION_LIBRARIES} - ${BULLET_LIBRARIES} + ${OSGGA_LIBRARIES} + ${OSGTEXT_LIBRARIES} + ${OSGDB_LIBRARIES} + ${OSGUTIL_LIBRARIES} + ${OSG_LIBRARIES} + ${OPENTHREADS_LIBRARIES} + + ${Boost_SYSTEM_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_PROGRAM_OPTIONS_LIBRARY} + ${Boost_IOSTREAMS_LIBRARY} + ${SDL2_LIBRARIES} ${OPENGL_gl_LIBRARY} ${MyGUI_LIBRARIES} + ${LUA_LIBRARIES} LZ4::LZ4 RecastNavigation::DebugUtils RecastNavigation::Detour RecastNavigation::Recast + + Base64 + SQLite::SQLite3 + smhasher + ${ICU_LIBRARIES} + yaml-cpp ) +if(Boost_VERSION_STRING VERSION_GREATER_EQUAL 1.77.0) + target_link_libraries(components ${Boost_ATOMIC_LIBRARY}) +endif() + +target_link_libraries(components ${BULLET_LIBRARIES}) + if (WIN32) target_link_libraries(components ${Boost_LOCALE_LIBRARY} @@ -246,13 +444,24 @@ if (WIN32) endif() if (USE_QT) - target_link_libraries(components Qt5::Widgets Qt5::Core) + add_library(components_qt STATIC ${COMPONENT_QT_FILES} ${ESM_UI_HDR}) + target_link_libraries(components_qt components Qt5::Widgets Qt5::Core) + target_compile_definitions(components_qt PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") endif() if (GIT_CHECKOUT) add_dependencies (components git-version) endif (GIT_CHECKOUT) +if (OSG_STATIC AND CMAKE_SYSTEM_NAME MATCHES "Linux") + find_package(X11 REQUIRED COMPONENTS Xinerama Xrandr) + target_link_libraries(components ${CMAKE_DL_LIBS} X11::X11 X11::Xinerama X11::Xrandr) + find_package(Fontconfig MODULE) + if(Fontconfig_FOUND) + target_link_libraries(components Fontconfig::Fontconfig) + endif() +endif() + if (WIN32) target_link_libraries(components shlwapi) endif() @@ -271,6 +480,67 @@ endif() # Make the variable accessible for other subdirectories set(COMPONENT_FILES ${COMPONENT_FILES} PARENT_SCOPE) -if (BULLET_USE_DOUBLES) - target_compile_definitions(components PUBLIC BT_USE_DOUBLE_PRECISION) +target_compile_definitions(components PUBLIC BT_USE_DOUBLE_PRECISION) + +if(OSG_STATIC) + unset(_osg_plugins_static_files) + add_library(components_osg_plugins INTERFACE) + foreach(_plugin ${USED_OSG_PLUGINS}) + string(TOUPPER ${_plugin} _plugin_uc) + if(OPENMW_USE_SYSTEM_OSG) + list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) + else() + list(APPEND _osg_plugins_static_files $) + target_link_libraries(components_osg_plugins INTERFACE $) + add_dependencies(components_osg_plugins ${${_plugin_uc}_LIBRARY}) + endif() + endforeach() + # We use --whole-archive because OSG plugins use registration. + get_whole_archive_options(_opts ${_osg_plugins_static_files}) + target_link_options(components_osg_plugins INTERFACE ${_opts}) + target_link_libraries(components components_osg_plugins) + + if(OPENMW_USE_SYSTEM_OSG) + # OSG plugin pkgconfig files are missing these dependencies. + # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 + find_package(Freetype REQUIRED) + find_package(JPEG REQUIRED) + find_package(PNG REQUIRED) + target_link_libraries(components Freetype::Freetype JPEG::JPEG PNG::PNG) + endif() +endif(OSG_STATIC) + +if(USE_QT) + set_property(TARGET components_qt PROPERTY AUTOMOC ON) +endif(USE_QT) + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16 AND MSVC) + target_precompile_headers(components PUBLIC + + + + + + + + + + + + + + + + + + + + + ) + + target_precompile_headers(components PRIVATE + + + + ) endif() diff --git a/components/bsa/bsa_file.cpp b/components/bsa/bsa_file.cpp index 3fd74dd838..9fc6448c2b 100644 --- a/components/bsa/bsa_file.cpp +++ b/components/bsa/bsa_file.cpp @@ -23,21 +23,48 @@ #include "bsa_file.hpp" -#include +#include -#include -#include +#include +#include +#include +#include +#include -using namespace std; using namespace Bsa; /// Error handling -void BSAFile::fail(const string &msg) +[[noreturn]] void BSAFile::fail(const std::string &msg) { throw std::runtime_error("BSA Error: " + msg + "\nArchive: " + mFilename); } +//the getHash code is from bsapack from ghostwheel +//the code is also the same as in https://github.com/arviceblot/bsatool_rs/commit/67cb59ec3aaeedc0849222ea387f031c33e48c81 +BSAFile::Hash getHash(const std::string& name) +{ + BSAFile::Hash hash; + unsigned l = (static_cast(name.size()) >> 1); + unsigned sum, off, temp, i, n; + + for (sum = off = i = 0; i < l; i++) { + sum ^= (((unsigned)(name[i])) << (off & 0x1F)); + off += 8; + } + hash.low = sum; + + for (sum = off = 0; i < name.size(); i++) { + temp = (((unsigned)(name[i])) << (off & 0x1F)); + sum ^= temp; + n = temp & 0x1F; + sum = (sum << (32 - n)) | (sum >> n); // binary "rotate right" + off += 8; + } + hash.high = sum; + return hash; +} + /// Read header information from the input source void BSAFile::readHeader() { @@ -73,8 +100,7 @@ void BSAFile::readHeader() */ assert(!mIsLoaded); - namespace bfs = boost::filesystem; - bfs::ifstream input(bfs::path(mFilename), std::ios_base::binary); + std::ifstream input(std::filesystem::path(mFilename), std::ios_base::binary); // Total archive size std::streamoff fsize = 0; @@ -114,14 +140,17 @@ void BSAFile::readHeader() // Read the offset info into a temporary buffer std::vector offsets(3*filenum); - input.read(reinterpret_cast(&offsets[0]), 12*filenum); + input.read(reinterpret_cast(offsets.data()), 12*filenum); // Read the string table mStringBuf.resize(dirsize-12*filenum); - input.read(&mStringBuf[0], mStringBuf.size()); + input.read(mStringBuf.data(), mStringBuf.size()); // Check our position assert(input.tellg() == std::streampos(12+dirsize)); + std::vector hashes(filenum); + static_assert(sizeof(Hash) == 8); + input.read(reinterpret_cast(hashes.data()), 8*filenum); // Calculate the offset of the data buffer. All file offsets are // relative to this. 12 header bytes + directory + hash table @@ -130,55 +159,158 @@ void BSAFile::readHeader() // Set up the the FileStruct table mFiles.resize(filenum); + size_t endOfNameBuffer = 0; for(size_t i=0;i(offsets[i*2+1] + fileDataOffset); + auto namesOffset = offsets[2*filenum+i]; + fs.setNameInfos(namesOffset, &mStringBuf); + fs.hash = hashes[i]; + + if (namesOffset >= mStringBuf.size()) { + fail("Archive contains names offset outside itself"); + } + const void* end = std::memchr(fs.name(), '\0', mStringBuf.size()-namesOffset); + if (!end) { + fail("Archive contains non-zero terminated string"); + } + + endOfNameBuffer = std::max(endOfNameBuffer, namesOffset + std::strlen(fs.name())+1); + assert(endOfNameBuffer <= mStringBuf.size()); if(fs.offset + fs.fileSize > fsize) fail("Archive contains offsets outside itself"); - // Add the file name to the lookup - mLookup[fs.name] = i; } + mStringBuf.resize(endOfNameBuffer); + + std::sort(mFiles.begin(), mFiles.end(), [](const FileStruct& left, const FileStruct& right) { + return left.offset < right.offset; + }); mIsLoaded = true; } -/// Get the index of a given file name, or -1 if not found -int BSAFile::getIndex(const char *str) const +/// Write header information to the output sink +void Bsa::BSAFile::writeHeader() { - auto it = mLookup.find(str); - if(it == mLookup.end()) - return -1; + std::fstream output(mFilename, std::ios::binary | std::ios::in | std::ios::out); - int res = it->second; - assert(res >= 0 && (size_t)res < mFiles.size()); - return res; + uint32_t head[3]; + head[0] = 0x100; + auto fileDataOffset = mFiles.empty() ? 12 : mFiles.front().offset; + head[1] = static_cast(fileDataOffset - 12 - 8*mFiles.size()); + + output.seekp(0, std::ios_base::end); + + head[2] = static_cast(mFiles.size()); + output.seekp(0); + output.write(reinterpret_cast(head), 12); + + std::sort(mFiles.begin(), mFiles.end(), [](const FileStruct& left, const FileStruct& right) { + return std::make_pair(left.hash.low, left.hash.high) < std::make_pair(right.hash.low, right.hash.high); + }); + + size_t filenum = mFiles.size(); + std::vector offsets(3* filenum); + std::vector hashes(filenum); + for(size_t i=0;i(offsets.data()), sizeof(uint32_t)*offsets.size()); + output.write(reinterpret_cast(mStringBuf.data()), mStringBuf.size()); + output.seekp(fileDataOffset - 8*mFiles.size(), std::ios_base::beg); + output.write(reinterpret_cast(hashes.data()), sizeof(Hash)*hashes.size()); } /// Open an archive file. -void BSAFile::open(const string &file) +void BSAFile::open(const std::string &file) { + if (mIsLoaded) + close(); + mFilename = file; - readHeader(); + if(std::filesystem::exists(file)) + readHeader(); + else + { + { std::fstream(mFilename, std::ios::binary | std::ios::out); } + writeHeader(); + mIsLoaded = true; + } } -Files::IStreamPtr BSAFile::getFile(const char *file) +/// Close the archive, write the updated headers to the file +void Bsa::BSAFile::close() { - assert(file); - int i = getIndex(file); - if(i == -1) - fail("File not found: " + string(file)); + if (mHasChanged) + writeHeader(); - const FileStruct &fs = mFiles[i]; + mFiles.clear(); + mStringBuf.clear(); + mIsLoaded = false; +} - return Files::openConstrainedFileStream (mFilename.c_str (), fs.offset, fs.fileSize); +Files::IStreamPtr Bsa::BSAFile::getFile(const FileStruct *file) +{ + return Files::openConstrainedFileStream(mFilename, file->offset, file->fileSize); } -Files::IStreamPtr BSAFile::getFile(const FileStruct *file) +void Bsa::BSAFile::addFile(const std::string& filename, std::istream& file) { - return Files::openConstrainedFileStream (mFilename.c_str (), file->offset, file->fileSize); + if (!mIsLoaded) + fail("Unable to add file " + filename + " the archive is not opened"); + + auto newStartOfDataBuffer = 12 + (12 + 8) * (mFiles.size() + 1) + mStringBuf.size() + filename.size() + 1; + if (mFiles.empty()) + std::filesystem::resize_file(mFilename, newStartOfDataBuffer); + + std::fstream stream(mFilename, std::ios::binary | std::ios::in | std::ios::out); + + FileStruct newFile; + file.seekg(0, std::ios::end); + newFile.fileSize = static_cast(file.tellg()); + newFile.setNameInfos(mStringBuf.size(), &mStringBuf); + newFile.hash = getHash(filename); + + if(mFiles.empty()) + newFile.offset = static_cast(newStartOfDataBuffer); + else + { + std::vector buffer; + while (mFiles.front().offset < newStartOfDataBuffer) { + FileStruct& firstFile = mFiles.front(); + buffer.resize(firstFile.fileSize); + + stream.seekg(firstFile.offset, std::ios::beg); + stream.read(buffer.data(), firstFile.fileSize); + + stream.seekp(0, std::ios::end); + firstFile.offset = static_cast(stream.tellp()); + + stream.write(buffer.data(), firstFile.fileSize); + + //ensure sort order is preserved + std::rotate(mFiles.begin(), mFiles.begin() + 1, mFiles.end()); + } + stream.seekp(0, std::ios::end); + newFile.offset = static_cast(stream.tellp()); + } + + mStringBuf.insert(mStringBuf.end(), filename.begin(), filename.end()); + mStringBuf.push_back('\0'); + mFiles.push_back(newFile); + + mHasChanged = true; + + stream.seekp(0, std::ios::end); + file.seekg(0, std::ios::beg); + stream << file.rdbuf(); } diff --git a/components/bsa/bsa_file.hpp b/components/bsa/bsa_file.hpp index 0378027397..f6af2e3269 100644 --- a/components/bsa/bsa_file.hpp +++ b/components/bsa/bsa_file.hpp @@ -27,12 +27,8 @@ #include #include #include -#include - -#include - -#include +#include namespace Bsa { @@ -43,20 +39,42 @@ namespace Bsa class BSAFile { public: + + #pragma pack(push) + #pragma pack(1) + struct Hash + { + uint32_t low, high; + }; + #pragma pack(pop) + /// Represents one file entry in the archive struct FileStruct { + void setNameInfos(size_t index, + std::vector* stringBuf + ) { + namesOffset = static_cast(index); + namesBuffer = stringBuf; + } + // File size and offset in file. We store the offset from the // beginning of the file, not the offset into the data buffer // (which is what is stored in the archive.) uint32_t fileSize, offset; + Hash hash; // Zero-terminated file name - const char *name; + const char* name() const { return &(*namesBuffer)[namesOffset]; }; + + uint32_t namesOffset = 0; + std::vector* namesBuffer = nullptr; }; typedef std::vector FileList; protected: + bool mHasChanged = false; + /// Table of files in this archive FileList mFiles; @@ -69,32 +87,12 @@ protected: /// Used for error messages std::string mFilename; - /// Case insensitive string comparison - struct iltstr - { - bool operator()(const char *s1, const char *s2) const - { return Misc::StringUtils::ciLess(s1, s2); } - }; - - /** A map used for fast file name lookup. The value is the index into - the files[] vector above. The iltstr ensures that file name - checks are case insensitive. - */ - typedef std::map Lookup; - Lookup mLookup; - /// Error handling - void fail(const std::string &msg); + [[noreturn]] void fail(const std::string &msg); /// Read header information from the input source virtual void readHeader(); - - /// Read header information from the input source - - - /// Get the index of a given file name, or -1 if not found - /// @note Thread safe. - int getIndex(const char *str) const; + virtual void writeHeader(); public: /* ----------------------------------- @@ -106,35 +104,37 @@ public: : mIsLoaded(false) { } - virtual ~BSAFile() = default; + virtual ~BSAFile() + { + close(); + } /// Open an archive file. void open(const std::string &file); + void close(); + /* ----------------------------------- * Archive file routines * ----------------------------------- */ - /// Check if a file exists - virtual bool exists(const char *file) const - { return getIndex(file) != -1; } - - /** Open a file contained in the archive. Throws an exception if the - file doesn't exist. - * @note Thread safe. - */ - virtual Files::IStreamPtr getFile(const char *file); - /** Open a file contained in the archive. * @note Thread safe. */ - virtual Files::IStreamPtr getFile(const FileStruct* file); + Files::IStreamPtr getFile(const FileStruct *file); + + void addFile(const std::string& filename, std::istream& file); /// Get a list of all files /// @note Thread safe. const FileList &getList() const { return mFiles; } + + const std::string& getFilename() const + { + return mFilename; + } }; } diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index 77e477ac58..67e172e3db 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -26,18 +26,28 @@ #include #include +#include +#include #include -#include -#include -#include #include #include -#include + +#if defined(_MSC_VER) + #pragma warning (push) + #pragma warning (disable : 4706) + #include + #pragma warning (pop) +#else + #include +#endif + #include #include +#include +#include namespace Bsa { @@ -79,19 +89,19 @@ void CompressedBSAFile::getBZString(std::string& str, std::istream& filestream) char size = 0; filestream.read(&size, 1); - boost::scoped_array buf(new char[size]); - filestream.read(buf.get(), size); + auto buf = std::vector(size); + filestream.read(buf.data(), size); if (buf[size - 1] != 0) { - str.assign(buf.get(), size); + str.assign(buf.data(), size); if (str.size() != ((size_t)size)) { fail("getBZString string size mismatch"); } } else { - str.assign(buf.get(), size - 1); // don't copy null terminator + str.assign(buf.data(), size - 1); // don't copy null terminator if (str.size() != ((size_t)size - 1)) { fail("getBZString string size mismatch (null terminator)"); } @@ -109,8 +119,7 @@ void CompressedBSAFile::readHeader() { assert(!mIsLoaded); - namespace bfs = boost::filesystem; - bfs::ifstream input(bfs::path(mFilename), std::ios_base::binary); + std::ifstream input(std::filesystem::path(mFilename), std::ios_base::binary); // Total archive size std::streamoff fsize = 0; @@ -226,7 +235,6 @@ void CompressedBSAFile::readHeader() FileStruct fileStruct{}; fileStruct.fileSize = file.getSizeWithoutCompressionFlag(); fileStruct.offset = file.offset; - fileStruct.name = nullptr; mFiles.push_back(fileStruct); fullPaths.push_back(folder); @@ -249,7 +257,7 @@ void CompressedBSAFile::readHeader() } //The vector guarantees that its elements occupy contiguous memory - mFiles[fileIndex].name = reinterpret_cast(mStringBuf.data() + mStringBuffOffset); + mFiles[fileIndex].setNameInfos(mStringBuffOffset, &mStringBuf); fullPaths.at(fileIndex) += "\\" + std::string(mStringBuf.data() + mStringBuffOffset); @@ -276,9 +284,8 @@ void CompressedBSAFile::readHeader() fullPaths.at(fileIndex).c_str() + stringLength + 1u, mStringBuf.data() + mStringBuffOffset); - mFiles[fileIndex].name = reinterpret_cast(mStringBuf.data() + mStringBuffOffset); + mFiles[fileIndex].setNameInfos(mStringBuffOffset, &mStringBuf); - mLookup[reinterpret_cast(mStringBuf.data() + mStringBuffOffset)] = fileIndex; mStringBuffOffset += stringLength + 1u; } @@ -298,12 +305,11 @@ CompressedBSAFile::FileRecord CompressedBSAFile::getFileRecord(const std::string std::string path = str; std::replace(path.begin(), path.end(), '\\', '/'); - boost::filesystem::path p(path); + std::filesystem::path p(path); std::string stem = p.stem().string(); std::string ext = p.extension().string(); - p.remove_filename(); - - std::string folder = p.string(); + + std::string folder = p.parent_path().string(); std::uint64_t folderHash = generateHash(folder, std::string()); auto it = mFolders.find(folderHash); @@ -320,13 +326,19 @@ CompressedBSAFile::FileRecord CompressedBSAFile::getFileRecord(const std::string Files::IStreamPtr CompressedBSAFile::getFile(const FileStruct* file) { - FileRecord fileRec = getFileRecord(file->name); + FileRecord fileRec = getFileRecord(file->name()); if (!fileRec.isValid()) { - fail("File not found: " + std::string(file->name)); + fail("File not found: " + std::string(file->name())); } return getFile(fileRec); } +void CompressedBSAFile::addFile(const std::string& filename, std::istream& file) +{ + assert(false); //not implemented yet + fail("Add file is not implemented for compressed BSA: " + filename); +} + Files::IStreamPtr CompressedBSAFile::getFile(const char* file) { FileRecord fileRec = getFileRecord(file); @@ -341,7 +353,7 @@ Files::IStreamPtr CompressedBSAFile::getFile(const FileRecord& fileRecord) size_t size = fileRecord.getSizeWithoutCompressionFlag(); size_t uncompressedSize = size; bool compressed = fileRecord.isCompressed(mCompressedByDefault); - Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilename.c_str(), fileRecord.offset, size); + Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilename, fileRecord.offset, size); std::istream* fileStream = streamPtr.get(); if (mEmbeddedFileNames) { @@ -356,7 +368,7 @@ Files::IStreamPtr CompressedBSAFile::getFile(const FileRecord& fileRecord) fileStream->read(reinterpret_cast(&uncompressedSize), sizeof(uint32_t)); size -= sizeof(uint32_t); } - std::shared_ptr memoryStreamPtr = std::make_shared(uncompressedSize); + auto memoryStreamPtr = std::make_unique(uncompressedSize); if (compressed) { @@ -371,13 +383,15 @@ Files::IStreamPtr CompressedBSAFile::getFile(const FileRecord& fileRecord) } else // SSE: lz4 { - boost::scoped_array buffer(new char[size]); - fileStream->read(buffer.get(), size); + auto buffer = std::vector(size); + fileStream->read(buffer.data(), size); LZ4F_decompressionContext_t context = nullptr; LZ4F_createDecompressionContext(&context, LZ4F_VERSION); LZ4F_decompressOptions_t options = {}; - LZ4F_decompress(context, memoryStreamPtr->getRawData(), &uncompressedSize, buffer.get(), &size, &options); - LZ4F_errorCode_t errorCode = LZ4F_freeDecompressionContext(context); + LZ4F_errorCode_t errorCode = LZ4F_decompress(context, memoryStreamPtr->getRawData(), &uncompressedSize, buffer.data(), &size, &options); + if (LZ4F_isError(errorCode)) + fail("LZ4 decompression error (file " + mFilename + "): " + LZ4F_getErrorName(errorCode)); + errorCode = LZ4F_freeDecompressionContext(context); if (LZ4F_isError(errorCode)) fail("LZ4 decompression error (file " + mFilename + "): " + LZ4F_getErrorName(errorCode)); } @@ -387,13 +401,12 @@ Files::IStreamPtr CompressedBSAFile::getFile(const FileRecord& fileRecord) fileStream->read(memoryStreamPtr->getRawData(), size); } - return std::shared_ptr(memoryStreamPtr, (std::istream*)memoryStreamPtr.get()); + return std::make_unique>(std::move(memoryStreamPtr)); } -BsaVersion CompressedBSAFile::detectVersion(std::string filePath) +BsaVersion CompressedBSAFile::detectVersion(const std::string& filePath) { - namespace bfs = boost::filesystem; - bfs::ifstream input(bfs::path(filePath), std::ios_base::binary); + std::ifstream input(std::filesystem::path(filePath), std::ios_base::binary); // Total archive size std::streamoff fsize = 0; @@ -430,10 +443,10 @@ void CompressedBSAFile::convertCompressedSizesToUncompressed() { for (auto & mFile : mFiles) { - const FileRecord& fileRecord = getFileRecord(mFile.name); + const FileRecord& fileRecord = getFileRecord(mFile.name()); if (!fileRecord.isValid()) { - fail("Could not find file " + std::string(mFile.name) + " in BSA"); + fail("Could not find file " + std::string(mFile.name()) + " in BSA"); } if (!fileRecord.isCompressed(mCompressedByDefault)) @@ -442,7 +455,7 @@ void CompressedBSAFile::convertCompressedSizesToUncompressed() continue; } - Files::IStreamPtr dataBegin = Files::openConstrainedFileStream(mFilename.c_str(), fileRecord.offset, fileRecord.getSizeWithoutCompressionFlag()); + Files::IStreamPtr dataBegin = Files::openConstrainedFileStream(mFilename, fileRecord.offset, fileRecord.getSizeWithoutCompressionFlag()); if (mEmbeddedFileNames) { diff --git a/components/bsa/compressedbsafile.hpp b/components/bsa/compressedbsafile.hpp index deddfae38b..2c0b3c0aac 100644 --- a/components/bsa/compressedbsafile.hpp +++ b/components/bsa/compressedbsafile.hpp @@ -26,6 +26,8 @@ #ifndef BSA_COMPRESSED_BSA_FILE_H #define BSA_COMPRESSED_BSA_FILE_H +#include + #include namespace Bsa @@ -37,7 +39,7 @@ namespace Bsa BSAVER_COMPRESSED = 0x415342 //B, S, A }; - class CompressedBSAFile : public BSAFile + class CompressedBSAFile : private BSAFile { private: //special marker for invalid records, @@ -83,18 +85,22 @@ namespace Bsa static std::uint64_t generateHash(std::string stem, std::string extension) ; Files::IStreamPtr getFile(const FileRecord& fileRecord); public: + using BSAFile::open; + using BSAFile::getList; + using BSAFile::getFilename; + CompressedBSAFile(); virtual ~CompressedBSAFile(); //checks version of BSA from file header - static BsaVersion detectVersion(std::string filePath); + static BsaVersion detectVersion(const std::string& filePath); /// Read header information from the input source void readHeader() override; - Files::IStreamPtr getFile(const char* filePath) override; - Files::IStreamPtr getFile(const FileStruct* fileStruct) override; - + Files::IStreamPtr getFile(const char* filePath); + Files::IStreamPtr getFile(const FileStruct* fileStruct); + void addFile(const std::string& filename, std::istream& file); }; } diff --git a/components/bsa/memorystream.cpp b/components/bsa/memorystream.cpp deleted file mode 100644 index 34e98e6b68..0000000000 --- a/components/bsa/memorystream.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (memorystream.cpp) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - Compressed BSA upgrade added by Azdul 2019 - - */ -#include "memorystream.hpp" - - -namespace Bsa -{ -MemoryInputStreamBuf::MemoryInputStreamBuf(size_t bufferSize) : mBufferPtr(bufferSize) -{ - this->setg(mBufferPtr.data(), mBufferPtr.data(), mBufferPtr.data() + bufferSize); -} - -char* MemoryInputStreamBuf::getRawData() { - return mBufferPtr.data(); -} - -MemoryInputStream::MemoryInputStream(size_t bufferSize) : - MemoryInputStreamBuf(bufferSize), - std::istream(static_cast(this)) { - -} - -char* MemoryInputStream::getRawData() { - return MemoryInputStreamBuf::getRawData(); -} -} diff --git a/components/bsa/memorystream.hpp b/components/bsa/memorystream.hpp index d168e93d65..f36f1e23f4 100644 --- a/components/bsa/memorystream.hpp +++ b/components/bsa/memorystream.hpp @@ -27,23 +27,11 @@ #define BSA_MEMORY_STREAM_H #include -#include +#include +#include namespace Bsa { -/** -Class used internally by MemoryInputStream. -*/ -class MemoryInputStreamBuf : public std::streambuf { - -public: - explicit MemoryInputStreamBuf(size_t bufferSize); - virtual char* getRawData(); -private: - //correct call to delete [] on C++ 11 - std::vector mBufferPtr; -}; - /** Class replaces Ogre memory streams without introducing any new external dependencies beyond standard library. @@ -52,10 +40,18 @@ private: Memory buffer is freed once the class instance is destroyed. */ -class MemoryInputStream : virtual MemoryInputStreamBuf, std::istream { +class MemoryInputStream : private std::vector, public Files::MemBuf, public std::istream { public: - explicit MemoryInputStream(size_t bufferSize); - char* getRawData() override; + explicit MemoryInputStream(size_t bufferSize) + : std::vector(bufferSize) + , Files::MemBuf(this->data(), this->size()) + , std::istream(static_cast(this)) + {} + + char* getRawData() + { + return this->data(); + } }; } diff --git a/components/bullethelpers/aabb.hpp b/components/bullethelpers/aabb.hpp new file mode 100644 index 0000000000..bfa1dd6e4e --- /dev/null +++ b/components/bullethelpers/aabb.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_AABB_H +#define OPENMW_COMPONENTS_BULLETHELPERS_AABB_H + +#include +#include + +inline bool operator==(const btAABB& lhs, const btAABB& rhs) +{ + return lhs.m_min == rhs.m_min && lhs.m_max == rhs.m_max; +} + +inline bool operator!=(const btAABB& lhs, const btAABB& rhs) +{ + return !(lhs == rhs); +} + +namespace BulletHelpers +{ + inline btAABB getAabb(const btCollisionShape& shape, const btTransform& transform) + { + btAABB result; + shape.getAabb(transform, result.m_min, result.m_max); + return result; + } +} + +#endif diff --git a/components/bullethelpers/collisionobject.hpp b/components/bullethelpers/collisionobject.hpp new file mode 100644 index 0000000000..0951c2af38 --- /dev/null +++ b/components/bullethelpers/collisionobject.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_COLLISIONOBJECT_H +#define OPENMW_COMPONENTS_BULLETHELPERS_COLLISIONOBJECT_H + +#include + +#include +#include +#include + +#include + +namespace BulletHelpers +{ + inline std::unique_ptr makeCollisionObject(btCollisionShape* shape, + const btVector3& position, const btQuaternion& rotation) + { + std::unique_ptr result = std::make_unique(); + result->setCollisionShape(shape); + result->setWorldTransform(btTransform(rotation, position)); + return result; + } +} + +#endif diff --git a/components/bullethelpers/debug.hpp b/components/bullethelpers/debug.hpp new file mode 100644 index 0000000000..564616043d --- /dev/null +++ b/components/bullethelpers/debug.hpp @@ -0,0 +1,70 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_DEBUG_H +#define OPENMW_COMPONENTS_BULLETHELPERS_DEBUG_H + +#include +#include +#include + +#include + +#include + +inline std::ostream& operator <<(std::ostream& stream, const btVector3& value) +{ + return stream << "btVector3(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() + << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.y() + << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.z() + << ')'; +} + +inline std::ostream& operator <<(std::ostream& stream, BroadphaseNativeTypes value) +{ + switch (value) + { +#ifndef SHAPE_NAME +#define SHAPE_NAME(name) case name: return stream << #name; + SHAPE_NAME(BOX_SHAPE_PROXYTYPE) + SHAPE_NAME(TRIANGLE_SHAPE_PROXYTYPE) + SHAPE_NAME(TETRAHEDRAL_SHAPE_PROXYTYPE) + SHAPE_NAME(CONVEX_TRIANGLEMESH_SHAPE_PROXYTYPE) + SHAPE_NAME(CONVEX_HULL_SHAPE_PROXYTYPE) + SHAPE_NAME(CONVEX_POINT_CLOUD_SHAPE_PROXYTYPE) + SHAPE_NAME(CUSTOM_POLYHEDRAL_SHAPE_TYPE) + SHAPE_NAME(IMPLICIT_CONVEX_SHAPES_START_HERE) + SHAPE_NAME(SPHERE_SHAPE_PROXYTYPE) + SHAPE_NAME(MULTI_SPHERE_SHAPE_PROXYTYPE) + SHAPE_NAME(CAPSULE_SHAPE_PROXYTYPE) + SHAPE_NAME(CONE_SHAPE_PROXYTYPE) + SHAPE_NAME(CONVEX_SHAPE_PROXYTYPE) + SHAPE_NAME(CYLINDER_SHAPE_PROXYTYPE) + SHAPE_NAME(UNIFORM_SCALING_SHAPE_PROXYTYPE) + SHAPE_NAME(MINKOWSKI_SUM_SHAPE_PROXYTYPE) + SHAPE_NAME(MINKOWSKI_DIFFERENCE_SHAPE_PROXYTYPE) + SHAPE_NAME(BOX_2D_SHAPE_PROXYTYPE) + SHAPE_NAME(CONVEX_2D_SHAPE_PROXYTYPE) + SHAPE_NAME(CUSTOM_CONVEX_SHAPE_TYPE) + SHAPE_NAME(CONCAVE_SHAPES_START_HERE) + SHAPE_NAME(TRIANGLE_MESH_SHAPE_PROXYTYPE) + SHAPE_NAME(SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE) + SHAPE_NAME(FAST_CONCAVE_MESH_PROXYTYPE) + SHAPE_NAME(TERRAIN_SHAPE_PROXYTYPE) + SHAPE_NAME(GIMPACT_SHAPE_PROXYTYPE) + SHAPE_NAME(MULTIMATERIAL_TRIANGLE_MESH_PROXYTYPE) + SHAPE_NAME(EMPTY_SHAPE_PROXYTYPE) + SHAPE_NAME(STATIC_PLANE_PROXYTYPE) + SHAPE_NAME(CUSTOM_CONCAVE_SHAPE_TYPE) + SHAPE_NAME(CONCAVE_SHAPES_END_HERE) + SHAPE_NAME(COMPOUND_SHAPE_PROXYTYPE) + SHAPE_NAME(SOFTBODY_SHAPE_PROXYTYPE) + SHAPE_NAME(HFFLUID_SHAPE_PROXYTYPE) + SHAPE_NAME(HFFLUID_BUOYANT_CONVEX_SHAPE_PROXYTYPE) + SHAPE_NAME(INVALID_SHAPE_PROXYTYPE) + SHAPE_NAME(MAX_BROADPHASE_COLLISION_TYPES) +#undef SHAPE_NAME +#endif + default: + return stream << "undefined(" << static_cast(value) << ")"; + } +} + +#endif diff --git a/components/bullethelpers/heightfield.hpp b/components/bullethelpers/heightfield.hpp new file mode 100644 index 0000000000..6b1f0ccc2e --- /dev/null +++ b/components/bullethelpers/heightfield.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_HEIGHTFIELD_H +#define OPENMW_COMPONENTS_BULLETHELPERS_HEIGHTFIELD_H + +#include + +namespace BulletHelpers +{ + inline btVector3 getHeightfieldShift(int x, int y, int size, float minHeight, float maxHeight) + { + return btVector3((x + 0.5f) * size, (y + 0.5f) * size, (maxHeight + minHeight) * 0.5f); + } +} + +#endif diff --git a/components/bullethelpers/operators.hpp b/components/bullethelpers/operators.hpp index ea88deddf0..f1e52938db 100644 --- a/components/bullethelpers/operators.hpp +++ b/components/bullethelpers/operators.hpp @@ -1,71 +1,24 @@ #ifndef OPENMW_COMPONENTS_BULLETHELPERS_OPERATORS_H #define OPENMW_COMPONENTS_BULLETHELPERS_OPERATORS_H -#include -#include -#include +#include -#include -#include -#include +#include #include -inline std::ostream& operator <<(std::ostream& stream, const btVector3& value) +inline bool operator <(const btVector3& lhs, const btVector3& rhs) { - return stream << "btVector3(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() - << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.y() - << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.z() - << ')'; + return std::tie(lhs.x(), lhs.y(), lhs.z()) < std::tie(rhs.x(), rhs.y(), rhs.z()); } -inline std::ostream& operator <<(std::ostream& stream, BroadphaseNativeTypes value) +inline bool operator <(const btMatrix3x3& lhs, const btMatrix3x3& rhs) { - switch (value) - { -#ifndef SHAPE_NAME -#define SHAPE_NAME(name) case name: return stream << #name; - SHAPE_NAME(BOX_SHAPE_PROXYTYPE) - SHAPE_NAME(TRIANGLE_SHAPE_PROXYTYPE) - SHAPE_NAME(TETRAHEDRAL_SHAPE_PROXYTYPE) - SHAPE_NAME(CONVEX_TRIANGLEMESH_SHAPE_PROXYTYPE) - SHAPE_NAME(CONVEX_HULL_SHAPE_PROXYTYPE) - SHAPE_NAME(CONVEX_POINT_CLOUD_SHAPE_PROXYTYPE) - SHAPE_NAME(CUSTOM_POLYHEDRAL_SHAPE_TYPE) - SHAPE_NAME(IMPLICIT_CONVEX_SHAPES_START_HERE) - SHAPE_NAME(SPHERE_SHAPE_PROXYTYPE) - SHAPE_NAME(MULTI_SPHERE_SHAPE_PROXYTYPE) - SHAPE_NAME(CAPSULE_SHAPE_PROXYTYPE) - SHAPE_NAME(CONE_SHAPE_PROXYTYPE) - SHAPE_NAME(CONVEX_SHAPE_PROXYTYPE) - SHAPE_NAME(CYLINDER_SHAPE_PROXYTYPE) - SHAPE_NAME(UNIFORM_SCALING_SHAPE_PROXYTYPE) - SHAPE_NAME(MINKOWSKI_SUM_SHAPE_PROXYTYPE) - SHAPE_NAME(MINKOWSKI_DIFFERENCE_SHAPE_PROXYTYPE) - SHAPE_NAME(BOX_2D_SHAPE_PROXYTYPE) - SHAPE_NAME(CONVEX_2D_SHAPE_PROXYTYPE) - SHAPE_NAME(CUSTOM_CONVEX_SHAPE_TYPE) - SHAPE_NAME(CONCAVE_SHAPES_START_HERE) - SHAPE_NAME(TRIANGLE_MESH_SHAPE_PROXYTYPE) - SHAPE_NAME(SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE) - SHAPE_NAME(FAST_CONCAVE_MESH_PROXYTYPE) - SHAPE_NAME(TERRAIN_SHAPE_PROXYTYPE) - SHAPE_NAME(GIMPACT_SHAPE_PROXYTYPE) - SHAPE_NAME(MULTIMATERIAL_TRIANGLE_MESH_PROXYTYPE) - SHAPE_NAME(EMPTY_SHAPE_PROXYTYPE) - SHAPE_NAME(STATIC_PLANE_PROXYTYPE) - SHAPE_NAME(CUSTOM_CONCAVE_SHAPE_TYPE) - SHAPE_NAME(CONCAVE_SHAPES_END_HERE) - SHAPE_NAME(COMPOUND_SHAPE_PROXYTYPE) - SHAPE_NAME(SOFTBODY_SHAPE_PROXYTYPE) - SHAPE_NAME(HFFLUID_SHAPE_PROXYTYPE) - SHAPE_NAME(HFFLUID_BUOYANT_CONVEX_SHAPE_PROXYTYPE) - SHAPE_NAME(INVALID_SHAPE_PROXYTYPE) - SHAPE_NAME(MAX_BROADPHASE_COLLISION_TYPES) -#undef SHAPE_NAME -#endif - default: - return stream << "undefined(" << static_cast(value) << ")"; - } + return std::tie(lhs[0], lhs[1], lhs[2]) < std::tie(rhs[0], rhs[1], rhs[2]); +} + +inline bool operator <(const btTransform& lhs, const btTransform& rhs) +{ + return std::tie(lhs.getBasis(), lhs.getOrigin()) < std::tie(rhs.getBasis(), rhs.getOrigin()); } #endif diff --git a/components/compiler/context.hpp b/components/compiler/context.hpp index 2d6af0e451..d3caba7c53 100644 --- a/components/compiler/context.hpp +++ b/components/compiler/context.hpp @@ -13,14 +13,14 @@ namespace Compiler public: - Context() : mExtensions (0) {} + Context() : mExtensions (nullptr) {} virtual ~Context() = default; virtual bool canDeclareLocals() const = 0; ///< Is the compiler allowed to declare local variables? - void setExtensions (const Extensions *extensions = 0) + void setExtensions (const Extensions *extensions = nullptr) { mExtensions = extensions; } @@ -42,9 +42,6 @@ namespace Compiler virtual bool isId (const std::string& name) const = 0; ///< Does \a name match an ID, that can be referenced? - - virtual bool isJournalId (const std::string& name) const = 0; - ///< Does \a name match a journal ID? }; } diff --git a/components/compiler/controlparser.cpp b/components/compiler/controlparser.cpp index ec69fffa2c..a5a68edbd5 100644 --- a/components/compiler/controlparser.cpp +++ b/components/compiler/controlparser.cpp @@ -39,14 +39,14 @@ namespace Compiler Codes block; if (iter!=mIfCode.rbegin()) - Generator::jump (iter->second, codes.size()+1); + Generator::jump (iter->second, static_cast(codes.size()+1)); if (!iter->first.empty()) { // if or elseif std::copy (iter->first.begin(), iter->first.end(), std::back_inserter (block)); - Generator::jumpOnZero (block, iter->second.size()+1); + Generator::jumpOnZero (block, static_cast(iter->second.size()+1)); } std::copy (iter->second.begin(), iter->second.end(), @@ -113,7 +113,7 @@ namespace Compiler Codes skip; - Generator::jumpOnZero (skip, mCodeBlock.size()+loop.size()+1); + Generator::jumpOnZero (skip, static_cast (mCodeBlock.size()+loop.size()+1)); std::copy (skip.begin(), skip.end(), std::back_inserter (mCode)); diff --git a/components/compiler/declarationparser.cpp b/components/compiler/declarationparser.cpp index 1c64aaadee..f29fe820c1 100644 --- a/components/compiler/declarationparser.cpp +++ b/components/compiler/declarationparser.cpp @@ -90,6 +90,16 @@ bool Compiler::DeclarationParser::parseSpecial (int code, const TokenLoc& loc, S return Parser::parseSpecial (code, loc, scanner); } +bool Compiler::DeclarationParser::parseInt(int value, const TokenLoc& loc, Scanner& scanner) +{ + if(mState == State_Name) + { + // Allow integers to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } + return Parser::parseInt(value, loc, scanner); +} + void Compiler::DeclarationParser::reset() { mState = State_Begin; diff --git a/components/compiler/declarationparser.hpp b/components/compiler/declarationparser.hpp index c04f1dc268..d08509fe50 100644 --- a/components/compiler/declarationparser.hpp +++ b/components/compiler/declarationparser.hpp @@ -35,6 +35,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void reset() override; }; diff --git a/components/compiler/discardparser.cpp b/components/compiler/discardparser.cpp index 0e7c4718cb..0a714d4eb6 100644 --- a/components/compiler/discardparser.cpp +++ b/components/compiler/discardparser.cpp @@ -12,7 +12,7 @@ namespace Compiler bool DiscardParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState || mState==MinusState) + if (mState==StartState || mState==MinusState) { if (isEmpty()) mTokenLoc = loc; @@ -26,7 +26,7 @@ namespace Compiler bool DiscardParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState || mState==MinusState) + if (mState==StartState || mState==MinusState) { if (isEmpty()) mTokenLoc = loc; @@ -41,7 +41,7 @@ namespace Compiler bool DiscardParser::parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState) + if (mState==StartState) { if (isEmpty()) mTokenLoc = loc; @@ -55,18 +55,7 @@ namespace Compiler bool DiscardParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) { - if (code==Scanner::S_comma && mState==StartState) - { - if (isEmpty()) - mTokenLoc = loc; - - start(); - - mState = CommaState; - return true; - } - - if (code==Scanner::S_minus && (mState==StartState || mState==CommaState)) + if (code==Scanner::S_minus && mState==StartState) { if (isEmpty()) mTokenLoc = loc; diff --git a/components/compiler/discardparser.hpp b/components/compiler/discardparser.hpp index 15e06756e2..5286676654 100644 --- a/components/compiler/discardparser.hpp +++ b/components/compiler/discardparser.hpp @@ -11,7 +11,7 @@ namespace Compiler { enum State { - StartState, CommaState, MinusState + StartState, MinusState }; State mState; diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 0afe3de6d0..668946f839 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -244,7 +244,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackInt (value, loc); return false; } @@ -267,7 +266,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackFloat (value, loc); return false; } @@ -343,7 +341,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackName (name, loc); return false; } @@ -353,15 +350,21 @@ namespace Compiler { if (const Extensions *extensions = getContext().getExtensions()) { + char returnType; // ignored std::string argumentType; // ignored bool hasExplicit = false; // ignored - if (extensions->isInstruction (keyword, argumentType, hasExplicit)) + bool isInstruction = extensions->isInstruction (keyword, argumentType, hasExplicit); + + if(isInstruction || (mExplicit.empty() && extensions->isFunction(keyword, returnType, argumentType, hasExplicit))) { - // pretend this is not a keyword std::string name = loc.mLiteral; if (name.size()>=2 && name[0]=='"' && name[name.size()-1]=='"') name = name.substr (1, name.size()-2); - return parseName (name, loc, scanner); + if(isInstruction || mLocals.getType(Misc::StringUtils::lowerCase(name)) != ' ') + { + // pretend this is not a keyword + return parseName (name, loc, scanner); + } } } @@ -421,48 +424,31 @@ namespace Compiler if (mNextOperand) { - if (keyword==Scanner::K_getsquareroot) + // check for custom extensions + if (const Extensions *extensions = getContext().getExtensions()) { start(); - mTokenLoc = loc; - parseArguments ("f", scanner); + char returnType; + std::string argumentType; - Generator::squareRoot (mCode); - mOperands.push_back ('f'); + bool hasExplicit = false; - mNextOperand = false; - return true; - } - else - { - // check for custom extensions - if (const Extensions *extensions = getContext().getExtensions()) + if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) { - start(); - - char returnType; - std::string argumentType; - - bool hasExplicit = false; + mTokenLoc = loc; + int optionals = parseArguments (argumentType, scanner); - if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) - { - mTokenLoc = loc; - int optionals = parseArguments (argumentType, scanner); + extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); + mOperands.push_back (returnType); - extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); - mOperands.push_back (returnType); - - mNextOperand = false; - return true; - } + mNextOperand = false; + return true; } } } else { - // no comma was used between arguments scanner.putbackKeyword (keyword, loc); return false; } @@ -497,22 +483,6 @@ namespace Compiler return Parser::parseSpecial (code, loc, scanner); } - if (code==Scanner::S_comma) - { - mTokenLoc = loc; - - if (mFirst) - { - // leading comma - mFirst = false; - return true; - } - - // end marker - scanner.putbackSpecial (code, loc); - return false; - } - mFirst = false; if (code==Scanner::S_newline) @@ -549,7 +519,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackSpecial (code, loc); return false; } @@ -642,7 +611,7 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword) + std::vector& code, int ignoreKeyword, bool expectNames) { bool optional = false; int optionalCount = 0; @@ -669,6 +638,7 @@ namespace Compiler if (argument=='c') stringParser.smashCase(); if (argument=='x') stringParser.discard(); + scanner.enableExpectName(); scanner.scan (stringParser); if ((optional || argument=='x') && stringParser.isEmpty()) @@ -726,6 +696,8 @@ namespace Compiler if (optional) parser.setOptional (true); + if(expectNames) + scanner.enableExpectName(); scanner.scan (parser); diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index 2f3eaa8a9f..42739658ec 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,7 +96,7 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword = -1); + std::vector& code, int ignoreKeyword = -1, bool expectNames = false); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 3989ef0f4f..64133bee84 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -337,6 +337,8 @@ namespace Compiler extensions.registerInstruction ("setnavmeshnumber", "l", opcodeSetNavMeshNumberToRender); extensions.registerFunction ("repairedonme", 'l', "S", opcodeRepairedOnMe, opcodeRepairedOnMeExplicit); extensions.registerInstruction ("togglerecastmesh", "", opcodeToggleRecastMesh); + extensions.registerInstruction ("help", "", opcodeHelp); + extensions.registerInstruction ("reloadlua", "", opcodeReloadLua); } } @@ -551,7 +553,7 @@ namespace Compiler extensions.registerFunction("getpos",'f',"c",opcodeGetPos,opcodeGetPosExplicit); extensions.registerFunction("getstartingpos",'f',"c",opcodeGetStartingPos,opcodeGetStartingPosExplicit); extensions.registerInstruction("position","ffffz",opcodePosition,opcodePositionExplicit); - extensions.registerInstruction("positioncell","ffffcX",opcodePositionCell,opcodePositionCellExplicit); + extensions.registerInstruction("positioncell","ffffczz",opcodePositionCell,opcodePositionCellExplicit); extensions.registerInstruction("placeitemcell","ccffffX",opcodePlaceItemCell); extensions.registerInstruction("placeitem","cffffX",opcodePlaceItem); extensions.registerInstruction("placeatpc","clflX",opcodePlaceAtPc); diff --git a/components/compiler/fileparser.cpp b/components/compiler/fileparser.cpp index c7459c2ae7..9d5c02785e 100644 --- a/components/compiler/fileparser.cpp +++ b/components/compiler/fileparser.cpp @@ -121,11 +121,6 @@ namespace Compiler return false; } } - else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState)) - { - // ignoring comma (for now) - return true; - } return Parser::parseSpecial (code, loc, scanner); } diff --git a/components/compiler/generator.cpp b/components/compiler/generator.cpp index 34da1c4120..3d8b87e69e 100644 --- a/components/compiler/generator.cpp +++ b/components/compiler/generator.cpp @@ -104,11 +104,6 @@ namespace code.push_back (Compiler::Generator::segment5 (17)); } - void opSquareRoot (Compiler::Generator::CodeContainer& code) - { - code.push_back (Compiler::Generator::segment5 (19)); - } - void opReturn (Compiler::Generator::CodeContainer& code) { code.push_back (Compiler::Generator::segment5 (20)); @@ -452,11 +447,6 @@ namespace Compiler::Generator } } - void squareRoot (CodeContainer& code) - { - opSquareRoot (code); - } - void exit (CodeContainer& code) { opReturn (code); diff --git a/components/compiler/generator.hpp b/components/compiler/generator.hpp index 55bba2a75c..00c6e56825 100644 --- a/components/compiler/generator.hpp +++ b/components/compiler/generator.hpp @@ -74,8 +74,6 @@ namespace Compiler void convert (CodeContainer& code, char fromType, char toType); - void squareRoot (CodeContainer& code); - void exit (CodeContainer& code); void message (CodeContainer& code, Literals& literals, const std::string& message, diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 77afaee8bd..3ad8c5bbe2 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -12,7 +12,6 @@ #include "generator.hpp" #include "extensions.hpp" #include "declarationparser.hpp" -#include "exception.hpp" namespace Compiler { @@ -67,6 +66,11 @@ namespace Compiler parseExpression (scanner, loc); return true; } + else if (mState == SetState) + { + // Allow ints to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } return Parser::parseInt (value, loc, scanner); } @@ -131,7 +135,7 @@ namespace Compiler return false; } - if (mState==MessageState || mState==MessageCommaState) + if (mState==MessageState) { GetArgumentsFromMessageFormat processor; processor.process(name); @@ -140,7 +144,7 @@ namespace Compiler if (!arguments.empty()) { mExprParser.reset(); - mExprParser.parseArguments (arguments, scanner, mCode); + mExprParser.parseArguments (arguments, scanner, mCode, -1, true); } mName = name; @@ -150,7 +154,7 @@ namespace Compiler return true; } - if (mState==MessageButtonState || mState==MessageButtonCommaState) + if (mState==MessageButtonState) { Generator::pushString (mCode, mLiterals, name); mState = MessageButtonState; @@ -193,7 +197,7 @@ namespace Compiler bool LineParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner) { - if (mState==MessageState || mState==MessageCommaState) + if (mState==MessageState) { if (const Extensions *extensions = getContext().getExtensions()) { @@ -254,33 +258,11 @@ namespace Compiler mExplicit.clear(); } - try - { - // workaround for broken positioncell instructions. - /// \todo add option to disable this - std::unique_ptr errorDowngrade (nullptr); - if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") - errorDowngrade = std::make_unique (getErrorHandler()); - - std::vector code; - int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword); - mCode.insert (mCode.end(), code.begin(), code.end()); - extensions->generateInstructionCode (keyword, mCode, mLiterals, - mExplicit, optionals); - } - catch (const SourceException&) - { - // Ignore argument exceptions for positioncell. - /// \todo add option to disable this - if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") - { - SkipParser skip (getErrorHandler(), getContext()); - scanner.scan (skip); - return false; - } - - throw; - } + std::vector code; + int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword); + mCode.insert (mCode.end(), code.begin(), code.end()); + extensions->generateInstructionCode (keyword, mCode, mLiterals, + mExplicit, optionals); mState = EndState; return true; @@ -441,12 +423,6 @@ namespace Compiler if (code==Scanner::S_newline && (mState==EndState || mState==BeginState)) return false; - if (code==Scanner::S_comma && mState==MessageState) - { - mState = MessageCommaState; - return true; - } - if (code==Scanner::S_ref && mState==SetPotentialMemberVarState) { getErrorHandler().warning ("Stray explicit reference", loc); @@ -474,12 +450,6 @@ namespace Compiler return false; } - if (code==Scanner::S_comma && mState==MessageButtonState) - { - mState = MessageButtonCommaState; - return true; - } - if (code==Scanner::S_member && mState==SetPotentialMemberVarState) { mState = SetMemberVarState; diff --git a/components/compiler/lineparser.hpp b/components/compiler/lineparser.hpp index c434792d18..2fd113478f 100644 --- a/components/compiler/lineparser.hpp +++ b/components/compiler/lineparser.hpp @@ -24,7 +24,7 @@ namespace Compiler BeginState, SetState, SetLocalVarState, SetGlobalVarState, SetPotentialMemberVarState, SetMemberVarState, SetMemberVarState2, - MessageState, MessageCommaState, MessageButtonState, MessageButtonCommaState, + MessageState, MessageButtonState, EndState, PotentialExplicitState, ExplicitState, MemberState }; @@ -86,7 +86,7 @@ namespace Compiler void visitedCharacter(char c) override {} public: - void process(const std::string& message) override + void process(std::string_view message) override { mArguments.clear(); ::Misc::MessageFormatParser::process(message); diff --git a/components/compiler/literals.cpp b/components/compiler/literals.cpp index 774ca4ca7f..a40ff2a02a 100644 --- a/components/compiler/literals.cpp +++ b/components/compiler/literals.cpp @@ -6,12 +6,12 @@ namespace Compiler { int Literals::getIntegerSize() const { - return mIntegers.size() * sizeof (Interpreter::Type_Integer); + return static_cast(mIntegers.size() * sizeof (Interpreter::Type_Integer)); } int Literals::getFloatSize() const { - return mFloats.size() * sizeof (Interpreter::Type_Float); + return static_cast(mFloats.size() * sizeof (Interpreter::Type_Float)); } int Literals::getStringSize() const @@ -41,11 +41,11 @@ namespace Compiler code.resize (size+stringBlockSize/4); - int offset = 0; + size_t offset = 0; for (const auto & mString : mStrings) { - int stringSize = mString.size()+1; + size_t stringSize = mString.size()+1; std::copy (mString.c_str(), mString.c_str()+stringSize, reinterpret_cast (&code[size]) + offset); diff --git a/components/compiler/locals.cpp b/components/compiler/locals.cpp index 9b233b8f53..8492b8649e 100644 --- a/components/compiler/locals.cpp +++ b/components/compiler/locals.cpp @@ -21,7 +21,7 @@ namespace Compiler throw std::logic_error ("Unknown variable type"); } - int Locals::searchIndex (char type, const std::string& name) const + int Locals::searchIndex (char type, std::string_view name) const { const std::vector& collection = get (type); @@ -30,10 +30,10 @@ namespace Compiler if (iter==collection.end()) return -1; - return iter-collection.begin(); + return static_cast(iter-collection.begin()); } - bool Locals::search (char type, const std::string& name) const + bool Locals::search (char type, std::string_view name) const { return searchIndex (type, name)!=-1; } @@ -50,7 +50,7 @@ namespace Compiler throw std::logic_error ("Unknown variable type"); } - char Locals::getType (const std::string& name) const + char Locals::getType (std::string_view name) const { if (search ('s', name)) return 's'; @@ -64,7 +64,7 @@ namespace Compiler return ' '; } - int Locals::getIndex (const std::string& name) const + int Locals::getIndex (std::string_view name) const { int index = searchIndex ('s', name); @@ -94,7 +94,7 @@ namespace Compiler std::ostream_iterator (localFile, " ")); } - void Locals::declare (char type, const std::string& name) + void Locals::declare (char type, std::string_view name) { get (type).push_back (Misc::StringUtils::lowerCase (name)); } diff --git a/components/compiler/locals.hpp b/components/compiler/locals.hpp index 1b2ae60426..c7636ed2a0 100644 --- a/components/compiler/locals.hpp +++ b/components/compiler/locals.hpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace Compiler @@ -19,24 +20,24 @@ namespace Compiler public: - char getType (const std::string& name) const; + char getType (std::string_view name) const; ///< 's': short, 'l': long, 'f': float, ' ': does not exist. - int getIndex (const std::string& name) const; + int getIndex (std::string_view name) const; ///< return index for local variable \a name (-1: does not exist). - bool search (char type, const std::string& name) const; + bool search (char type, std::string_view name) const; /// Return index for local variable \a name of type \a type (-1: variable does not /// exit). - int searchIndex (char type, const std::string& name) const; + int searchIndex (char type, std::string_view name) const; const std::vector& get (char type) const; void write (std::ostream& localFile) const; ///< write declarations to file. - void declare (char type, const std::string& name); + void declare (char type, std::string_view name); ///< declares a variable. void clear(); diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index fdc3e0eabd..49adfbe209 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -318,6 +318,8 @@ namespace Compiler const int opcodeDisableExplicit = 0x200031b; const int opcodeGetDisabledExplicit = 0x200031c; const int opcodeStartScriptExplicit = 0x200031d; + const int opcodeHelp = 0x2000320; + const int opcodeReloadLua = 0x2000321; } namespace Sky diff --git a/components/compiler/parser.cpp b/components/compiler/parser.cpp index ffa393a29e..dc924b1bab 100644 --- a/components/compiler/parser.cpp +++ b/components/compiler/parser.cpp @@ -10,7 +10,7 @@ namespace Compiler { // Report the error and throw an exception. - void Parser::reportSeriousError (const std::string& message, const TokenLoc& loc) + [[noreturn]] void Parser::reportSeriousError (const std::string& message, const TokenLoc& loc) { mErrorHandler.error (message, loc); throw SourceException(); @@ -25,7 +25,7 @@ namespace Compiler // Report an unexpected EOF condition. - void Parser::reportEOF() + [[noreturn]] void Parser::reportEOF() { mErrorHandler.endOfFile(); throw EOFException(); diff --git a/components/compiler/parser.hpp b/components/compiler/parser.hpp index 2ef6e85b98..1f2a57a489 100644 --- a/components/compiler/parser.hpp +++ b/components/compiler/parser.hpp @@ -23,13 +23,13 @@ namespace Compiler protected: - void reportSeriousError (const std::string& message, const TokenLoc& loc); + [[noreturn]] void reportSeriousError (const std::string& message, const TokenLoc& loc); ///< Report the error and throw a exception. void reportWarning (const std::string& message, const TokenLoc& loc); ///< Report the warning without throwing an exception. - void reportEOF(); + [[noreturn]] void reportEOF(); ///< Report an unexpected EOF condition. ErrorHandler& getErrorHandler(); diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 573c8cc93a..161de1bb76 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -1,6 +1,7 @@ #include "scanner.hpp" #include +#include #include "exception.hpp" #include "errorhandler.hpp" @@ -22,6 +23,7 @@ namespace Compiler { mStrictKeywords = false; mTolerantNames = false; + mExpectName = false; mLoc.mColumn = 0; ++mLoc.mLine; mLoc.mLiteral.clear(); @@ -129,7 +131,8 @@ namespace Compiler { bool cont = false; - if (scanInt (c, parser, cont)) + bool scanned = mExpectName ? scanName(c, parser, cont) : scanInt(c, parser, cont); + if (scanned) { mLoc.mLiteral.clear(); return cont; @@ -163,8 +166,6 @@ namespace Compiler std::string value; c.appendTo(value); - bool error = false; - while (get (c)) { if (c.isDigit()) @@ -173,16 +174,11 @@ namespace Compiler } else if (!c.isMinusSign() && isStringCharacter (c)) { - error = true; - c.appendTo(value); + /// workaround that allows names to begin with digits + return scanName(c, parser, cont, value); } else if (c=='.') { - if (error) - { - putback (c); - break; - } return scanFloat (value, parser, cont); } else @@ -192,17 +188,6 @@ namespace Compiler } } - if (error) - { - /// workaround that allows names to begin with digits - /// \todo disable - TokenLoc loc (mLoc); - mLoc.mLiteral.clear(); - cont = parser.parseName (value, loc, *this); - return true; -// return false; - } - TokenLoc loc (mLoc); mLoc.mLiteral.clear(); @@ -264,13 +249,11 @@ namespace Compiler "return", "messagebox", "set", "to", - "getsquareroot", nullptr }; - bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont) + bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont, std::string name) { - std::string name; c.appendTo(name); if (!scanName (name)) @@ -406,6 +389,8 @@ namespace Compiler bool Scanner::scanSpecial (MultiChar& c, Parser& parser, bool& cont) { + bool expectName = mExpectName; + mExpectName = false; int special = -1; if (c=='\n') @@ -416,12 +401,13 @@ namespace Compiler special = S_close; else if (c=='.') { + MultiChar next; // check, if this starts a float literal - if (get (c)) + if (get (next)) { - putback (c); + putback (next); - if (c.isDigit()) + if (next.isDigit()) return scanFloat ("", parser, cont); } @@ -476,13 +462,14 @@ namespace Compiler } else if (c.isMinusSign()) { - if (get (c)) + MultiChar next; + if (get (next)) { - if (c=='>') + if (next=='>') special = S_ref; else { - putback (c); + putback (next); special = S_minus; } } @@ -545,8 +532,6 @@ namespace Compiler else special = S_cmpGT; } - else if (c==',') - special = S_comma; else if (c=='+') special = S_plus; else if (c=='*') @@ -558,6 +543,14 @@ namespace Compiler if (special==S_newline) mLoc.mLiteral = ""; + else if (expectName && (special == S_member || special == S_minus)) + { + bool tolerant = mTolerantNames; + mTolerantNames = true; + bool out = scanName(c, parser, cont); + mTolerantNames = tolerant; + return out; + } TokenLoc loc (mLoc); mLoc.mLiteral.clear(); @@ -590,13 +583,14 @@ namespace Compiler const Extensions *extensions) : mErrorHandler (errorHandler), mStream (inputStream), mExtensions (extensions), mPutback (Putback_None), mPutbackCode(0), mPutbackInteger(0), mPutbackFloat(0), - mStrictKeywords (false), mTolerantNames (false), mIgnoreNewline(false) + mStrictKeywords (false), mTolerantNames (false), mIgnoreNewline(false), mExpectName(false) { } void Scanner::scan (Parser& parser) { while (scanToken (parser)); + mExpectName = false; } void Scanner::putbackSpecial (int code, const TokenLoc& loc) @@ -657,4 +651,9 @@ namespace Compiler { mTolerantNames = true; } + + void Scanner::enableExpectName() + { + mExpectName = true; + } } diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 2139f04b26..7c80f622ee 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include "tokenloc.hpp" @@ -63,7 +63,7 @@ namespace Compiler bool isWhitespace() { - return (mData[0]==' ' || mData[0]=='\t') && mData[1]==0 && mData[2]==0 && mData[3]==0; + return (mData[0]==' ' || mData[0]=='\t' || mData[0]==',') && mData[1]==0 && mData[2]==0 && mData[3]==0; } bool isDigit() @@ -103,7 +103,7 @@ namespace Compiler { blank(); - char ch = in.peek(); + char ch = static_cast(in.peek()); if (!in.good()) return false; @@ -130,7 +130,7 @@ namespace Compiler { std::streampos p_orig = in.tellg(); - char ch = in.peek(); + char ch = static_cast(in.peek()); if (!in.good()) return false; @@ -140,15 +140,12 @@ namespace Compiler for (int i = 0; i <= length; i++) { - if (length >= i) - { - in.get (ch); + in.get (ch); - if (!in.good()) - return false; + if (!in.good()) + return false; - mData[i] = ch; - } + mData[i] = ch; } mLength = length; @@ -159,7 +156,7 @@ namespace Compiler void blank() { - std::fill(mData, mData + sizeof(mData), 0); + std::fill(std::begin(mData), std::end(mData), '\0'); mLength = -1; } @@ -196,6 +193,7 @@ namespace Compiler bool mStrictKeywords; bool mTolerantNames; bool mIgnoreNewline; + bool mExpectName; public: @@ -207,8 +205,7 @@ namespace Compiler K_while, K_endwhile, K_return, K_messagebox, - K_set, K_to, - K_getsquareroot + K_set, K_to }; enum special @@ -217,7 +214,6 @@ namespace Compiler S_open, S_close, S_cmpEQ, S_cmpNE, S_cmpLT, S_cmpLE, S_cmpGT, S_cmpGE, S_plus, S_minus, S_mult, S_div, - S_comma, S_ref, S_member }; @@ -239,7 +235,7 @@ namespace Compiler bool scanFloat (const std::string& intValue, Parser& parser, bool& cont); - bool scanName (MultiChar& c, Parser& parser, bool& cont); + bool scanName (MultiChar& c, Parser& parser, bool& cont, std::string name = {}); /// \param name May contain the start of the name (one or more characters) bool scanName (std::string& name); @@ -289,6 +285,11 @@ namespace Compiler /// /// \attention This mode lasts only until the next newline is reached. void enableTolerantNames(); + + /// Treat '.' and '-' as the start of a name. + /// + /// \attention This mode lasts only until the next newline is reached or the call to scan ends. + void enableExpectName(); }; } diff --git a/components/compiler/streamerrorhandler.hpp b/components/compiler/streamerrorhandler.hpp index ad34166953..f70c64d25d 100644 --- a/components/compiler/streamerrorhandler.hpp +++ b/components/compiler/streamerrorhandler.hpp @@ -1,8 +1,6 @@ #ifndef COMPILER_STREAMERRORHANDLER_H_INCLUDED #define COMPILER_STREAMERRORHANDLER_H_INCLUDED -#include - #include "errorhandler.hpp" namespace Compiler diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index 1bacf79410..a6423211c2 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -13,7 +13,7 @@ namespace Compiler { StringParser::StringParser (ErrorHandler& errorHandler, const Context& context, Literals& literals) - : Parser (errorHandler, context), mLiterals (literals), mState (StartState), mSmashCase (false), mDiscard (false) + : Parser (errorHandler, context), mLiterals (literals), mSmashCase (false), mDiscard (false) { } @@ -21,23 +21,18 @@ namespace Compiler bool StringParser::parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState) - { - start(); - mTokenLoc = loc; - - if (!mDiscard) - { - if (mSmashCase) - Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name)); - else - Generator::pushString (mCode, mLiterals, name); - } + start(); + mTokenLoc = loc; - return false; + if (!mDiscard) + { + if (mSmashCase) + Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name)); + else + Generator::pushString (mCode, mLiterals, name); } - return Parser::parseName (name, loc, scanner); + return false; } bool StringParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner) @@ -63,23 +58,22 @@ namespace Compiler keyword==Scanner::K_elseif || keyword==Scanner::K_while || keyword==Scanner::K_endwhile || keyword==Scanner::K_return || keyword==Scanner::K_messagebox || keyword==Scanner::K_set || - keyword==Scanner::K_to || keyword==Scanner::K_getsquareroot) + keyword==Scanner::K_to) { - return parseName (loc.mLiteral, loc, scanner); + // pretend this is not a keyword + std::string name = loc.mLiteral; + if (name.size()>=2 && name[0]=='"' && name[name.size()-1]=='"') + name = name.substr (1, name.size()-2); + return parseName (name, loc, scanner); } return Parser::parseKeyword (keyword, loc, scanner); } - bool StringParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) + bool StringParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) { - if (code==Scanner::S_comma && mState==StartState) - { - mState = CommaState; - return true; - } - - return Parser::parseSpecial (code, loc, scanner); + reportWarning("Treating integer argument as a string", loc); + return parseName(loc.mLiteral, loc, scanner); } void StringParser::append (std::vector& code) @@ -89,7 +83,6 @@ namespace Compiler void StringParser::reset() { - mState = StartState; mCode.clear(); mSmashCase = false; mTokenLoc = TokenLoc(); diff --git a/components/compiler/stringparser.hpp b/components/compiler/stringparser.hpp index 1976628360..cdc7c676a7 100644 --- a/components/compiler/stringparser.hpp +++ b/components/compiler/stringparser.hpp @@ -14,13 +14,7 @@ namespace Compiler class StringParser : public Parser { - enum State - { - StartState, CommaState - }; - Literals& mLiterals; - State mState; std::vector mCode; bool mSmashCase; TokenLoc mTokenLoc; @@ -39,8 +33,8 @@ namespace Compiler ///< Handle a keyword token. /// \return fetch another token? - bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) override; - ///< Handle a special character token. + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. /// \return fetch another token? void append (std::vector& code); diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 85c090a3ff..3598e739b5 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -7,15 +7,15 @@ #include +const char Config::GameSettings::sArchiveKey[] = "fallback-archive"; const char Config::GameSettings::sContentKey[] = "content"; +const char Config::GameSettings::sDirectoryKey[] = "data"; Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) : mCfgMgr(cfg) { } -Config::GameSettings::~GameSettings() = default; - void Config::GameSettings::validatePaths() { QStringList paths = mSettings.values(QString("data")); @@ -28,7 +28,7 @@ void Config::GameSettings::validatePaths() } // Parse the data dirs to convert the tokenized paths - mCfgMgr.processPaths(dataDirs); + mCfgMgr.processPaths(dataDirs, /*basePath=*/""); mDataDirs.clear(); for (auto & dataDir : dataDirs) { @@ -54,7 +54,7 @@ void Config::GameSettings::validatePaths() QByteArray bytes = local.toUtf8(); dataDirs.push_back(Files::PathContainer::value_type(std::string(bytes.constData(), bytes.length()))); - mCfgMgr.processPaths(dataDirs); + mCfgMgr.processPaths(dataDirs, /*basePath=*/""); if (!dataDirs.empty()) { QString path = QString::fromUtf8(dataDirs.front().string().c_str()); @@ -65,6 +65,14 @@ void Config::GameSettings::validatePaths() } } +std::string Config::GameSettings::getGlobalDataDir() const +{ + // global data dir may not exists if OpenMW is not installed (ie if run from build directory) + if (boost::filesystem::exists(mCfgMgr.getGlobalDataPath())) + return boost::filesystem::canonical(mCfgMgr.getGlobalDataPath()).string(); + return {}; +} + QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) const { if (!mSettings.values(key).isEmpty()) @@ -72,17 +80,17 @@ QStringList Config::GameSettings::values(const QString &key, const QStringList & return defaultValues; } -bool Config::GameSettings::readFile(QTextStream &stream) +bool Config::GameSettings::readFile(QTextStream &stream, bool ignoreContent) { - return readFile(stream, mSettings); + return readFile(stream, mSettings, ignoreContent); } -bool Config::GameSettings::readUserFile(QTextStream &stream) +bool Config::GameSettings::readUserFile(QTextStream &stream, bool ignoreContent) { - return readFile(stream, mUserSettings); + return readFile(stream, mUserSettings, ignoreContent); } -bool Config::GameSettings::readFile(QTextStream &stream, QMultiMap &settings) +bool Config::GameSettings::readFile(QTextStream &stream, QMultiMap &settings, bool ignoreContent) { QMultiMap cache; QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); @@ -102,6 +110,7 @@ bool Config::GameSettings::readFile(QTextStream &stream, QMultiMap &settings); - bool readUserFile(QTextStream &stream); + bool readFile(QTextStream &stream, bool ignoreContent = false); + bool readFile(QTextStream &stream, QMultiMap &settings, bool ignoreContent = false); + bool readUserFile(QTextStream &stream, bool ignoreContent = false); bool writeFile(QTextStream &stream); bool writeFileWithComments(QFile &file); - void setContentList(const QStringList& fileNames); + QStringList getArchiveList() const; + void setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); QStringList getContentList() const; void clear(); @@ -86,7 +87,9 @@ namespace Config QStringList mDataDirs; QString mDataLocal; + static const char sArchiveKey[]; static const char sContentKey[]; + static const char sDirectoryKey[]; static bool isOrderedLine(const QString& line) ; }; diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index ff2c38438f..11ced3c145 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -7,15 +7,15 @@ #include +#include + const char Config::LauncherSettings::sCurrentContentListKey[] = "Profiles/currentprofile"; const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg"; const char Config::LauncherSettings::sContentListsSectionPrefix[] = "Profiles/"; +const char Config::LauncherSettings::sDirectoryListSuffix[] = "/data"; +const char Config::LauncherSettings::sArchiveListSuffix[] = "/fallback-archive"; const char Config::LauncherSettings::sContentListSuffix[] = "/content"; -Config::LauncherSettings::LauncherSettings() = default; - -Config::LauncherSettings::~LauncherSettings() = default; - QStringList Config::LauncherSettings::subKeys(const QString &key) { QMultiMap settings = SettingsBase::getSettings(); @@ -90,6 +90,16 @@ QStringList Config::LauncherSettings::getContentLists() return subKeys(QString(sContentListsSectionPrefix)); } +QString Config::LauncherSettings::makeDirectoryListKey(const QString& contentListName) +{ + return QString(sContentListsSectionPrefix) + contentListName + QString(sDirectoryListSuffix); +} + +QString Config::LauncherSettings::makeArchiveListKey(const QString& contentListName) +{ + return QString(sContentListsSectionPrefix) + contentListName + QString(sArchiveListSuffix); +} + QString Config::LauncherSettings::makeContentListKey(const QString& contentListName) { return QString(sContentListsSectionPrefix) + contentListName + QString(sContentListSuffix); @@ -98,18 +108,28 @@ QString Config::LauncherSettings::makeContentListKey(const QString& contentListN void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) { // obtain content list from game settings (if present) + QStringList dirs(gameSettings.getDataDirs()); + const QStringList archives(gameSettings.getArchiveList()); const QStringList files(gameSettings.getContentList()); // if openmw.cfg has no content, exit so we don't create an empty content list. - if (files.isEmpty()) + if (dirs.isEmpty() || files.isEmpty()) { return; } + // global and local data directories are not part of any profile + const auto globalDataDir = QString(gameSettings.getGlobalDataDir().c_str()); + const auto dataLocal = gameSettings.getDataLocal(); + dirs.removeAll(globalDataDir); + dirs.removeAll(dataLocal); + // if any existing profile in launcher matches the content list, make that profile the default for (const QString &listName : getContentLists()) { - if (isEqual(files, getContentListFiles(listName))) + if (isEqual(files, getContentListFiles(listName)) && + isEqual(archives, getArchiveList(listName)) && + isEqual(dirs, getDataDirectoryList(listName))) { setCurrentContentListName(listName); return; @@ -119,11 +139,13 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) // otherwise, add content list QString newContentListName(makeNewContentListName()); setCurrentContentListName(newContentListName); - setContentList(newContentListName, files); + setContentList(newContentListName, dirs, archives, files); } void Config::LauncherSettings::removeContentList(const QString &contentListName) { + remove(makeDirectoryListKey(contentListName)); + remove(makeArchiveListKey(contentListName)); remove(makeContentListKey(contentListName)); } @@ -133,14 +155,18 @@ void Config::LauncherSettings::setCurrentContentListName(const QString &contentL setValue(QString(sCurrentContentListKey), contentListName); } -void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& fileNames) +void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames) { - removeContentList(contentListName); - QString key = makeContentListKey(contentListName); - for (const QString& fileName : fileNames) + auto const assign = [this](const QString key, const QStringList& list) { - setMultiValue(key, fileName); - } + for (auto const& item : list) + setMultiValue(key, item); + }; + + removeContentList(contentListName); + assign(makeDirectoryListKey(contentListName), dirNames); + assign(makeArchiveListKey(contentListName), archiveNames); + assign(makeContentListKey(contentListName), fileNames); } QString Config::LauncherSettings::getCurrentContentListName() const @@ -148,6 +174,17 @@ QString Config::LauncherSettings::getCurrentContentListName() const return value(QString(sCurrentContentListKey)); } +QStringList Config::LauncherSettings::getDataDirectoryList(const QString& contentListName) const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return reverse(getSettings().values(makeDirectoryListKey(contentListName))); +} + +QStringList Config::LauncherSettings::getArchiveList(const QString& contentListName) const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return reverse(getSettings().values(makeArchiveListKey(contentListName))); +} QStringList Config::LauncherSettings::getContentListFiles(const QString& contentListName) const { // QMap returns multiple rows in LIFO order, so need to reverse diff --git a/components/config/launchersettings.hpp b/components/config/launchersettings.hpp index 1483052bbc..06632423a8 100644 --- a/components/config/launchersettings.hpp +++ b/components/config/launchersettings.hpp @@ -9,9 +9,6 @@ namespace Config class LauncherSettings : public SettingsBase > { public: - LauncherSettings(); - ~LauncherSettings(); - bool writeFile(QTextStream &stream); /// \return names of all Content Lists in the launcher's .cfg file. @@ -21,7 +18,7 @@ namespace Config void setContentList(const GameSettings& gameSettings); /// Create a Content List (or replace if it already exists) - void setContentList(const QString& contentListName, const QStringList& fileNames); + void setContentList(const QString& contentListName, const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); void removeContentList(const QString &contentListName); @@ -29,15 +26,23 @@ namespace Config QString getCurrentContentListName() const; + QStringList getDataDirectoryList(const QString& contentListName) const; + QStringList getArchiveList(const QString& contentListName) const; QStringList getContentListFiles(const QString& contentListName) const; /// \return new list that is reversed order of input static QStringList reverse(const QStringList& toReverse); static const char sLauncherConfigFileName[]; - + private: + /// \return key to use to get/set the files in the specified data Directory List + static QString makeDirectoryListKey(const QString& contentListName); + + /// \return key to use to get/set the files in the specified Archive List + static QString makeArchiveListKey(const QString& contentListName); + /// \return key to use to get/set the files in the specified Content List static QString makeContentListKey(const QString& contentListName); @@ -54,6 +59,8 @@ namespace Config /// section of launcher.cfg holding the Content Lists static const char sContentListsSectionPrefix[]; + static const char sDirectoryListSuffix[]; + static const char sArchiveListSuffix[]; static const char sContentListSuffix[]; }; } diff --git a/components/config/settingsbase.hpp b/components/config/settingsbase.hpp index 86fa962ae0..595fb9d78f 100644 --- a/components/config/settingsbase.hpp +++ b/components/config/settingsbase.hpp @@ -5,7 +5,6 @@ #include #include #include -#include namespace Config { @@ -24,9 +23,7 @@ namespace Config inline void setValue(const QString &key, const QString &value) { - QStringList values = mSettings.values(key); - if (!values.contains(value)) - mSettings.insert(key, value); + mSettings.replace(key, value); } inline void setMultiValue(const QString &key, const QString &value) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 86208d7af6..8f69f6bc0f 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -2,16 +2,17 @@ #include "esmfile.hpp" #include +#include #include -#include #include -#include +#include -ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) : +ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts) : QAbstractTableModel(parent), mWarningIcon(warningIcon), + mShowOMWScripts(showOMWScripts), mMimeType ("application/omwcontent"), mMimeTypes (QStringList() << mMimeType), mColumnCount (1), @@ -53,7 +54,7 @@ const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(in if (row >= 0 && row < mFiles.size()) return mFiles.at(row); - return 0; + return nullptr; } ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(int row) @@ -61,7 +62,7 @@ ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(int row) if (row >= 0 && row < mFiles.count()) return mFiles.at(row); - return 0; + return nullptr; } const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(const QString &name) const { @@ -75,7 +76,7 @@ const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(co if (name.compare(file->fileProperty (fp).toString(), Qt::CaseInsensitive) == 0) return file; } - return 0; + return nullptr; } QModelIndex ContentSelectorModel::ContentModel::indexFromItem(const EsmFile *item) const @@ -107,34 +108,28 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index // addon can be checked if its gamefile is // ... special case, addon with no dependency can be used with any gamefile. - bool gamefileChecked = (file->gameFiles().count() == 0); + bool gamefileChecked = false; + bool noGameFiles = true; for (const QString &fileName : file->gameFiles()) { for (QListIterator dependencyIter(mFiles); dependencyIter.hasNext(); dependencyIter.next()) { //compare filenames only. Multiple instances //of the filename (with different paths) is not relevant here. - bool depFound = (dependencyIter.peekNext()->fileName().compare(fileName, Qt::CaseInsensitive) == 0); - - if (!depFound) + EsmFile* depFile = dependencyIter.peekNext(); + if (!depFile->isGameFile() || depFile->fileName().compare(fileName, Qt::CaseInsensitive) != 0) continue; - if (!gamefileChecked) + noGameFiles = false; + if (isChecked(depFile->filePath())) { - if (isChecked (dependencyIter.peekNext()->filePath())) - gamefileChecked = (dependencyIter.peekNext()->isGameFile()); - } - - // force it to iterate all files in cases where the current - // dependency is a game file to ensure that a later duplicate - // game file is / is not checked. - // (i.e., break only if it's not a gamefile or the game file has been checked previously) - if (gamefileChecked || !(dependencyIter.peekNext()->isGameFile())) + gamefileChecked = true; break; + } } } - if (gamefileChecked) + if (gamefileChecked || noGameFiles) { returnFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } @@ -164,6 +159,24 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int return isLoadOrderError(file) ? mWarningIcon : QVariant(); } + case Qt::BackgroundRole: + { + if (isNew(file->fileName())) + { + return QVariant(QColor(Qt::green)); + } + return QVariant(); + } + + case Qt::ForegroundRole: + { + if (isNew(file->fileName())) + { + return QVariant(QColor(Qt::black)); + } + return QVariant(); + } + case Qt::EditRole: case Qt::DisplayRole: { @@ -417,12 +430,15 @@ void ContentSelectorModel::ContentModel::addFile(EsmFile *file) emit dataChanged (idx, idx); } -void ContentSelectorModel::ContentModel::addFiles(const QString &path) +void ContentSelectorModel::ContentModel::addFiles(const QString &path, bool newfiles) { QDir dir(path); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + if (mShowOMWScripts) + filters << "*.omwscripts"; dir.setNameFilters(filters); + dir.setSorting(QDir::Name); for (const QString &path2 : dir.entryList()) { @@ -431,10 +447,22 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) if (item(info.fileName())) continue; + // Enabled by default in system openmw.cfg; shouldn't be shown in content list. + if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0) + continue; + + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) + { + EsmFile *file = new EsmFile(path2); + file->setDate(info.lastModified()); + file->setFilePath(info.absoluteFilePath()); + addFile(file); + continue; + } + try { ESM::ESMReader fileReader; - ToUTF8::Utf8Encoder encoder = - ToUTF8::calculateEncoding(mEncoding.toStdString()); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding.toStdString())); fileReader.setEncoder(&encoder); fileReader.open(std::string(dir.absoluteFilePath(path2).toUtf8().constData())); @@ -460,6 +488,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) // Put the file in the table addFile(file); + setNew(file->fileName(), newfiles); } catch(std::runtime_error &e) { // An error occurred while reading the .esp @@ -468,8 +497,16 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) } } +} + +bool ContentSelectorModel::ContentModel::containsDataFiles(const QString &path) +{ + QDir dir(path); + QStringList filters; + filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + dir.setNameFilters(filters); - sortFiles(); + return dir.entryList().count() != 0; } void ContentSelectorModel::ContentModel::clearFiles() @@ -498,41 +535,37 @@ QStringList ContentSelectorModel::ContentModel::gameFiles() const void ContentSelectorModel::ContentModel::sortFiles() { - //first, sort the model such that all dependencies are ordered upstream (gamefile) first. - bool movedFiles = true; - int fileCount = mFiles.size(); - + emit layoutAboutToBeChanged(); //Dependency sort - //iterate until no sorting of files occurs - while (movedFiles) + std::unordered_set moved; + for(int i = mFiles.size() - 1; i > 0;) { - movedFiles = false; - //iterate each file, obtaining a reference to it's gamefiles list - for (int i = 0; i < fileCount; i++) + const auto file = mFiles.at(i); + if(moved.find(file) == moved.end()) { - QModelIndex idx1 = index (i, 0, QModelIndex()); - const QStringList &gamefiles = mFiles.at(i)->gameFiles(); - //iterate each file after the current file, verifying that none of it's - //dependencies appear. - for (int j = i + 1; j < fileCount; j++) + int index = -1; + for(int j = 0; j < i; ++j) { - if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive) - || (!mFiles.at(i)->isGameFile() && gamefiles.isEmpty() - && mFiles.at(j)->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files + const QStringList& gameFiles = mFiles.at(j)->gameFiles(); + if(gameFiles.contains(file->fileName(), Qt::CaseInsensitive) + || (!mFiles.at(j)->isGameFile() && gameFiles.isEmpty() + && file->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files { - mFiles.move(j, i); - - QModelIndex idx2 = index (j, 0, QModelIndex()); - - emit dataChanged (idx1, idx2); - - movedFiles = true; + index = j; + break; } } - if (movedFiles) - break; + if(index >= 0) + { + mFiles.move(i, index); + moved.insert(file); + continue; + } } + --i; + moved.clear(); } + emit layoutChanged(); } bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const @@ -543,11 +576,33 @@ bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) cons return false; } -bool ContentSelectorModel::ContentModel::isEnabled (QModelIndex index) const +bool ContentSelectorModel::ContentModel::isEnabled (const QModelIndex& index) const { return (flags(index) & Qt::ItemIsEnabled); } +bool ContentSelectorModel::ContentModel::isNew(const QString& filepath) const +{ + if (mNewFiles.contains(filepath)) + return mNewFiles[filepath]; + + return false; +} + +void ContentSelectorModel::ContentModel::setNew(const QString &filepath, bool isNew) +{ + if (filepath.isEmpty()) + return; + + const EsmFile *file = item(filepath); + + if (!file) + return; + + mNewFiles[filepath] = isNew; +} + + bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile *file) const { return mPluginsWithLoadOrderError.contains(file->filePath()); diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 030865b35a..9a3dddb1c7 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorModel { Q_OBJECT public: - explicit ContentModel(QObject *parent, QIcon warningIcon); + explicit ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts); ~ContentModel(); void setEncoding(const QString &encoding); @@ -43,7 +43,9 @@ namespace ContentSelectorModel QMimeData *mimeData(const QModelIndexList &indexes) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; - void addFiles(const QString &path); + void addFiles(const QString &path, bool newfiles); + void sortFiles(); + bool containsDataFiles(const QString &path); void clearFiles(); QModelIndex indexFromItem(const EsmFile *item) const; @@ -52,9 +54,11 @@ namespace ContentSelectorModel EsmFile *item(int row); QStringList gameFiles() const; - bool isEnabled (QModelIndex index) const; + bool isEnabled (const QModelIndex& index) const; bool isChecked(const QString &filepath) const; bool setCheckState(const QString &filepath, bool isChecked); + bool isNew(const QString &filepath) const; + void setNew(const QString &filepath, bool isChecked); void setContentList(const QStringList &fileList); ContentFileList checkedItems() const; void uncheckAll(); @@ -68,8 +72,6 @@ namespace ContentSelectorModel void addFile(EsmFile *file); - void sortFiles(); - /// Checks a specific plug-in for load order errors /// \return all errors found for specific plug-in QList checkForLoadOrderErrors(const EsmFile *file, int row) const; @@ -80,10 +82,13 @@ namespace ContentSelectorModel QString toolTip(const EsmFile *file) const; ContentFileList mFiles; + QStringList mArchives; QHash mCheckStates; + QHash mNewFiles; QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; + bool mShowOMWScripts; public: diff --git a/components/contentselector/model/esmfile.cpp b/components/contentselector/model/esmfile.cpp index 46a7c96008..60ca199a3b 100644 --- a/components/contentselector/model/esmfile.cpp +++ b/components/contentselector/model/esmfile.cpp @@ -1,6 +1,5 @@ #include "esmfile.hpp" -#include #include int ContentSelectorModel::EsmFile::sPropertyCount = 7; @@ -66,7 +65,7 @@ QByteArray ContentSelectorModel::EsmFile::encodedData() const bool ContentSelectorModel::EsmFile::isGameFile() const { return (mGameFiles.size() == 0) && - (mFileName.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive) || + (mFileName.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive) || mFileName.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive)); } diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index 614eee2987..66863d7c61 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -28,7 +28,7 @@ namespace ContentSelectorModel FileProperty_GameFile = 6 }; - EsmFile(QString fileName = QString(), ModelItem *parent = 0); + EsmFile(QString fileName = QString(), ModelItem *parent = nullptr); // EsmFile(const EsmFile &); ~EsmFile() diff --git a/components/contentselector/model/modelitem.hpp b/components/contentselector/model/modelitem.hpp index e4ea7acc6a..a860b1891a 100644 --- a/components/contentselector/model/modelitem.hpp +++ b/components/contentselector/model/modelitem.hpp @@ -11,7 +11,7 @@ namespace ContentSelectorModel Q_OBJECT public: - ModelItem(ModelItem *parent = 0); + ModelItem(ModelItem *parent = nullptr); //ModelItem(const ModelItem *parent = 0); ~ModelItem(); diff --git a/components/contentselector/view/combobox.cpp b/components/contentselector/view/combobox.cpp index 1ef9f9bd75..310806240d 100644 --- a/components/contentselector/view/combobox.cpp +++ b/components/contentselector/view/combobox.cpp @@ -1,6 +1,8 @@ #include #include +#include + #include "combobox.hpp" ContentSelectorView::ComboBox::ComboBox(QWidget *parent) : @@ -9,7 +11,7 @@ ContentSelectorView::ComboBox::ComboBox(QWidget *parent) : mValidator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore setValidator(mValidator); setEditable(true); - setCompleter(0); + setCompleter(nullptr); setEnabled (true); setInsertPolicy(QComboBox::NoInsert); diff --git a/components/contentselector/view/combobox.hpp b/components/contentselector/view/combobox.hpp index c57a5f1169..ebca824c3d 100644 --- a/components/contentselector/view/combobox.hpp +++ b/components/contentselector/view/combobox.hpp @@ -2,7 +2,6 @@ #define COMBOBOX_HPP #include -#include class QString; class QRegExpValidator; @@ -14,7 +13,7 @@ namespace ContentSelectorView Q_OBJECT public: - explicit ComboBox (QWidget *parent = 0); + explicit ComboBox (QWidget *parent = nullptr); void setPlaceholderText(const QString &text); diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 6bb8e6e2c7..0a12b01942 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -3,40 +3,35 @@ #include #include - #include -#include - #include #include -ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : +ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent, bool showOMWScripts) : QObject(parent) { ui.setupUi(parent); ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); - buildContentModel(); + buildContentModel(showOMWScripts); buildGameFileView(); buildAddonView(); } -void ContentSelectorView::ContentSelector::buildContentModel() +void ContentSelectorView::ContentSelector::buildContentModel(bool showOMWScripts) { QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); - mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon); + mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } void ContentSelectorView::ContentSelector::buildGameFileView() { - ui.gameFileView->setVisible (true); - - ui.gameFileView->setPlaceholderText(QString("Select a game file...")); + ui.gameFileView->addItem(""); + ui.gameFileView->setVisible(true); connect (ui.gameFileView, SIGNAL (currentIndexChanged(int)), this, SLOT (slotCurrentGameFileIndexChanged(int))); - ui.gameFileView->setCurrentIndex(-1); ui.gameFileView->setCurrentIndex(0); } @@ -108,7 +103,7 @@ void ContentSelectorView::ContentSelector::setProfileContent(const QStringList & void ContentSelectorView::ContentSelector::setGameFile(const QString &filename) { - int index = -1; + int index = 0; if (!filename.isEmpty()) { @@ -155,9 +150,9 @@ ContentSelectorModel::ContentFileList return mContentModel->checkedItems(); } -void ContentSelectorView::ContentSelector::addFiles(const QString &path) +void ContentSelectorView::ContentSelector::addFiles(const QString &path, bool newfiles) { - mContentModel->addFiles(path); + mContentModel->addFiles(path, newfiles); // add any game files to the combo box for (const QString& gameFileName : mContentModel->gameFiles()) @@ -168,10 +163,21 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path) } } - if (ui.gameFileView->currentIndex() != -1) - ui.gameFileView->setCurrentIndex(-1); + if (ui.gameFileView->currentIndex() != 0) + ui.gameFileView->setCurrentIndex(0); mContentModel->uncheckAll(); + mContentModel->checkForLoadOrderErrors(); +} + +void ContentSelectorView::ContentSelector::sortFiles() +{ + mContentModel->sortFiles(); +} + +bool ContentSelectorView::ContentSelector::containsDataFiles(const QString &path) +{ + return mContentModel->containsDataFiles(path); } void ContentSelectorView::ContentSelector::clearFiles() @@ -183,7 +189,7 @@ QString ContentSelectorView::ContentSelector::currentFile() const { QModelIndex currentIdx = ui.addonView->currentIndex(); - if (!currentIdx.isValid()) + if (!currentIdx.isValid() && ui.gameFileView->currentIndex() > 0) return ui.gameFileView->currentText(); QModelIndex idx = mContentModel->index(mAddonProxyModel->mapToSource(currentIdx).row(), 0, QModelIndex()); diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index f1058d510f..75ca3e17b4 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -23,11 +23,13 @@ namespace ContentSelectorView public: - explicit ContentSelector(QWidget *parent = 0); + explicit ContentSelector(QWidget *parent = nullptr, bool showOMWScripts = false); QString currentFile() const; - void addFiles(const QString &path); + void addFiles(const QString &path, bool newfiles = false); + void sortFiles(); + bool containsDataFiles(const QString &path); void clearFiles(); void setProfileContent (const QStringList &fileList); @@ -40,7 +42,7 @@ namespace ContentSelectorView void setGameFile (const QString &filename = QString("")); bool isGamefileSelected() const - { return ui.gameFileView->currentIndex() != -1; } + { return ui.gameFileView->currentIndex() > 0; } QWidget *uiWidget() const { return ui.contentGroupBox; } @@ -56,7 +58,7 @@ namespace ContentSelectorView Ui::ContentSelector ui; - void buildContentModel(); + void buildContentModel(bool showOMWScripts); void buildGameFileView(); void buildAddonView(); void buildContextMenu(); diff --git a/components/crashcatcher/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp index 307c08d95f..d2fad06c72 100644 --- a/components/crashcatcher/crashcatcher.cpp +++ b/components/crashcatcher/crashcatcher.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -16,11 +19,6 @@ #include -#include -#include - -namespace bfs = boost::filesystem; - #include #ifdef __linux__ @@ -43,7 +41,7 @@ namespace bfs = boost::filesystem; #include #endif -static const char crash_switch[] = "--cc-handle-crash"; +#include "crashcatcher.hpp" static const char fatal_err[] = "\n\n*** Fatal Error ***\n"; static const char pipe_err[] = "!!! Failed to create pipe\n"; @@ -56,8 +54,6 @@ static const char exec_err[] = "!!! Failed to exec debug process\n"; static char argv0[PATH_MAX]; -static char altstack[SIGSTKSZ]; - static struct { int signum; @@ -146,11 +142,13 @@ static void gdb_info(pid_t pid) /* * Create a temp file to put gdb commands into. * Note: POSIX.1-2008 declares that the file should be already created with mode 0600 by default. - * Modern systems implement it and and suggest to do not touch masks in multithreaded applications. + * Modern systems implement it and suggest to do not touch masks in multithreaded applications. * So CoverityScan warning is valid only for ancient versions of stdlib. */ strcpy(respfile, "/tmp/gdb-respfile-XXXXXX"); - // coverity[secure_temp] +#ifdef __COVERITY__ + umask(0600); +#endif if((fd=mkstemp(respfile)) >= 0 && (f=fdopen(fd, "w")) != nullptr) { fprintf(f, "attach %d\n" @@ -451,7 +449,7 @@ static void getExecPath(char **argv) if(argv[0][0] == '/') snprintf(argv0, sizeof(argv0), "%s", argv[0]); - else if (getcwd(argv0, sizeof(argv0)) != NULL) + else if (getcwd(argv0, sizeof(argv0)) != nullptr) { cwdlen = strlen(argv0); snprintf(argv0+cwdlen, sizeof(argv0)-cwdlen, "/%s", argv[0]); @@ -473,9 +471,10 @@ int crashCatcherInstallHandlers(int argc, char **argv, int num_signals, int *sig /* Set an alternate signal stack so SIGSEGVs caused by stack overflows * still run */ + static char* altstack = new char [SIGSTKSZ]; altss.ss_sp = altstack; altss.ss_flags = 0; - altss.ss_size = sizeof(altstack); + altss.ss_size = SIGSTKSZ; sigaltstack(&altss, nullptr); memset(&sa, 0, sizeof(sa)); @@ -500,10 +499,10 @@ int crashCatcherInstallHandlers(int argc, char **argv, int num_signals, int *sig static bool is_debugger_present() { #if defined (__linux__) - bfs::path procstatus = bfs::path("/proc/self/status"); - if (bfs::exists(procstatus)) + std::filesystem::path procstatus = std::filesystem::path("/proc/self/status"); + if (std::filesystem::exists(procstatus)) { - bfs::ifstream file((procstatus)); + std::ifstream file((procstatus)); while (!file.eof()) { std::string word; @@ -561,9 +560,6 @@ static bool is_debugger_present() void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath) { - if (const auto env = std::getenv("OPENMW_DISABLE_CRASH_CATCHER")) - if (std::atol(env) != 0) - return; if ((argc == 2 && strcmp(argv[1], crash_switch) == 0) || !is_debugger_present()) { int s[5] = { SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGABRT }; diff --git a/components/crashcatcher/crashcatcher.hpp b/components/crashcatcher/crashcatcher.hpp index fd8f0d1542..b693ccae43 100644 --- a/components/crashcatcher/crashcatcher.hpp +++ b/components/crashcatcher/crashcatcher.hpp @@ -9,6 +9,8 @@ #define USE_CRASH_CATCHER 0 #endif +constexpr char crash_switch[] = "--cc-handle-crash"; + #if USE_CRASH_CATCHER extern void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath); #else diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windows_crashcatcher.cpp new file mode 100644 index 0000000000..d31fdac578 --- /dev/null +++ b/components/crashcatcher/windows_crashcatcher.cpp @@ -0,0 +1,204 @@ +#include "windows_crashcatcher.hpp" + +#include +#include +#include +#include + +#include "windows_crashmonitor.hpp" +#include "windows_crashshm.hpp" +#include + +namespace Crash +{ + + HANDLE duplicateHandle(HANDLE handle) + { + HANDLE duplicate; + if (!DuplicateHandle(GetCurrentProcess(), handle, + GetCurrentProcess(), &duplicate, + 0, TRUE, DUPLICATE_SAME_ACCESS)) + { + throw std::runtime_error("Crash monitor could not duplicate handle"); + } + return duplicate; + } + + CrashCatcher* CrashCatcher::sInstance = nullptr; + + CrashCatcher::CrashCatcher(int argc, char **argv, const std::string& crashLogPath) + { + assert(sInstance == nullptr); // don't allow two instances + + sInstance = this; + + HANDLE shmHandle = nullptr; + for (int i=0; i= argc - 1) + throw std::runtime_error("Crash monitor is missing the SHM handle argument"); + + sscanf(argv[i + 1], "%p", &shmHandle); + break; + } + + if (!shmHandle) + { + setupIpc(); + startMonitorProcess(crashLogPath); + installHandler(); + } + else + { + CrashMonitor(shmHandle).run(); + exit(0); + } + } + + CrashCatcher::~CrashCatcher() + { + sInstance = nullptr; + + if (mShm && mSignalMonitorEvent) + { + shmLock(); + mShm->mEvent = CrashSHM::Event::Shutdown; + shmUnlock(); + + SetEvent(mSignalMonitorEvent); + } + + if (mShmHandle) + CloseHandle(mShmHandle); + } + + void CrashCatcher::setupIpc() + { + SECURITY_ATTRIBUTES attributes; + ZeroMemory(&attributes, sizeof(attributes)); + attributes.bInheritHandle = TRUE; + + mSignalAppEvent = CreateEventW(&attributes, FALSE, FALSE, NULL); + mSignalMonitorEvent = CreateEventW(&attributes, FALSE, FALSE, NULL); + + mShmHandle = CreateFileMappingW(INVALID_HANDLE_VALUE, &attributes, PAGE_READWRITE, HIWORD(sizeof(CrashSHM)), LOWORD(sizeof(CrashSHM)), NULL); + if (mShmHandle == nullptr) + throw std::runtime_error("Failed to allocate crash catcher shared memory"); + + mShm = reinterpret_cast(MapViewOfFile(mShmHandle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(CrashSHM))); + if (mShm == nullptr) + throw std::runtime_error("Failed to map crash catcher shared memory"); + + mShmMutex = CreateMutexW(&attributes, FALSE, NULL); + if (mShmMutex == nullptr) + throw std::runtime_error("Failed to create crash catcher shared memory mutex"); + } + + void CrashCatcher::shmLock() + { + if (WaitForSingleObject(mShmMutex, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("SHM lock timed out"); + } + + void CrashCatcher::shmUnlock() + { + ReleaseMutex(mShmMutex); + } + + void CrashCatcher::waitMonitor() + { + if (WaitForSingleObject(mSignalAppEvent, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("Waiting for monitor failed"); + } + + void CrashCatcher::signalMonitor() + { + SetEvent(mSignalMonitorEvent); + } + + void CrashCatcher::installHandler() + { + SetUnhandledExceptionFilter(vectoredExceptionHandler); + } + + void CrashCatcher::startMonitorProcess(const std::string& crashLogPath) + { + std::wstring executablePath; + DWORD copied = 0; + do { + executablePath.resize(executablePath.size() + MAX_PATH); + copied = GetModuleFileNameW(nullptr, executablePath.data(), static_cast(executablePath.size())); + } while (copied >= executablePath.size()); + executablePath.resize(copied); + + memset(mShm->mStartup.mLogFilePath, 0, sizeof(mShm->mStartup.mLogFilePath)); + size_t length = crashLogPath.length(); + if (length >= MAX_LONG_PATH) length = MAX_LONG_PATH - 1; + strncpy(mShm->mStartup.mLogFilePath, crashLogPath.c_str(), length); + mShm->mStartup.mLogFilePath[length] = '\0'; + + // note that we don't need to lock the SHM here, the other process has not started yet + mShm->mEvent = CrashSHM::Event::Startup; + mShm->mStartup.mShmMutex = duplicateHandle(mShmMutex); + mShm->mStartup.mAppProcessHandle = duplicateHandle(GetCurrentProcess()); + mShm->mStartup.mAppMainThreadId = GetThreadId(GetCurrentThread()); + mShm->mStartup.mSignalApp = duplicateHandle(mSignalAppEvent); + mShm->mStartup.mSignalMonitor = duplicateHandle(mSignalMonitorEvent); + + std::wstringstream ss; + ss << "--crash-monitor " << std::hex << duplicateHandle(mShmHandle); + std::wstring arguments(ss.str()); + + STARTUPINFOW si; + ZeroMemory(&si, sizeof(si)); + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessW(executablePath.data(), arguments.data(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) + throw std::runtime_error("Could not start crash monitor process"); + + waitMonitor(); + } + + LONG CrashCatcher::vectoredExceptionHandler(PEXCEPTION_POINTERS info) + { + switch (info->ExceptionRecord->ExceptionCode) + { + case EXCEPTION_SINGLE_STEP: + case EXCEPTION_BREAKPOINT: + case DBG_PRINTEXCEPTION_C: + return EXCEPTION_EXECUTE_HANDLER; + } + if (!sInstance) + return EXCEPTION_EXECUTE_HANDLER; + + sInstance->handleVectoredException(info); + + _Exit(1); + } + + void CrashCatcher::handleVectoredException(PEXCEPTION_POINTERS info) + { + shmLock(); + + mShm->mEvent = CrashSHM::Event::Crashed; + mShm->mCrashed.mThreadId = GetCurrentThreadId(); + mShm->mCrashed.mContext = *info->ContextRecord; + mShm->mCrashed.mExceptionRecord = *info->ExceptionRecord; + + shmUnlock(); + + signalMonitor(); + + // must remain until monitor has finished + waitMonitor(); + + std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !"; + SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr); + } + +} // namespace Crash diff --git a/components/crashcatcher/windows_crashcatcher.hpp b/components/crashcatcher/windows_crashcatcher.hpp new file mode 100644 index 0000000000..1133afe69c --- /dev/null +++ b/components/crashcatcher/windows_crashcatcher.hpp @@ -0,0 +1,76 @@ +#ifndef WINDOWS_CRASHCATCHER_HPP +#define WINDOWS_CRASHCATCHER_HPP + +#include + +#include +#include + +namespace Crash +{ + + // The implementation spawns the current executable as a monitor process which waits + // for a global synchronization event which is sent when the parent process crashes. + // The monitor process then extracts crash information from the parent process while + // the parent process waits for the monitor process to finish. The crashed process + // quits and the monitor writes the crash information to a file. + // + // To detect unexpected shutdowns of the application which are not handled by the + // crash handler, the monitor periodically checks the exit code of the parent + // process and exits if it does not return STILL_ACTIVE. You can test this by closing + // the main openmw process in task manager. + + static constexpr const int CrashCatcherTimeout = 2500; + + struct CrashSHM; + + class CrashCatcher final + { + public: + + CrashCatcher(int argc, char **argv, const std::string& crashLogPath); + ~CrashCatcher(); + + private: + + static CrashCatcher* sInstance; + + // mapped SHM area + CrashSHM* mShm = nullptr; + // the handle is allocated by the catcher and passed to the monitor + // process via the command line which maps the SHM and sends / receives + // events through it + HANDLE mShmHandle = nullptr; + // mutex which guards SHM area + HANDLE mShmMutex = nullptr; + + // triggered when the monitor signals the application + HANDLE mSignalAppEvent = INVALID_HANDLE_VALUE; + + // triggered when the application wants to wake the monitor process + HANDLE mSignalMonitorEvent = INVALID_HANDLE_VALUE; + + void setupIpc(); + + void shmLock(); + + void shmUnlock(); + + void startMonitorProcess(const std::string& crashLogPath); + + void waitMonitor(); + + void signalMonitor(); + + void installHandler(); + + void handleVectoredException(PEXCEPTION_POINTERS info); + + public: + + static LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS info); + }; + +} // namespace Crash + +#endif // WINDOWS_CRASHCATCHER_HPP diff --git a/components/crashcatcher/windows_crashmonitor.cpp b/components/crashcatcher/windows_crashmonitor.cpp new file mode 100644 index 0000000000..d2e5737949 --- /dev/null +++ b/components/crashcatcher/windows_crashmonitor.cpp @@ -0,0 +1,324 @@ +#include "windows_crashmonitor.hpp" + +#include +#include + +#include + +#include +#include + +#include + +#include "windows_crashcatcher.hpp" +#include "windows_crashshm.hpp" +#include + +namespace Crash +{ + std::unordered_map CrashMonitor::smEventHookOwners{}; + + using IsHungAppWindowFn = BOOL(WINAPI*)(HWND hwnd); + + // Obtains the pointer to user32.IsHungAppWindow, this function may be removed in the future. + // See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-ishungappwindow + static IsHungAppWindowFn getIsHungAppWindow() noexcept + { + auto user32Handle = LoadLibraryA("user32.dll"); + if (user32Handle == nullptr) + return nullptr; + + return reinterpret_cast(GetProcAddress(user32Handle, "IsHungAppWindow")); + } + + static const IsHungAppWindowFn sIsHungAppWindow = getIsHungAppWindow(); + + CrashMonitor::CrashMonitor(HANDLE shmHandle) + : mShmHandle(shmHandle) + { + mShm = reinterpret_cast(MapViewOfFile(mShmHandle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(CrashSHM))); + if (mShm == nullptr) + throw std::runtime_error("Failed to map crash monitor shared memory"); + + // accessing SHM without lock is OK here, the parent waits for a signal before continuing + + mShmMutex = mShm->mStartup.mShmMutex; + mAppProcessHandle = mShm->mStartup.mAppProcessHandle; + mAppMainThreadId = mShm->mStartup.mAppMainThreadId; + mSignalAppEvent = mShm->mStartup.mSignalApp; + mSignalMonitorEvent = mShm->mStartup.mSignalMonitor; + } + + CrashMonitor::~CrashMonitor() + { + if (mShm) + UnmapViewOfFile(mShm); + + // the handles received from the app are duplicates, we must close them + + if (mShmHandle) + CloseHandle(mShmHandle); + + if (mShmMutex) + CloseHandle(mShmMutex); + + if (mSignalAppEvent) + CloseHandle(mSignalAppEvent); + + if (mSignalMonitorEvent) + CloseHandle(mSignalMonitorEvent); + } + + void CrashMonitor::shmLock() + { + if (WaitForSingleObject(mShmMutex, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("SHM monitor lock timed out"); + } + + void CrashMonitor::shmUnlock() + { + ReleaseMutex(mShmMutex); + } + + void CrashMonitor::signalApp() const + { + SetEvent(mSignalAppEvent); + } + + bool CrashMonitor::waitApp() const + { + return WaitForSingleObject(mSignalMonitorEvent, CrashCatcherTimeout) == WAIT_OBJECT_0; + } + + bool CrashMonitor::isAppAlive() const + { + DWORD code = 0; + GetExitCodeProcess(mAppProcessHandle, &code); + return code == STILL_ACTIVE; + } + + bool CrashMonitor::isAppFrozen() + { + MSG message; + // Allow the event hook callback to run + PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE); + + if (!mAppWindowHandle) + { + EnumWindows([](HWND handle, LPARAM param) -> BOOL { + CrashMonitor& crashMonitor = *(CrashMonitor*)param; + DWORD processId; + if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mAppMainThreadId && processId == GetProcessId(crashMonitor.mAppProcessHandle)) + { + if (GetWindow(handle, GW_OWNER) == 0) + { + crashMonitor.mAppWindowHandle = handle; + return false; + } + } + return true; + }, (LPARAM)this); + if (mAppWindowHandle) + { + DWORD processId; + GetWindowThreadProcessId(mAppWindowHandle, &processId); + HWINEVENTHOOK eventHookHandle = SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, nullptr, + [](HWINEVENTHOOK hWinEventHook, DWORD event, HWND windowHandle, LONG objectId, LONG childId, DWORD eventThread, DWORD eventTime) + { + CrashMonitor& crashMonitor = *smEventHookOwners[hWinEventHook]; + if (event == EVENT_OBJECT_DESTROY && windowHandle == crashMonitor.mAppWindowHandle && objectId == OBJID_WINDOW && childId == INDEXID_CONTAINER) + { + crashMonitor.mAppWindowHandle = nullptr; + smEventHookOwners.erase(hWinEventHook); + UnhookWinEvent(hWinEventHook); + } + }, processId, mAppMainThreadId, WINEVENT_OUTOFCONTEXT); + smEventHookOwners[eventHookHandle] = this; + } + else + return false; + } + if (sIsHungAppWindow != nullptr) + return sIsHungAppWindow(mAppWindowHandle); + else + { + BOOL debuggerPresent; + + if (CheckRemoteDebuggerPresent(mAppProcessHandle, &debuggerPresent) && debuggerPresent) + return false; + if (SendMessageTimeoutA(mAppWindowHandle, WM_NULL, 0, 0, 0, 5000, nullptr) == 0) + return GetLastError() == ERROR_TIMEOUT; + } + return false; + } + + void CrashMonitor::run() + { + try + { + // app waits for monitor start up, let it continue + signalApp(); + + bool running = true; + bool frozen = false; + while (isAppAlive() && running && !mFreezeAbort) + { + if (isAppFrozen()) + { + if (!frozen) + { + showFreezeMessageBox(); + frozen = true; + } + } + else if (frozen) + { + hideFreezeMessageBox(); + frozen = false; + } + + if (!mFreezeAbort && waitApp()) + { + shmLock(); + + switch (mShm->mEvent) + { + case CrashSHM::Event::None: + break; + case CrashSHM::Event::Crashed: + handleCrash(); + running = false; + break; + case CrashSHM::Event::Shutdown: + running = false; + break; + case CrashSHM::Event::Startup: + break; + } + + shmUnlock(); + } + } + + if (frozen) + hideFreezeMessageBox(); + + if (mFreezeAbort) + { + TerminateProcess(mAppProcessHandle, 0xDEAD); + std::string message = "OpenMW appears to have frozen.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !"; + SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr); + } + + } + catch (...) + { + Log(Debug::Error) << "Exception in crash monitor, exiting"; + } + signalApp(); + } + + static std::wstring utf8ToUtf16(const std::string& utf8) + { + const int nLenWide = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), nullptr, 0); + + std::wstring utf16; + utf16.resize(nLenWide); + if (MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), utf16.data(), nLenWide) != nLenWide) + return {}; + + return utf16; + } + + void CrashMonitor::handleCrash() + { + DWORD processId = GetProcessId(mAppProcessHandle); + + try + { + HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); + if (dbghelp == NULL) + return; + + using MiniDumpWirteDumpFn = BOOL (WINAPI*)( + HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, PMINIDUMP_CALLBACK_INFORMATION CallbackParam + ); + + MiniDumpWirteDumpFn miniDumpWriteDump = (MiniDumpWirteDumpFn)GetProcAddress(dbghelp, "MiniDumpWriteDump"); + if (miniDumpWriteDump == NULL) + return; + + std::wstring utf16Path = utf8ToUtf16(mShm->mStartup.mLogFilePath); + if (utf16Path.empty()) + return; + + if (utf16Path.length() > MAX_PATH) + utf16Path = LR"(\\?\)" + utf16Path; + + HANDLE hCrashLog = CreateFileW(utf16Path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hCrashLog == NULL || hCrashLog == INVALID_HANDLE_VALUE) + return; + if (auto err = GetLastError(); err != ERROR_ALREADY_EXISTS && err != 0) + return; + + EXCEPTION_POINTERS exp; + exp.ContextRecord = &mShm->mCrashed.mContext; + exp.ExceptionRecord = &mShm->mCrashed.mExceptionRecord; + MINIDUMP_EXCEPTION_INFORMATION infos = {}; + infos.ThreadId = mShm->mCrashed.mThreadId; + infos.ExceptionPointers = &exp; + infos.ClientPointers = FALSE; + MINIDUMP_TYPE type = (MINIDUMP_TYPE)(MiniDumpWithDataSegs | MiniDumpWithHandleData); + miniDumpWriteDump(mAppProcessHandle, processId, hCrashLog, type, &infos, 0, 0); + } + catch (const std::exception&e) + { + Log(Debug::Error) << "CrashMonitor: " << e.what(); + } + catch (...) + { + Log(Debug::Error) << "CrashMonitor: unknown exception"; + } + } + + void CrashMonitor::showFreezeMessageBox() + { + std::thread messageBoxThread([&]() { + SDL_MessageBoxButtonData button = { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Abort" }; + SDL_MessageBoxData messageBoxData = { + SDL_MESSAGEBOX_ERROR, + nullptr, + "OpenMW appears to have frozen", + "OpenMW appears to have frozen. Press Abort to terminate it and generate a crash dump.\nIf OpenMW hasn't actually frozen, this message box will disappear a within a few seconds of it becoming responsive.", + 1, + &button, + nullptr + }; + + int buttonId; + if (SDL_ShowMessageBox(&messageBoxData, &buttonId) == 0 && buttonId == 0) + mFreezeAbort = true; + }); + + mFreezeMessageBoxThreadId = GetThreadId(messageBoxThread.native_handle()); + messageBoxThread.detach(); + } + + void CrashMonitor::hideFreezeMessageBox() + { + if (!mFreezeMessageBoxThreadId) + return; + + EnumWindows([](HWND handle, LPARAM param) -> BOOL { + CrashMonitor& crashMonitor = *(CrashMonitor*)param; + DWORD processId; + if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mFreezeMessageBoxThreadId && processId == GetCurrentProcessId()) + PostMessage(handle, WM_CLOSE, 0, 0); + return true; + }, (LPARAM)this); + + mFreezeMessageBoxThreadId = 0; + } + +} // namespace Crash diff --git a/components/crashcatcher/windows_crashmonitor.hpp b/components/crashcatcher/windows_crashmonitor.hpp new file mode 100644 index 0000000000..d6a8dd7ac5 --- /dev/null +++ b/components/crashcatcher/windows_crashmonitor.hpp @@ -0,0 +1,65 @@ +#ifndef WINDOWS_CRASHMONITOR_HPP +#define WINDOWS_CRASHMONITOR_HPP + +#include + +#include +#include + +namespace Crash +{ + +struct CrashSHM; + +class CrashMonitor final +{ +public: + + CrashMonitor(HANDLE shmHandle); + + ~CrashMonitor(); + + void run(); + +private: + + HANDLE mAppProcessHandle = nullptr; + DWORD mAppMainThreadId = 0; + HWND mAppWindowHandle = nullptr; + + // triggered when the monitor process wants to wake the parent process (received via SHM) + HANDLE mSignalAppEvent = nullptr; + // triggered when the application wants to wake the monitor process (received via SHM) + HANDLE mSignalMonitorEvent = nullptr; + + CrashSHM* mShm = nullptr; + HANDLE mShmHandle = nullptr; + HANDLE mShmMutex = nullptr; + + DWORD mFreezeMessageBoxThreadId = 0; + std::atomic_bool mFreezeAbort; + + static std::unordered_map smEventHookOwners; + + void signalApp() const; + + bool waitApp() const; + + bool isAppAlive() const; + + bool isAppFrozen(); + + void shmLock(); + + void shmUnlock(); + + void handleCrash(); + + void showFreezeMessageBox(); + + void hideFreezeMessageBox(); +}; + +} // namespace Crash + +#endif // WINDOWS_CRASHMONITOR_HPP diff --git a/components/crashcatcher/windows_crashshm.hpp b/components/crashcatcher/windows_crashshm.hpp new file mode 100644 index 0000000000..eba478e744 --- /dev/null +++ b/components/crashcatcher/windows_crashshm.hpp @@ -0,0 +1,44 @@ +#ifndef WINDOWS_CRASHSHM_HPP +#define WINDOWS_CRASHSHM_HPP + +#include + +namespace Crash +{ + + // Used to communicate between the app and the monitor, fields are is overwritten with each event. + static constexpr const int MAX_LONG_PATH = 0x7fff; + + struct CrashSHM + { + enum class Event + { + None, + Startup, + Crashed, + Shutdown + }; + + Event mEvent; + + struct Startup + { + HANDLE mAppProcessHandle; + DWORD mAppMainThreadId; + HANDLE mSignalApp; + HANDLE mSignalMonitor; + HANDLE mShmMutex; + char mLogFilePath[MAX_LONG_PATH]; + } mStartup; + + struct Crashed + { + DWORD mThreadId; + CONTEXT mContext; + EXCEPTION_RECORD mExceptionRecord; + } mCrashed; + }; + +} // namespace Crash + +#endif // WINDOWS_CRASHSHM_HPP diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index dfed077e37..e37406299d 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -1,21 +1,39 @@ #include "debugging.hpp" -#include +#include +#include +#include +#include +#include +#include +#include #ifdef _WIN32 -# undef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN -# include +#include +#include #endif +#include + namespace Debug { #ifdef _WIN32 + bool isRedirected(DWORD nStdHandle) + { + DWORD fileType = GetFileType(GetStdHandle(nStdHandle)); + + return (fileType == FILE_TYPE_DISK) || (fileType == FILE_TYPE_PIPE); + } + bool attachParentConsole() { if (GetConsoleWindow() != nullptr) return true; + bool inRedirected = isRedirected(STD_INPUT_HANDLE); + bool outRedirected = isRedirected(STD_OUTPUT_HANDLE); + bool errRedirected = isRedirected(STD_ERROR_HANDLE); + if (AttachConsole(ATTACH_PARENT_PROCESS)) { fflush(stdout); @@ -24,12 +42,21 @@ namespace Debug std::cerr.flush(); // this looks dubious but is really the right way - _wfreopen(L"CON", L"w", stdout); - _wfreopen(L"CON", L"w", stderr); - _wfreopen(L"CON", L"r", stdin); - freopen("CON", "w", stdout); - freopen("CON", "w", stderr); - freopen("CON", "r", stdin); + if (!inRedirected) + { + _wfreopen(L"CON", L"r", stdin); + freopen("CON", "r", stdin); + } + if (!outRedirected) + { + _wfreopen(L"CON", L"w", stdout); + freopen("CON", "w", stdout); + } + if (!errRedirected) + { + _wfreopen(L"CON", L"w", stderr); + freopen("CON", "w", stderr); + } return true; } @@ -38,106 +65,272 @@ namespace Debug } #endif - std::streamsize DebugOutputBase::write(const char *str, std::streamsize size) + static LogListener logListener; + void setLogListener(LogListener listener) { logListener = std::move(listener); } + + class DebugOutputBase : public boost::iostreams::sink { - // Skip debug level marker - Level level = getLevelMarker(str); - if (level != NoLevel) + public: + DebugOutputBase() + { + if (CurrentDebugLevel == NoLevel) + fillCurrentDebugLevel(); + } + + virtual std::streamsize write(const char* str, std::streamsize size) { - writeImpl(str+1, size-1, level); + if (size <= 0) + return size; + std::string_view msg{ str, size_t(size) }; + + // Skip debug level marker + Level level = getLevelMarker(str); + if (level != NoLevel) + msg = msg.substr(1); + + char prefix[32]; + int prefixSize; + { + prefix[0] = '['; + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", std::localtime(&time)) + 1; + char levelLetter = " EWIVD*"[int(level)]; + const auto ms = std::chrono::duration_cast(now.time_since_epoch()).count(); + prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, + ".%03u %c] ", static_cast(ms % 1000), levelLetter); + } + + while (!msg.empty()) + { + if (msg[0] == 0) + break; + size_t lineSize = 1; + while (lineSize < msg.size() && msg[lineSize - 1] != '\n') + lineSize++; + writeImpl(prefix, prefixSize, level); + writeImpl(msg.data(), lineSize, level); + if (logListener) + logListener(level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize)); + msg = msg.substr(lineSize); + } + return size; } - writeImpl(str, size, NoLevel); - return size; - } + virtual ~DebugOutputBase() = default; - Level DebugOutputBase::getLevelMarker(const char *str) + protected: + static Level getLevelMarker(const char* str) + { + if (unsigned(*str) <= unsigned(Marker)) + { + return Level(*str); + } + + return NoLevel; + } + + static void fillCurrentDebugLevel() + { + const char* env = getenv("OPENMW_DEBUG_LEVEL"); + if (env) + { + std::string value(env); + if (value == "ERROR") + CurrentDebugLevel = Error; + else if (value == "WARNING") + CurrentDebugLevel = Warning; + else if (value == "INFO") + CurrentDebugLevel = Info; + else if (value == "VERBOSE") + CurrentDebugLevel = Verbose; + else if (value == "DEBUG") + CurrentDebugLevel = Debug; + + return; + } + + CurrentDebugLevel = Verbose; + } + + virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) + { + return size; + } + }; + +#if defined _WIN32 && defined _DEBUG + class DebugOutput : public DebugOutputBase { - if (unsigned(*str) <= unsigned(Marker)) + public: + std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) { - return Level(*str); + // Make a copy for null termination + std::string tmp(str, static_cast(size)); + // Write string to Visual Studio Debug output + OutputDebugString(tmp.c_str()); + return size; } - return NoLevel; - } + virtual ~DebugOutput() = default; + }; +#else - void DebugOutputBase::fillCurrentDebugLevel() + class Tee : public DebugOutputBase { - const char* env = getenv("OPENMW_DEBUG_LEVEL"); - if (env) + public: + Tee(std::ostream& stream, std::ostream& stream2) + : out(stream), out2(stream2) { - std::string value(env); - if (value == "ERROR") - CurrentDebugLevel = Error; - else if (value == "WARNING") - CurrentDebugLevel = Warning; - else if (value == "INFO") - CurrentDebugLevel = Info; - else if (value == "VERBOSE") - CurrentDebugLevel = Verbose; - else if (value == "DEBUG") - CurrentDebugLevel = Debug; - - return; + // TODO: check which stream is stderr? + mUseColor = useColoredOutput(); + + mColors[Error] = Red; + mColors[Warning] = Yellow; + mColors[Info] = Reset; + mColors[Verbose] = DarkGray; + mColors[Debug] = DarkGray; + mColors[NoLevel] = Reset; } - CurrentDebugLevel = Verbose; - } -} + std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) override + { + out.write(str, size); + out.flush(); -int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName) -{ -#if defined _WIN32 - (void)Debug::attachParentConsole(); + if (mUseColor) + { + out2 << "\033[0;" << mColors[debugLevel] << "m"; + out2.write(str, size); + out2 << "\033[0;" << Reset << "m"; + } + else + { + out2.write(str, size); + } + out2.flush(); + + return size; + } + + virtual ~Tee() = default; + + private: + + static bool useColoredOutput() + { + // Note: cmd.exe in Win10 should support ANSI colors, but in its own way. +#if defined(_WIN32) + return 0; +#else + char* term = getenv("TERM"); + bool useColor = term && !getenv("NO_COLOR") && isatty(fileno(stderr)); + + return useColor; #endif + } + + std::ostream& out; + std::ostream& out2; + bool mUseColor; + + std::map mColors; + }; +#endif + +} - // Some objects used to redirect cout and cerr - // Scope must be here, so this still works inside the catch block for logging exceptions - std::streambuf* cout_rdbuf = std::cout.rdbuf (); - std::streambuf* cerr_rdbuf = std::cerr.rdbuf (); +static std::unique_ptr rawStdout = nullptr; +static std::unique_ptr rawStderr = nullptr; +static std::unique_ptr rawStderrMutex = nullptr; +static std::ofstream logfile; #if defined(_WIN32) && defined(_DEBUG) - boost::iostreams::stream_buffer sb; +static boost::iostreams::stream_buffer sb; #else - boost::iostreams::stream_buffer coutsb; - boost::iostreams::stream_buffer cerrsb; - std::ostream oldcout(cout_rdbuf); - std::ostream oldcerr(cerr_rdbuf); +static boost::iostreams::stream_buffer coutsb; +static boost::iostreams::stream_buffer cerrsb; #endif +std::ostream& getRawStdout() +{ + return rawStdout ? *rawStdout : std::cout; +} + +std::ostream& getRawStderr() +{ + return rawStderr ? *rawStderr : std::cerr; +} + +Misc::Locked getLockedRawStderr() +{ + return Misc::Locked(*rawStderrMutex, getRawStderr()); +} + +// Redirect cout and cerr to the log file +void setupLogging(const std::string& logDir, const std::string& appName, std::ios_base::openmode mode) +{ +#if defined(_WIN32) && defined(_DEBUG) + // Redirect cout and cerr to VS debug output when running in debug mode + sb.open(Debug::DebugOutput()); + std::cout.rdbuf(&sb); + std::cerr.rdbuf(&sb); +#else const std::string logName = Misc::StringUtils::lowerCase(appName) + ".log"; - const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.log"; - boost::filesystem::ofstream logfile; + logfile.open(std::filesystem::path(logDir) / logName, mode); + + coutsb.open(Debug::Tee(logfile, *rawStdout)); + cerrsb.open(Debug::Tee(logfile, *rawStderr)); + + std::cout.rdbuf(&coutsb); + std::cerr.rdbuf(&cerrsb); +#endif +} + +int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], + const std::string& appName, bool autoSetupLogging) +{ +#if defined _WIN32 + (void)Debug::attachParentConsole(); +#endif + rawStdout = std::make_unique(std::cout.rdbuf()); + rawStderr = std::make_unique(std::cerr.rdbuf()); + rawStderrMutex = std::make_unique(); int ret = 0; try { Files::ConfigurationManager cfgMgr; -#if defined(_WIN32) && defined(_DEBUG) - // Redirect cout and cerr to VS debug output when running in debug mode - sb.open(Debug::DebugOutput()); - std::cout.rdbuf (&sb); - std::cerr.rdbuf (&sb); -#else - // Redirect cout and cerr to the log file - logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName)); - - coutsb.open (Debug::Tee(logfile, oldcout)); - cerrsb.open (Debug::Tee(logfile, oldcerr)); + if (autoSetupLogging) + { + std::ios_base::openmode mode = std::ios::out; - std::cout.rdbuf (&coutsb); - std::cerr.rdbuf (&cerrsb); -#endif + // If we are collecting a stack trace, append to existing log file + if (argc == 2 && strcmp(argv[1], crash_switch) == 0) + mode |= std::ios::app; - // install the crash handler as soon as possible. note that the log path - // does not depend on config being read. - crashCatcherInstall(argc, argv, (cfgMgr.getLogPath() / crashLogName).string()); + setupLogging(cfgMgr.getLogPath().string(), appName, mode); + } - ret = innerApplication(argc, argv); + if (const auto env = std::getenv("OPENMW_DISABLE_CRASH_CATCHER"); env == nullptr || std::atol(env) == 0) + { +#if defined(_WIN32) + const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.dmp"; + Crash::CrashCatcher crashy(argc, argv, (cfgMgr.getLogPath() / crashLogName).make_preferred().string()); +#else + const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.log"; + // install the crash handler as soon as possible. note that the log path + // does not depend on config being read. + crashCatcherInstall(argc, argv, (cfgMgr.getLogPath() / crashLogName).string()); +#endif + ret = innerApplication(argc, argv); + } + else + ret = innerApplication(argc, argv); } - catch (std::exception& e) + catch (const std::exception& e) { #if (defined(__APPLE__) || defined(__linux) || defined(__unix) || defined(__posix)) if (!isatty(fileno(stdin))) @@ -150,8 +343,9 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c } // Restore cout and cerr - std::cout.rdbuf(cout_rdbuf); - std::cerr.rdbuf(cerr_rdbuf); + std::cout.rdbuf(rawStdout->rdbuf()); + std::cerr.rdbuf(rawStderr->rdbuf()); + Debug::CurrentDebugLevel = Debug::NoLevel; return ret; } diff --git a/components/debug/debugging.hpp b/components/debug/debugging.hpp index 39390446ff..8ac4bcd8ef 100644 --- a/components/debug/debugging.hpp +++ b/components/debug/debugging.hpp @@ -4,18 +4,10 @@ #include #include -#include - -#include +#include #include "debuglog.hpp" -#if defined _WIN32 && defined _DEBUG -# undef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN -# include -#endif - namespace Debug { // ANSI colors for terminal @@ -27,114 +19,24 @@ namespace Debug Yellow = 93 }; - class DebugOutputBase : public boost::iostreams::sink - { - public: - DebugOutputBase() - { - if (CurrentDebugLevel == NoLevel) - fillCurrentDebugLevel(); - } - - virtual std::streamsize write(const char *str, std::streamsize size); - - virtual ~DebugOutputBase() = default; - - protected: - static Level getLevelMarker(const char *str); - - static void fillCurrentDebugLevel(); - - virtual std::streamsize writeImpl(const char *str, std::streamsize size, Level debugLevel) - { - return size; - } - }; - #ifdef _WIN32 bool attachParentConsole(); #endif -#if defined _WIN32 && defined _DEBUG - class DebugOutput : public DebugOutputBase - { - public: - std::streamsize writeImpl(const char *str, std::streamsize size, Level debugLevel) - { - // Make a copy for null termination - std::string tmp (str, static_cast(size)); - // Write string to Visual Studio Debug output - OutputDebugString (tmp.c_str ()); - return size; - } - - virtual ~DebugOutput() {} - }; -#else - - class Tee : public DebugOutputBase - { - public: - Tee(std::ostream &stream, std::ostream &stream2) - : out(stream), out2(stream2) - { - // TODO: check which stream is stderr? - mUseColor = useColoredOutput(); - - mColors[Error] = Red; - mColors[Warning] = Yellow; - mColors[Info] = Reset; - mColors[Verbose] = DarkGray; - mColors[Debug] = DarkGray; - mColors[NoLevel] = Reset; - } - - std::streamsize writeImpl(const char *str, std::streamsize size, Level debugLevel) override - { - out.write (str, size); - out.flush(); - - if(mUseColor) - { - out2 << "\033[0;" << mColors[debugLevel] << "m"; - out2.write (str, size); - out2 << "\033[0;" << Reset << "m"; - } - else - { - out2.write(str, size); - } - out2.flush(); - - return size; - } - - virtual ~Tee() {} - - private: + using LogListener = std::function; + void setLogListener(LogListener); +} - static bool useColoredOutput() - { - // Note: cmd.exe in Win10 should support ANSI colors, but in its own way. -#if defined(_WIN32) - return 0; -#else - char *term = getenv("TERM"); - bool useColor = term && !getenv("NO_COLOR") && isatty(fileno(stderr)); +// Can be used to print messages without timestamps +std::ostream& getRawStdout(); - return useColor; -#endif - } +std::ostream& getRawStderr(); - std::ostream &out; - std::ostream &out2; - bool mUseColor; +Misc::Locked getLockedRawStderr(); - std::map mColors; - }; -#endif -} +void setupLogging(const std::string& logDir, const std::string& appName, std::ios_base::openmode = std::ios::out); -int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName); +int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], + const std::string& appName, bool autoSetupLogging = true); #endif diff --git a/components/debug/debuglog.cpp b/components/debug/debuglog.cpp index 510c638614..f4f0fdffa6 100644 --- a/components/debug/debuglog.cpp +++ b/components/debug/debuglog.cpp @@ -1,8 +1,36 @@ #include "debuglog.hpp" +#include namespace Debug { Level CurrentDebugLevel = Level::NoLevel; } -std::mutex Log::sLock; +static std::mutex sLock; + +Log::Log(Debug::Level level) + : mShouldLog(level <= Debug::CurrentDebugLevel) +{ + // No need to hold the lock if there will be no logging anyway + if (!mShouldLog) + return; + + // Locks a global lock while the object is alive + sLock.lock(); + + // If the app has no logging system enabled, log level is not specified. + // Show all messages without marker - we just use the plain cout in this case. + if (Debug::CurrentDebugLevel == Debug::NoLevel) + return; + + std::cout << static_cast(level); +} + +Log::~Log() +{ + if (!mShouldLog) + return; + + std::cout << std::endl; + sLock.unlock(); +} diff --git a/components/debug/debuglog.hpp b/components/debug/debuglog.hpp index 0da5b9cbdd..aa8156e119 100644 --- a/components/debug/debuglog.hpp +++ b/components/debug/debuglog.hpp @@ -1,11 +1,8 @@ #ifndef DEBUG_LOG_H #define DEBUG_LOG_H -#include #include -#include - namespace Debug { enum Level @@ -25,42 +22,22 @@ namespace Debug class Log { - static std::mutex sLock; - - std::unique_lock mLock; public: - // Locks a global lock while the object is alive - Log(Debug::Level level) : - mLock(sLock), - mLevel(level) - { - // If the app has no logging system enabled, log level is not specified. - // Show all messages without marker - we just use the plain cout in this case. - if (Debug::CurrentDebugLevel == Debug::NoLevel) - return; - - if (mLevel <= Debug::CurrentDebugLevel) - std::cout << static_cast(mLevel); - } + explicit Log(Debug::Level level); + ~Log(); // Perfect forwarding wrappers to give the chain of objects to cout template Log& operator<<(T&& rhs) { - if (mLevel <= Debug::CurrentDebugLevel) + if (mShouldLog) std::cout << std::forward(rhs); return *this; } - ~Log() - { - if (mLevel <= Debug::CurrentDebugLevel) - std::cout << std::endl; - } - private: - Debug::Level mLevel; + const bool mShouldLog; }; #endif diff --git a/components/debug/gldebug.cpp b/components/debug/gldebug.cpp index 3c5ec728ac..81bd7eb003 100644 --- a/components/debug/gldebug.cpp +++ b/components/debug/gldebug.cpp @@ -32,133 +32,251 @@ either expressed or implied, of the FreeBSD Project. #include "gldebug.hpp" #include +#include #include // OpenGL constants not provided by OSG: #include -void debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) +namespace Debug { -#ifdef GL_DEBUG_OUTPUT - std::string srcStr; - switch (source) + + void GL_APIENTRY debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) { - case GL_DEBUG_SOURCE_API: - srcStr = "API"; - break; - case GL_DEBUG_SOURCE_WINDOW_SYSTEM: - srcStr = "WINDOW_SYSTEM"; - break; - case GL_DEBUG_SOURCE_SHADER_COMPILER: - srcStr = "SHADER_COMPILER"; - break; - case GL_DEBUG_SOURCE_THIRD_PARTY: - srcStr = "THIRD_PARTY"; - break; - case GL_DEBUG_SOURCE_APPLICATION: - srcStr = "APPLICATION"; - break; - case GL_DEBUG_SOURCE_OTHER: - srcStr = "OTHER"; - break; - default: - srcStr = "UNDEFINED"; - break; +#ifdef GL_DEBUG_OUTPUT + std::string srcStr; + switch (source) + { + case GL_DEBUG_SOURCE_API: + srcStr = "API"; + break; + case GL_DEBUG_SOURCE_WINDOW_SYSTEM: + srcStr = "WINDOW_SYSTEM"; + break; + case GL_DEBUG_SOURCE_SHADER_COMPILER: + srcStr = "SHADER_COMPILER"; + break; + case GL_DEBUG_SOURCE_THIRD_PARTY: + srcStr = "THIRD_PARTY"; + break; + case GL_DEBUG_SOURCE_APPLICATION: + srcStr = "APPLICATION"; + break; + case GL_DEBUG_SOURCE_OTHER: + srcStr = "OTHER"; + break; + default: + srcStr = "UNDEFINED"; + break; + } + + std::string typeStr; + + Level logSeverity = Warning; + switch (type) + { + case GL_DEBUG_TYPE_ERROR: + typeStr = "ERROR"; + logSeverity = Error; + break; + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + typeStr = "DEPRECATED_BEHAVIOR"; + logSeverity = Warning; + break; + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + typeStr = "UNDEFINED_BEHAVIOR"; + logSeverity = Warning; + break; + case GL_DEBUG_TYPE_PORTABILITY: + typeStr = "PORTABILITY"; + logSeverity = Debug; + break; + case GL_DEBUG_TYPE_PERFORMANCE: + typeStr = "PERFORMANCE"; + logSeverity = Debug; + break; + case GL_DEBUG_TYPE_OTHER: + typeStr = "OTHER"; + logSeverity = Debug; + break; + default: + typeStr = "UNDEFINED"; + break; + } + + Log(logSeverity) << "OpenGL " << typeStr << " [" << srcStr << "]: " << message; +#endif } - std::string typeStr; + class PushDebugGroup + { + public: + static std::unique_ptr sInstance; + + void (GL_APIENTRY * glPushDebugGroup) (GLenum source, GLuint id, GLsizei length, const GLchar * message); + + void (GL_APIENTRY * glPopDebugGroup) (void); - Debug::Level logSeverity = Debug::Warning; - switch (type) + bool valid() + { + return glPushDebugGroup && glPopDebugGroup; + } + }; + + std::unique_ptr PushDebugGroup::sInstance{ std::make_unique() }; + + EnableGLDebugOperation::EnableGLDebugOperation() : osg::GraphicsOperation("EnableGLDebugOperation", false) { - case GL_DEBUG_TYPE_ERROR: - typeStr = "ERROR"; - logSeverity = Debug::Error; - break; - case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: - typeStr = "DEPRECATED_BEHAVIOR"; - break; - case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: - typeStr = "UNDEFINED_BEHAVIOR"; - break; - case GL_DEBUG_TYPE_PORTABILITY: - typeStr = "PORTABILITY"; - break; - case GL_DEBUG_TYPE_PERFORMANCE: - typeStr = "PERFORMANCE"; - break; - case GL_DEBUG_TYPE_OTHER: - typeStr = "OTHER"; - break; - default: - typeStr = "UNDEFINED"; - break; } - Log(logSeverity) << "OpenGL " << typeStr << " [" << srcStr << "]: " << message; + void EnableGLDebugOperation::operator()(osg::GraphicsContext* graphicsContext) + { +#ifdef GL_DEBUG_OUTPUT + OpenThreads::ScopedLock lock(mMutex); + + unsigned int contextID = graphicsContext->getState()->getContextID(); + + typedef void (GL_APIENTRY *DEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); + typedef void (GL_APIENTRY *GLDebugMessageControlFunction)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled); + typedef void (GL_APIENTRY *GLDebugMessageCallbackFunction)(DEBUGPROC, const void* userParam); + + GLDebugMessageControlFunction glDebugMessageControl = nullptr; + GLDebugMessageCallbackFunction glDebugMessageCallback = nullptr; + + if (osg::isGLExtensionSupported(contextID, "GL_KHR_debug")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallback"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControl"); + osg::setGLExtensionFuncPtr(PushDebugGroup::sInstance->glPushDebugGroup, "glPushDebugGroup"); + osg::setGLExtensionFuncPtr(PushDebugGroup::sInstance->glPopDebugGroup, "glPopDebugGroup"); + } + else if (osg::isGLExtensionSupported(contextID, "GL_ARB_debug_output")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackARB"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlARB"); + } + else if (osg::isGLExtensionSupported(contextID, "GL_AMD_debug_output")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackAMD"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlAMD"); + } + + if (glDebugMessageCallback && glDebugMessageControl) + { + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_MEDIUM, 0, nullptr, true); + glDebugMessageCallback(debugCallback, nullptr); + + Log(Info) << "OpenGL debug callback attached."; + } + else #endif -} + Log(Error) << "Unable to attach OpenGL debug callback."; + } -void enableGLDebugExtension(unsigned int contextID) -{ -#ifdef GL_DEBUG_OUTPUT - typedef void (GL_APIENTRY *DEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); - typedef void (GL_APIENTRY *GLDebugMessageControlFunction)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled); - typedef void (GL_APIENTRY *GLDebugMessageCallbackFunction)(DEBUGPROC, const void* userParam); - - GLDebugMessageControlFunction glDebugMessageControl = nullptr; - GLDebugMessageCallbackFunction glDebugMessageCallback = nullptr; - - if (osg::isGLExtensionSupported(contextID, "GL_KHR_debug")) + bool shouldDebugOpenGL() + { + const char* env = std::getenv("OPENMW_DEBUG_OPENGL"); + if (!env) + return false; + std::string str(env); + if (str.length() == 0) + return true; + + return str.find("OFF") == std::string::npos && str.find('0') == std::string::npos && str.find("NO") == std::string::npos; + } + + DebugGroup::DebugGroup(const std::string & message, GLuint id) + #ifdef GL_DEBUG_OUTPUT + : mSource(GL_DEBUG_SOURCE_APPLICATION) + #else + : mSource(0x824A) + #endif + , mId(id) + , mMessage(message) { - osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallback"); - osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControl"); } - else if (osg::isGLExtensionSupported(contextID, "GL_ARB_debug_output")) + + DebugGroup::DebugGroup(const DebugGroup & debugGroup, const osg::CopyOp & copyop) + : osg::StateAttribute(debugGroup, copyop) + , mSource(debugGroup.mSource) + , mId(debugGroup.mId) + , mMessage(debugGroup.mMessage) { - osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackARB"); - osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlARB"); } - else if (osg::isGLExtensionSupported(contextID, "GL_AMD_debug_output")) + + void DebugGroup::apply(osg::State & state) const { - osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackAMD"); - osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlAMD"); + if (!PushDebugGroup::sInstance->valid()) + { + Log(Error) << "OpenGL debug groups not supported on this system, or OPENMW_DEBUG_OPENGL environment variable not set."; + return; + } + + auto& attributeVec = state.getAttributeVec(this); + auto& lastAppliedStack = sLastAppliedStack[state.getContextID()]; + + size_t firstNonMatch = 0; + while (firstNonMatch < lastAppliedStack.size() + && ((firstNonMatch < attributeVec.size() && lastAppliedStack[firstNonMatch] == attributeVec[firstNonMatch].first) + || lastAppliedStack[firstNonMatch] == this)) + firstNonMatch++; + + for (size_t i = lastAppliedStack.size(); i > firstNonMatch; --i) + lastAppliedStack[i - 1]->pop(state); + lastAppliedStack.resize(firstNonMatch); + + lastAppliedStack.reserve(attributeVec.size()); + for (size_t i = firstNonMatch; i < attributeVec.size(); ++i) + { + const DebugGroup* group = static_cast(attributeVec[i].first); + group->push(state); + lastAppliedStack.push_back(group); + } + if (!(lastAppliedStack.back() == this)) + { + push(state); + lastAppliedStack.push_back(this); + } } - if (glDebugMessageCallback && glDebugMessageControl) + int DebugGroup::compare(const StateAttribute & sa) const { - glEnable(GL_DEBUG_OUTPUT); - glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_MEDIUM, 0, nullptr, true); - glDebugMessageCallback(debugCallback, nullptr); + COMPARE_StateAttribute_Types(DebugGroup, sa); + + COMPARE_StateAttribute_Parameter(mSource); + COMPARE_StateAttribute_Parameter(mId); + COMPARE_StateAttribute_Parameter(mMessage); - Log(Debug::Info) << "OpenGL debug callback attached."; + return 0; } - else -#endif - Log(Debug::Error) << "Unable to attach OpenGL debug callback."; -} -Debug::EnableGLDebugOperation::EnableGLDebugOperation() : osg::GraphicsOperation("EnableGLDebugOperation", false) -{ -} + void DebugGroup::releaseGLObjects(osg::State * state) const + { + if (state) + sLastAppliedStack.erase(state->getContextID()); + else + sLastAppliedStack.clear(); + } -void Debug::EnableGLDebugOperation::operator()(osg::GraphicsContext* graphicsContext) -{ - OpenThreads::ScopedLock lock(mMutex); + bool DebugGroup::isValid() const + { + return mSource || mId || mMessage.length(); + } - unsigned int contextID = graphicsContext->getState()->getContextID(); - enableGLDebugExtension(contextID); -} + void DebugGroup::push(osg::State & state) const + { + if (isValid()) + PushDebugGroup::sInstance->glPushDebugGroup(mSource, mId, mMessage.size(), mMessage.c_str()); + } + + void DebugGroup::pop(osg::State & state) const + { + if (isValid()) + PushDebugGroup::sInstance->glPopDebugGroup(); + } + + std::map> DebugGroup::sLastAppliedStack{}; -bool Debug::shouldDebugOpenGL() -{ - const char* env = std::getenv("OPENMW_DEBUG_OPENGL"); - if (!env) - return false; - std::string str(env); - if (str.length() == 0) - return true; - - return str.find("OFF") == std::string::npos && str.find("0") == std::string::npos && str.find("NO") == std::string::npos; } diff --git a/components/debug/gldebug.hpp b/components/debug/gldebug.hpp index 8be747afe8..b6f32c9cff 100644 --- a/components/debug/gldebug.hpp +++ b/components/debug/gldebug.hpp @@ -17,5 +17,65 @@ namespace Debug }; bool shouldDebugOpenGL(); + + + /* + Debug groups allow rendering to be annotated, making debugging via APITrace/CodeXL/NSight etc. much clearer. + + Because I've not thought of a quick and clean way of doing it without incurring a small performance cost, + there are no uses of this class checked in. For now, add annotations locally when you need them. + + To use this class, add it to a StateSet just like any other StateAttribute. Prefer the string-only constructor. + You'll need OPENMW_DEBUG_OPENGL set to true, or shouldDebugOpenGL() redefined to just return true as otherwise + the extension function pointers won't get set up. That can maybe be cleaned up in the future. + + Beware that consecutive identical debug groups (i.e. pointers match) won't always get applied due to OSG thinking + it's already applied them. Either avoid nesting the same object, add dummy groups so they're not consecutive, or + ensure the leaf group isn't identical to its parent. + */ + class DebugGroup : public osg::StateAttribute + { + public: + DebugGroup() + : mSource(0) + , mId(0) + , mMessage("") + {} + + DebugGroup(GLenum source, GLuint id, const std::string& message) + : mSource(source) + , mId(id) + , mMessage(message) + {} + + DebugGroup(const std::string& message, GLuint id = 0); + + DebugGroup(const DebugGroup& debugGroup, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY); + + META_StateAttribute(Debug, DebugGroup, osg::StateAttribute::Type(101)); + + void apply(osg::State& state) const override; + + int compare(const StateAttribute& sa) const override; + + void releaseGLObjects(osg::State* state = nullptr) const override; + + virtual bool isValid() const; + + protected: + virtual ~DebugGroup() = default; + + virtual void push(osg::State& state) const; + + virtual void pop(osg::State& state) const; + + GLenum mSource; + GLuint mId; + std::string mMessage; + + static std::map> sLastAppliedStack; + + friend EnableGLDebugOperation; + }; } #endif diff --git a/components/detournavigator/agentbounds.hpp b/components/detournavigator/agentbounds.hpp new file mode 100644 index 0000000000..b45a2fa6cc --- /dev/null +++ b/components/detournavigator/agentbounds.hpp @@ -0,0 +1,34 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_AGENTBOUNDS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_AGENTBOUNDS_H + +#include "collisionshapetype.hpp" + +#include + +#include + +namespace DetourNavigator +{ + struct AgentBounds + { + CollisionShapeType mShapeType; + osg::Vec3f mHalfExtents; + }; + + inline auto tie(const AgentBounds& value) + { + return std::tie(value.mShapeType, value.mHalfExtents); + } + + inline bool operator==(const AgentBounds& lhs, const AgentBounds& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline bool operator<(const AgentBounds& lhs, const AgentBounds& rhs) + { + return tie(lhs) < tie(rhs); + } +} + +#endif diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 1ac928f077..3cc19d12ba 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -2,59 +2,151 @@ #include "debug.hpp" #include "makenavmesh.hpp" #include "settings.hpp" +#include "version.hpp" +#include "serialization.hpp" +#include "navmeshdbutils.hpp" +#include "dbrefgeometryobject.hpp" #include +#include +#include + +#include + +#include #include +#include +#include #include +#include +#include -namespace +namespace DetourNavigator { - using DetourNavigator::ChangeType; - using DetourNavigator::TilePosition; - - int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs) + namespace { - return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y()); + int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs) + { + return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y()); + } + + int getMinDistanceTo(const TilePosition& position, int maxDistance, + const std::set>& pushedTiles, + const std::set>& presentTiles) + { + int result = maxDistance; + for (const auto& [agentBounds, tile] : pushedTiles) + if (presentTiles.find(std::tie(agentBounds, tile)) == presentTiles.end()) + result = std::min(result, getManhattanDistance(position, tile)); + return result; + } + + auto getPriority(const Job& job) noexcept + { + return std::make_tuple(-static_cast>(job.mState), job.mProcessTime, + job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin); + } + + struct LessByJobPriority + { + bool operator()(JobIt lhs, JobIt rhs) const noexcept + { + return getPriority(*lhs) < getPriority(*rhs); + } + }; + + void insertPrioritizedJob(JobIt job, std::deque& queue) + { + const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {}); + queue.insert(it, job); + } + + auto getDbPriority(const Job& job) noexcept + { + return std::make_tuple(static_cast>(job.mState), + job.mChangeType, job.mDistanceToPlayer, job.mDistanceToOrigin); + } + + struct LessByJobDbPriority + { + bool operator()(JobIt lhs, JobIt rhs) const noexcept + { + return getDbPriority(*lhs) < getDbPriority(*rhs); + } + }; + + void insertPrioritizedDbJob(JobIt job, std::deque& queue) + { + const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobDbPriority {}); + queue.insert(it, job); + } + + auto getAgentAndTile(const Job& job) noexcept + { + return std::make_tuple(job.mAgentBounds, job.mChangedTile); + } + + std::unique_ptr makeDbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, const Settings& settings) + { + if (db == nullptr) + return nullptr; + return std::make_unique(updater, std::move(db), TileVersion(navMeshFormatVersion), + settings.mRecast, settings.mWriteToNavMeshDb); + } + + void updateJobs(std::deque& jobs, TilePosition playerTile, int maxTiles) + { + for (JobIt job : jobs) + { + job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile); + if (!shouldAddTile(job->mChangedTile, playerTile, maxTiles)) + job->mChangeType = ChangeType::remove; + } + } + + std::size_t getNextJobId() + { + static std::atomic_size_t nextJobId {1}; + return nextJobId.fetch_add(1); + } } -} -namespace DetourNavigator -{ - static std::ostream& operator <<(std::ostream& stream, UpdateNavMeshStatus value) + std::ostream& operator<<(std::ostream& stream, JobStatus value) { switch (value) { - case UpdateNavMeshStatus::ignored: - return stream << "ignore"; - case UpdateNavMeshStatus::removed: - return stream << "removed"; - case UpdateNavMeshStatus::added: - return stream << "add"; - case UpdateNavMeshStatus::replaced: - return stream << "replaced"; - case UpdateNavMeshStatus::failed: - return stream << "failed"; - case UpdateNavMeshStatus::lost: - return stream << "lost"; - case UpdateNavMeshStatus::cached: - return stream << "cached"; - case UpdateNavMeshStatus::unchanged: - return stream << "unchanged"; - case UpdateNavMeshStatus::restored: - return stream << "restored"; - } - return stream << "unknown(" << static_cast(value) << ")"; + case JobStatus::Done: return stream << "JobStatus::Done"; + case JobStatus::Fail: return stream << "JobStatus::Fail"; + case JobStatus::MemoryCacheMiss: return stream << "JobStatus::MemoryCacheMiss"; + } + return stream << "JobStatus::" << static_cast>(value); + } + + Job::Job(const AgentBounds& agentBounds, std::weak_ptr navMeshCacheItem, + std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, + std::chrono::steady_clock::time_point processTime) + : mId(getNextJobId()) + , mAgentBounds(agentBounds) + , mNavMeshCacheItem(std::move(navMeshCacheItem)) + , mWorldspace(worldspace) + , mChangedTile(changedTile) + , mProcessTime(processTime) + , mChangeType(changeType) + , mDistanceToPlayer(distanceToPlayer) + , mDistanceToOrigin(getManhattanDistance(changedTile, TilePosition {0, 0})) + { } AsyncNavMeshUpdater::AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager, - OffMeshConnectionsManager& offMeshConnectionsManager) + OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr&& db) : mSettings(settings) , mRecastMeshManager(recastMeshManager) , mOffMeshConnectionsManager(offMeshConnectionsManager) , mShouldStop() , mNavMeshTilesCache(settings.mMaxNavMeshTilesCacheSize) + , mDbWorker(makeDbWorker(*this, std::move(db), mSettings)) { for (std::size_t i = 0; i < mSettings.get().mAsyncNavMeshUpdaterThreads; ++i) mThreads.emplace_back([&] { process(); }); @@ -62,89 +154,221 @@ namespace DetourNavigator AsyncNavMeshUpdater::~AsyncNavMeshUpdater() { - mShouldStop = true; - std::unique_lock lock(mMutex); - mJobs = decltype(mJobs)(); - mHasJob.notify_all(); - lock.unlock(); - for (auto& thread : mThreads) - thread.join(); + stop(); } - void AsyncNavMeshUpdater::post(const osg::Vec3f& agentHalfExtents, - const SharedNavMeshCacheItem& navMeshCacheItem, const TilePosition& playerTile, + void AsyncNavMeshUpdater::post(const AgentBounds& agentBounds, const SharedNavMeshCacheItem& navMeshCacheItem, + const TilePosition& playerTile, std::string_view worldspace, const std::map& changedTiles) { - *mPlayerTile.lock() = playerTile; + bool playerTileChanged = false; + { + auto locked = mPlayerTile.lock(); + playerTileChanged = *locked != playerTile; + *locked = playerTile; + } - if (changedTiles.empty()) + if (!playerTileChanged && changedTiles.empty()) return; - const std::lock_guard lock(mMutex); + const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + const int maxTiles = std::min(mSettings.get().mMaxTilesNumber, params.maxTiles); + + std::unique_lock lock(mMutex); - for (const auto& changedTile : changedTiles) + if (playerTileChanged) + updateJobs(mWaiting, playerTile, maxTiles); + + for (const auto& [changedTile, changeType] : changedTiles) { - if (mPushed[agentHalfExtents].insert(changedTile.first).second) + if (mPushed.emplace(agentBounds, changedTile).second) { - Job job; - - job.mAgentHalfExtents = agentHalfExtents; - job.mNavMeshCacheItem = navMeshCacheItem; - job.mChangedTile = changedTile.first; - job.mTryNumber = 0; - job.mChangeType = changedTile.second; - job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile); - job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0}); - job.mProcessTime = job.mChangeType == ChangeType::update - ? mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] + mSettings.get().mMinUpdateInterval + const auto processTime = changeType == ChangeType::update + ? mLastUpdates[std::tie(agentBounds, changedTile)] + mSettings.get().mMinUpdateInterval : std::chrono::steady_clock::time_point(); - mJobs.push(std::move(job)); + const JobIt it = mJobs.emplace(mJobs.end(), agentBounds, navMeshCacheItem, worldspace, + changedTile, changeType, getManhattanDistance(changedTile, playerTile), processTime); + + Log(Debug::Debug) << "Post job " << it->mId << " for agent=(" << it->mAgentBounds << ")" + << " changedTile=(" << it->mChangedTile << ")"; + + if (playerTileChanged) + mWaiting.push_back(it); + else + insertPrioritizedJob(it, mWaiting); } } + if (playerTileChanged) + std::sort(mWaiting.begin(), mWaiting.end(), LessByJobPriority {}); + Log(Debug::Debug) << "Posted " << mJobs.size() << " navigator jobs"; - if (!mJobs.empty()) + if (!mWaiting.empty()) mHasJob.notify_all(); + + lock.unlock(); + + if (playerTileChanged && mDbWorker != nullptr) + mDbWorker->updateJobs(playerTile, maxTiles); } - void AsyncNavMeshUpdater::wait() + void AsyncNavMeshUpdater::wait(Loading::Listener& listener, WaitConditionType waitConditionType) + { + if (mSettings.get().mWaitUntilMinDistanceToPlayer == 0) + return; + listener.setLabel("#{Navigation:BuildingNavigationMesh}"); + const std::size_t initialJobsLeft = getTotalJobs(); + std::size_t maxProgress = initialJobsLeft + mThreads.size(); + listener.setProgressRange(maxProgress); + switch (waitConditionType) + { + case WaitConditionType::requiredTilesPresent: + { + const int minDistanceToPlayer = waitUntilJobsDoneForNotPresentTiles(initialJobsLeft, maxProgress, listener); + if (minDistanceToPlayer < mSettings.get().mWaitUntilMinDistanceToPlayer) + { + mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); }); + listener.setProgress(maxProgress); + } + break; + } + case WaitConditionType::allJobsDone: + waitUntilAllJobsDone(); + listener.setProgress(maxProgress); + break; + } + } + + void AsyncNavMeshUpdater::stop() + { + mShouldStop = true; + if (mDbWorker != nullptr) + mDbWorker->stop(); + std::unique_lock lock(mMutex); + mWaiting.clear(); + mHasJob.notify_all(); + lock.unlock(); + for (auto& thread : mThreads) + if (thread.joinable()) + thread.join(); + } + + int AsyncNavMeshUpdater::waitUntilJobsDoneForNotPresentTiles(const std::size_t initialJobsLeft, std::size_t& maxProgress, Loading::Listener& listener) + { + std::size_t prevJobsLeft = initialJobsLeft; + std::size_t jobsDone = 0; + std::size_t jobsLeft = 0; + const int maxDistanceToPlayer = mSettings.get().mWaitUntilMinDistanceToPlayer; + const TilePosition playerPosition = *mPlayerTile.lockConst(); + int minDistanceToPlayer = 0; + const auto isDone = [&] + { + jobsLeft = mJobs.size(); + if (jobsLeft == 0) + { + minDistanceToPlayer = 0; + return true; + } + minDistanceToPlayer = getMinDistanceTo(playerPosition, maxDistanceToPlayer, mPushed, mPresentTiles); + return minDistanceToPlayer >= maxDistanceToPlayer; + }; + std::unique_lock lock(mMutex); + while (!mDone.wait_for(lock, std::chrono::milliseconds(250), isDone)) + { + if (maxProgress < jobsLeft) + { + maxProgress = jobsLeft + mThreads.size(); + listener.setProgressRange(maxProgress); + listener.setProgress(jobsDone); + } + else if (jobsLeft < prevJobsLeft) + { + const std::size_t newJobsDone = prevJobsLeft - jobsLeft; + jobsDone += newJobsDone; + prevJobsLeft = jobsLeft; + listener.increaseProgress(newJobsDone); + } + } + return minDistanceToPlayer; + } + + void AsyncNavMeshUpdater::waitUntilAllJobsDone() { { std::unique_lock lock(mMutex); - mDone.wait(lock, [&] { return mJobs.empty() && getTotalThreadJobsUnsafe() == 0; }); + mDone.wait(lock, [this] { return mJobs.size() == 0; }); } mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); }); } - void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const + AsyncNavMeshUpdater::Stats AsyncNavMeshUpdater::getStats() const { - std::size_t jobs = 0; - + Stats result; { const std::lock_guard lock(mMutex); - jobs = mJobs.size() + getTotalThreadJobsUnsafe(); + result.mJobs = mJobs.size(); + result.mWaiting = mWaiting.size(); + result.mPushed = mPushed.size(); } + result.mProcessing = mProcessingTiles.lockConst()->size(); + if (mDbWorker != nullptr) + result.mDb = mDbWorker->getStats(); + result.mCache = mNavMeshTilesCache.getStats(); + result.mDbGetTileHits = mDbGetTileHits.load(std::memory_order_relaxed); + return result; + } + + void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out) + { + out.setAttribute(frameNumber, "NavMesh Jobs", static_cast(stats.mJobs)); + out.setAttribute(frameNumber, "NavMesh Waiting", static_cast(stats.mWaiting)); + out.setAttribute(frameNumber, "NavMesh Pushed", static_cast(stats.mPushed)); + out.setAttribute(frameNumber, "NavMesh Processing", static_cast(stats.mProcessing)); - stats.setAttribute(frameNumber, "NavMesh UpdateJobs", jobs); + if (stats.mDb.has_value()) + { + out.setAttribute(frameNumber, "NavMesh DbJobs", static_cast(stats.mDb->mJobs)); + + if (stats.mDb->mGetTileCount > 0) + out.setAttribute(frameNumber, "NavMesh DbCacheHitRate", static_cast(stats.mDbGetTileHits) + / static_cast(stats.mDb->mGetTileCount) * 100.0); + } - mNavMeshTilesCache.reportStats(frameNumber, stats); + reportStats(stats.mCache, frameNumber, out); } void AsyncNavMeshUpdater::process() noexcept { Log(Debug::Debug) << "Start process navigator jobs by thread=" << std::this_thread::get_id(); + Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { try { - if (auto job = getNextJob()) + if (JobIt job = getNextJob(); job != mJobs.end()) { - const auto processed = processJob(*job); - unlockTile(job->mAgentHalfExtents, job->mChangedTile); - if (!processed) - repost(std::move(*job)); + const JobStatus status = processJob(*job); + Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status; + switch (status) + { + case JobStatus::Done: + unlockTile(job->mAgentBounds, job->mChangedTile); + if (job->mGeneratedNavMeshData != nullptr) + mDbWorker->enqueueJob(job); + else + removeJob(job); + break; + case JobStatus::Fail: + repost(job); + break; + case JobStatus::MemoryCacheMiss: + { + mDbWorker->enqueueJob(job); + break; + } + } } else cleanupLastUpdates(); @@ -157,107 +381,216 @@ namespace DetourNavigator Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id(); } - bool AsyncNavMeshUpdater::processJob(const Job& job) + JobStatus AsyncNavMeshUpdater::processJob(Job& job) { - Log(Debug::Debug) << "Process job for agent=(" << std::fixed << std::setprecision(2) << job.mAgentHalfExtents << ")" - " by thread=" << std::this_thread::get_id(); - - const auto start = std::chrono::steady_clock::now(); - - const auto firstStart = setFirstStart(start); + Log(Debug::Debug) << "Processing job " << job.mId << " by thread=" << std::this_thread::get_id(); const auto navMeshCacheItem = job.mNavMeshCacheItem.lock(); if (!navMeshCacheItem) - return true; + return JobStatus::Done; - const auto recastMesh = mRecastMeshManager.get().getMesh(job.mChangedTile); const auto playerTile = *mPlayerTile.lockConst(); - const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); + const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile, - offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache); + if (!shouldAddTile(job.mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) + { + Log(Debug::Debug) << "Ignore add tile by job " << job.mId << ": too far from player"; + navMeshCacheItem->lock()->removeTile(job.mChangedTile); + return JobStatus::Done; + } - const auto finish = std::chrono::steady_clock::now(); + switch (job.mState) + { + case JobState::Initial: + return processInitialJob(job, *navMeshCacheItem); + case JobState::WithDbResult: + return processJobWithDbResult(job, *navMeshCacheItem); + } - writeDebugFiles(job, recastMesh.get()); + return JobStatus::Done; + } - using FloatMs = std::chrono::duration; + JobStatus AsyncNavMeshUpdater::processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem) + { + Log(Debug::Debug) << "Processing initial job " << job.mId; - const auto locked = navMeshCacheItem->lockConst(); - Log(Debug::Debug) << std::fixed << std::setprecision(2) << - "Cache updated for agent=(" << job.mAgentHalfExtents << ")" << - " tile=" << job.mChangedTile << - " status=" << status << - " generation=" << locked->getGeneration() << - " revision=" << locked->getNavMeshRevision() << - " time=" << std::chrono::duration_cast(finish - start).count() << "ms" << - " total_time=" << std::chrono::duration_cast(finish - firstStart).count() << "ms" - " thread=" << std::this_thread::get_id(); + std::shared_ptr recastMesh = mRecastMeshManager.get().getMesh(job.mWorldspace, job.mChangedTile); - return isSuccess(status); - } + if (recastMesh == nullptr) + { + Log(Debug::Debug) << "Null recast mesh for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } - std::optional AsyncNavMeshUpdater::getNextJob() - { - std::unique_lock lock(mMutex); + if (isEmpty(*recastMesh)) + { + Log(Debug::Debug) << "Empty bounds for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } - const auto threadId = std::this_thread::get_id(); - auto& threadQueue = mThreadsQueues[threadId]; + NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh); + std::unique_ptr preparedNavMeshData; + const PreparedNavMeshData* preparedNavMeshDataPtr = nullptr; - while (true) + if (cachedNavMeshData) + { + preparedNavMeshDataPtr = &cachedNavMeshData.get(); + } + else { - const auto hasJob = [&] { - return (!mJobs.empty() && mJobs.top().mProcessTime <= std::chrono::steady_clock::now()) - || !threadQueue.mJobs.empty(); - }; + if (job.mChangeType != ChangeType::update && mDbWorker != nullptr) + { + job.mRecastMesh = std::move(recastMesh); + return JobStatus::MemoryCacheMiss; + } + + preparedNavMeshData = prepareNavMeshTileData(*recastMesh, job.mChangedTile, job.mAgentBounds, mSettings.get().mRecast); + + if (preparedNavMeshData == nullptr) + { + Log(Debug::Debug) << "Null navmesh data for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } - if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) + if (job.mChangeType == ChangeType::update) { - mFirstStart.lock()->reset(); - if (mJobs.empty() && getTotalThreadJobsUnsafe() == 0) - mDone.notify_all(); - return std::nullopt; + preparedNavMeshDataPtr = preparedNavMeshData.get(); } + else + { + cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentBounds, job.mChangedTile, + *recastMesh, std::move(preparedNavMeshData)); + preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get(); + } + } + + const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); + + const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData), + makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentBounds, job.mChangedTile, mSettings.get().mRecast)); + + return handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *recastMesh); + } + + JobStatus AsyncNavMeshUpdater::processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem) + { + Log(Debug::Debug) << "Processing job with db result " << job.mId; + + std::unique_ptr preparedNavMeshData; + bool generatedNavMeshData = false; - Log(Debug::Debug) << "Got " << mJobs.size() << " navigator jobs and " - << threadQueue.mJobs.size() << " thread jobs by thread=" << std::this_thread::get_id(); + if (job.mCachedTileData.has_value() && job.mCachedTileData->mVersion == navMeshFormatVersion) + { + preparedNavMeshData = std::make_unique(); + if (deserialize(job.mCachedTileData->mData, *preparedNavMeshData)) + ++mDbGetTileHits; + else + preparedNavMeshData = nullptr; + } + + if (preparedNavMeshData == nullptr) + { + preparedNavMeshData = prepareNavMeshTileData(*job.mRecastMesh, job.mChangedTile, job.mAgentBounds, mSettings.get().mRecast); + generatedNavMeshData = true; + } + + if (preparedNavMeshData == nullptr) + { + Log(Debug::Debug) << "Null navmesh data for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } - auto job = threadQueue.mJobs.empty() - ? getJob(mJobs, mPushed, true) - : getJob(threadQueue.mJobs, threadQueue.mPushed, false); + auto cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentBounds, job.mChangedTile, *job.mRecastMesh, + std::move(preparedNavMeshData)); - if (!job) - continue; + const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); + + const PreparedNavMeshData* preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get(); + assert (preparedNavMeshDataPtr != nullptr); + + const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData), + makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentBounds, job.mChangedTile, mSettings.get().mRecast)); - const auto owner = lockTile(job->mAgentHalfExtents, job->mChangedTile); + const JobStatus result = handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *job.mRecastMesh); - if (owner == threadId) - return job; + if (result == JobStatus::Done && job.mChangeType != ChangeType::update + && mDbWorker != nullptr && mSettings.get().mWriteToNavMeshDb && generatedNavMeshData) + job.mGeneratedNavMeshData = std::make_unique(*preparedNavMeshDataPtr); + + return result; + } - postThreadJob(std::move(*job), mThreadsQueues[owner]); + JobStatus AsyncNavMeshUpdater::handleUpdateNavMeshStatus(UpdateNavMeshStatus status, + const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh) + { + const Version navMeshVersion = navMeshCacheItem.lockConst()->getVersion(); + mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, + Version {recastMesh.getGeneration(), recastMesh.getRevision()}, + navMeshVersion); + + if (status == UpdateNavMeshStatus::removed || status == UpdateNavMeshStatus::lost) + { + const std::scoped_lock lock(mMutex); + mPresentTiles.erase(std::make_tuple(job.mAgentBounds, job.mChangedTile)); + } + else if (isSuccess(status) && status != UpdateNavMeshStatus::ignored) + { + const std::scoped_lock lock(mMutex); + mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile)); } + + writeDebugFiles(job, &recastMesh); + + return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } - std::optional AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate) + JobIt AsyncNavMeshUpdater::getNextJob() { - const auto now = std::chrono::steady_clock::now(); + std::unique_lock lock(mMutex); + + bool shouldStop = false; + const auto hasJob = [&] + { + shouldStop = mShouldStop; + return shouldStop + || (!mWaiting.empty() && mWaiting.front()->mProcessTime <= std::chrono::steady_clock::now()); + }; + + if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) + { + if (mJobs.empty()) + mDone.notify_all(); + return mJobs.end(); + } + + if (shouldStop) + return mJobs.end(); + + const JobIt job = mWaiting.front(); - if (jobs.top().mProcessTime > now) - return {}; + mWaiting.pop_front(); - Job job = std::move(jobs.top()); - jobs.pop(); + if (job->mRecastMesh != nullptr) + return job; - if (changeLastUpdate && job.mChangeType == ChangeType::update) - mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] = now; + if (!lockTile(job->mAgentBounds, job->mChangedTile)) + { + Log(Debug::Debug) << "Failed to lock tile by " << job->mId; + ++job->mTryNumber; + insertPrioritizedJob(job, mWaiting); + return mJobs.end(); + } - const auto it = pushed.find(job.mAgentHalfExtents); - it->second.erase(job.mChangedTile); - if (it->second.empty()) - pushed.erase(it); + if (job->mChangeType == ChangeType::update) + mLastUpdates[getAgentAndTile(*job)] = std::chrono::steady_clock::now(); + mPushed.erase(getAgentAndTile(*job)); - return {std::move(job)}; + return job; } void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const @@ -277,120 +610,282 @@ namespace DetourNavigator } if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile) writeToFile(*recastMesh, mSettings.get().mRecastMeshPathPrefix + std::to_string(job.mChangedTile.x()) - + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision); + + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision, mSettings.get().mRecast); if (mSettings.get().mEnableWriteNavMeshToFile) if (const auto shared = job.mNavMeshCacheItem.lock()) writeToFile(shared->lockConst()->getImpl(), mSettings.get().mNavMeshPathPrefix, navMeshRevision); } - std::chrono::steady_clock::time_point AsyncNavMeshUpdater::setFirstStart(const std::chrono::steady_clock::time_point& value) + void AsyncNavMeshUpdater::repost(JobIt job) { - const auto locked = mFirstStart.lock(); - if (!*locked) - *locked = value; - return *locked.get(); - } + unlockTile(job->mAgentBounds, job->mChangedTile); - void AsyncNavMeshUpdater::repost(Job&& job) - { - if (mShouldStop || job.mTryNumber > 2) + if (mShouldStop || job->mTryNumber > 2) return; const std::lock_guard lock(mMutex); - if (mPushed[job.mAgentHalfExtents].insert(job.mChangedTile).second) + if (mPushed.emplace(job->mAgentBounds, job->mChangedTile).second) { - ++job.mTryNumber; - mJobs.push(std::move(job)); + ++job->mTryNumber; + insertPrioritizedJob(job, mWaiting); mHasJob.notify_all(); + return; } + + mJobs.erase(job); } - void AsyncNavMeshUpdater::postThreadJob(Job&& job, Queue& queue) + bool AsyncNavMeshUpdater::lockTile(const AgentBounds& agentBounds, const TilePosition& changedTile) { - if (queue.mPushed[job.mAgentHalfExtents].insert(job.mChangedTile).second) - { - queue.mJobs.push(std::move(job)); - mHasJob.notify_all(); - } + Log(Debug::Debug) << "Locking tile agent=" << agentBounds << " changedTile=(" << changedTile << ")"; + return mProcessingTiles.lock()->emplace(agentBounds, changedTile).second; } - std::thread::id AsyncNavMeshUpdater::lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) + void AsyncNavMeshUpdater::unlockTile(const AgentBounds& agentBounds, const TilePosition& changedTile) { - if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) - return std::this_thread::get_id(); - auto locked = mProcessingTiles.lock(); + locked->erase(std::tie(agentBounds, changedTile)); + Log(Debug::Debug) << "Unlocked tile agent=" << agentBounds << " changedTile=(" << changedTile << ")"; + if (locked->empty()) + mProcessed.notify_all(); + } - auto agent = locked->find(agentHalfExtents); - if (agent == locked->end()) - { - const auto threadId = std::this_thread::get_id(); - locked->emplace(agentHalfExtents, std::map({{changedTile, threadId}})); - return threadId; - } + std::size_t AsyncNavMeshUpdater::getTotalJobs() const + { + const std::scoped_lock lock(mMutex); + return mJobs.size(); + } - auto tile = agent->second.find(changedTile); - if (tile == agent->second.end()) + void AsyncNavMeshUpdater::cleanupLastUpdates() + { + const auto now = std::chrono::steady_clock::now(); + + const std::lock_guard lock(mMutex); + + for (auto it = mLastUpdates.begin(); it != mLastUpdates.end();) { - const auto threadId = std::this_thread::get_id(); - agent->second.emplace(changedTile, threadId); - return threadId; + if (now - it->second > mSettings.get().mMinUpdateInterval) + it = mLastUpdates.erase(it); + else + ++it; } + } - return tile->second; + void AsyncNavMeshUpdater::enqueueJob(JobIt job) + { + Log(Debug::Debug) << "Enqueueing job " << job->mId << " by thread=" << std::this_thread::get_id(); + const std::lock_guard lock(mMutex); + insertPrioritizedJob(job, mWaiting); + mHasJob.notify_all(); } - void AsyncNavMeshUpdater::unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) + void AsyncNavMeshUpdater::removeJob(JobIt job) { - if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) - return; + Log(Debug::Debug) << "Removing job " << job->mId << " by thread=" << std::this_thread::get_id(); + const std::lock_guard lock(mMutex); + mJobs.erase(job); + } - auto locked = mProcessingTiles.lock(); + void DbJobQueue::push(JobIt job) + { + const std::lock_guard lock(mMutex); + insertPrioritizedDbJob(job, mJobs); + mHasJob.notify_all(); + } - auto agent = locked->find(agentHalfExtents); - if (agent == locked->end()) - return; + std::optional DbJobQueue::pop() + { + std::unique_lock lock(mMutex); + mHasJob.wait(lock, [&] { return mShouldStop || !mJobs.empty(); }); + if (mJobs.empty()) + return std::nullopt; + const JobIt job = mJobs.front(); + mJobs.pop_front(); + return job; + } - auto tile = agent->second.find(changedTile); - if (tile == agent->second.end()) - return; + void DbJobQueue::update(TilePosition playerTile, int maxTiles) + { + const std::lock_guard lock(mMutex); + updateJobs(mJobs, playerTile, maxTiles); + std::sort(mJobs.begin(), mJobs.end(), LessByJobDbPriority {}); + } - agent->second.erase(tile); + void DbJobQueue::stop() + { + const std::lock_guard lock(mMutex); + mJobs.clear(); + mShouldStop = true; + mHasJob.notify_all(); + } - if (agent->second.empty()) - locked->erase(agent); + std::size_t DbJobQueue::size() const + { + const std::lock_guard lock(mMutex); + return mJobs.size(); + } - if (locked->empty()) - mProcessed.notify_all(); + DbWorker::DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, + TileVersion version, const RecastSettings& recastSettings, bool writeToDb) + : mUpdater(updater) + , mRecastSettings(recastSettings) + , mDb(std::move(db)) + , mVersion(version) + , mWriteToDb(writeToDb) + , mNextTileId(mDb->getMaxTileId() + 1) + , mNextShapeId(mDb->getMaxShapeId() + 1) + , mThread([this] { run(); }) + { } - std::size_t AsyncNavMeshUpdater::getTotalThreadJobsUnsafe() const + DbWorker::~DbWorker() { - return std::accumulate(mThreadsQueues.begin(), mThreadsQueues.end(), std::size_t(0), - [] (auto r, const auto& v) { return r + v.second.mJobs.size(); }); + stop(); } - void AsyncNavMeshUpdater::cleanupLastUpdates() + void DbWorker::enqueueJob(JobIt job) { - const auto now = std::chrono::steady_clock::now(); + Log(Debug::Debug) << "Enqueueing db job " << job->mId << " by thread=" << std::this_thread::get_id(); + mQueue.push(job); + } - const std::lock_guard lock(mMutex); + DbWorker::Stats DbWorker::getStats() const + { + Stats result; + result.mJobs = mQueue.size(); + result.mGetTileCount = mGetTileCount.load(std::memory_order_relaxed); + return result; + } - for (auto agent = mLastUpdates.begin(); agent != mLastUpdates.end();) + void DbWorker::stop() + { + mShouldStop = true; + mQueue.stop(); + if (mThread.joinable()) + mThread.join(); + } + + void DbWorker::run() noexcept + { + while (!mShouldStop) { - for (auto tile = agent->second.begin(); tile != agent->second.end();) + try { - if (now - tile->second > mSettings.get().mMinUpdateInterval) - tile = agent->second.erase(tile); - else - ++tile; + if (const auto job = mQueue.pop()) + processJob(*job); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "DbWorker exception: " << e.what(); + } + } + } + + void DbWorker::processJob(JobIt job) + { + const auto process = [&] (auto f) + { + try + { + f(job); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "DbWorker exception while processing job " << job->mId << ": " << e.what(); + if (mWriteToDb) + { + const std::string_view message(e.what()); + if (message.find("database or disk is full") != std::string_view::npos) + { + mWriteToDb = false; + Log(Debug::Warning) << "Writes to navmeshdb are disabled because file size limit is reached or disk is full"; + } + else if (message.find("database is locked") != std::string_view::npos) + { + mWriteToDb = false; + Log(Debug::Warning) << "Writes to navmeshdb are disabled to avoid concurrent writes from multiple processes"; + } + } } + }; + + if (job->mGeneratedNavMeshData != nullptr) + { + process([&] (JobIt job) { processWritingJob(job); }); + mUpdater.removeJob(job); + return; + } + + process([&] (JobIt job) { processReadingJob(job); }); + job->mState = JobState::WithDbResult; + mUpdater.enqueueJob(job); + } + + void DbWorker::processReadingJob(JobIt job) + { + Log(Debug::Debug) << "Processing db read job " << job->mId; - if (agent->second.empty()) - agent = mLastUpdates.erase(agent); + if (job->mInput.empty()) + { + Log(Debug::Debug) << "Serializing input for job " << job->mId; + if (mWriteToDb) + { + const auto objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); }); + job->mInput = serialize(mRecastSettings, job->mAgentBounds, *job->mRecastMesh, objects); + } else - ++agent; + { + const auto objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v); }); + if (!objects.has_value()) + return; + job->mInput = serialize(mRecastSettings, job->mAgentBounds, *job->mRecastMesh, *objects); + } } + + job->mCachedTileData = mDb->getTileData(job->mWorldspace, job->mChangedTile, job->mInput); + ++mGetTileCount; + } + + void DbWorker::processWritingJob(JobIt job) + { + if (!mWriteToDb) + { + Log(Debug::Debug) << "Ignored db write job " << job->mId; + return; + } + + Log(Debug::Debug) << "Processing db write job " << job->mId; + + if (job->mInput.empty()) + { + Log(Debug::Debug) << "Serializing input for job " << job->mId; + const std::vector objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); }); + job->mInput = serialize(mRecastSettings, job->mAgentBounds, *job->mRecastMesh, objects); + } + + if (const auto& cachedTileData = job->mCachedTileData) + { + Log(Debug::Debug) << "Update db tile by job " << job->mId; + job->mGeneratedNavMeshData->mUserId = cachedTileData->mTileId; + mDb->updateTile(cachedTileData->mTileId, mVersion, serialize(*job->mGeneratedNavMeshData)); + return; + } + + const auto cached = mDb->findTile(job->mWorldspace, job->mChangedTile, job->mInput); + if (cached.has_value() && cached->mVersion == mVersion) + { + Log(Debug::Debug) << "Ignore existing db tile by job " << job->mId; + return; + } + + job->mGeneratedNavMeshData->mUserId = mNextTileId; + Log(Debug::Debug) << "Insert db tile by job " << job->mId; + mDb->insertTile(mNextTileId, job->mWorldspace, job->mChangedTile, + mVersion, job->mInput, serialize(*job->mGeneratedNavMeshData)); + ++mNextTileId; } } diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 53e7fd7c14..abdd235678 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -6,6 +6,10 @@ #include "tilecachedrecastmeshmanager.hpp" #include "tileposition.hpp" #include "navmeshtilescache.hpp" +#include "waitconditiontype.hpp" +#include "navmeshdb.hpp" +#include "changetype.hpp" +#include "agentbounds.hpp" #include @@ -14,85 +18,162 @@ #include #include #include -#include +#include #include #include +#include +#include +#include +#include class dtNavMesh; +namespace Loading +{ + class Listener; +} + namespace DetourNavigator { - enum class ChangeType + enum class JobState { - remove = 0, - mixed = 1, - add = 2, - update = 3, + Initial, + WithDbResult, }; - inline std::ostream& operator <<(std::ostream& stream, ChangeType value) + struct Job { - switch (value) { - case ChangeType::remove: - return stream << "ChangeType::remove"; - case ChangeType::mixed: - return stream << "ChangeType::mixed"; - case ChangeType::add: - return stream << "ChangeType::add"; - case ChangeType::update: - return stream << "ChangeType::update"; - } - return stream << "ChangeType::" << static_cast(value); - } + const std::size_t mId; + const AgentBounds mAgentBounds; + const std::weak_ptr mNavMeshCacheItem; + const std::string mWorldspace; + const TilePosition mChangedTile; + const std::chrono::steady_clock::time_point mProcessTime; + unsigned mTryNumber = 0; + ChangeType mChangeType; + int mDistanceToPlayer; + const int mDistanceToOrigin; + JobState mState = JobState::Initial; + std::vector mInput; + std::shared_ptr mRecastMesh; + std::optional mCachedTileData; + std::unique_ptr mGeneratedNavMeshData; + + Job(const AgentBounds& agentBounds, std::weak_ptr navMeshCacheItem, + std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, + std::chrono::steady_clock::time_point processTime); + }; - class AsyncNavMeshUpdater + using JobIt = std::list::iterator; + + enum class JobStatus + { + Done, + Fail, + MemoryCacheMiss, + }; + + std::ostream& operator<<(std::ostream& stream, JobStatus value); + + class DbJobQueue { public: - AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager, - OffMeshConnectionsManager& offMeshConnectionsManager); - ~AsyncNavMeshUpdater(); + void push(JobIt job); + + std::optional pop(); - void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& mNavMeshCacheItem, - const TilePosition& playerTile, const std::map& changedTiles); + void update(TilePosition playerTile, int maxTiles); - void wait(); + void stop(); - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + std::size_t size() const; private: - struct Job + mutable std::mutex mMutex; + std::condition_variable mHasJob; + std::deque mJobs; + bool mShouldStop = false; + }; + + class AsyncNavMeshUpdater; + + class DbWorker + { + public: + struct Stats { - osg::Vec3f mAgentHalfExtents; - std::weak_ptr mNavMeshCacheItem; - TilePosition mChangedTile; - unsigned mTryNumber; - ChangeType mChangeType; - int mDistanceToPlayer; - int mDistanceToOrigin; - std::chrono::steady_clock::time_point mProcessTime; - - std::tuple getPriority() const - { - return std::make_tuple(mProcessTime, mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin); - } - - friend inline bool operator <(const Job& lhs, const Job& rhs) - { - return lhs.getPriority() > rhs.getPriority(); - } + std::size_t mJobs = 0; + std::size_t mGetTileCount = 0; }; - using Jobs = std::priority_queue>; - using Pushed = std::map>; + DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, + TileVersion version, const RecastSettings& recastSettings, bool writeToDb); - struct Queue - { - Jobs mJobs; - Pushed mPushed; + ~DbWorker(); + + Stats getStats() const; + + void enqueueJob(JobIt job); + + void updateJobs(TilePosition playerTile, int maxTiles) { mQueue.update(playerTile, maxTiles); } + + void stop(); + + private: + AsyncNavMeshUpdater& mUpdater; + const RecastSettings& mRecastSettings; + const std::unique_ptr mDb; + const TileVersion mVersion; + bool mWriteToDb; + TileId mNextTileId; + ShapeId mNextShapeId; + DbJobQueue mQueue; + std::atomic_bool mShouldStop {false}; + std::atomic_size_t mGetTileCount {0}; + std::thread mThread; + + inline void run() noexcept; + + inline void processJob(JobIt job); + + inline void processReadingJob(JobIt job); - Queue() = default; + inline void processWritingJob(JobIt job); + }; + + class AsyncNavMeshUpdater + { + public: + struct Stats + { + std::size_t mJobs = 0; + std::size_t mWaiting = 0; + std::size_t mPushed = 0; + std::size_t mProcessing = 0; + std::size_t mDbGetTileHits = 0; + std::optional mDb; + NavMeshTilesCache::Stats mCache; }; + AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager, + OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr&& db); + ~AsyncNavMeshUpdater(); + + void post(const AgentBounds& agentBounds, const SharedNavMeshCacheItem& navMeshCacheItem, + const TilePosition& playerTile, std::string_view worldspace, + const std::map& changedTiles); + + void wait(Loading::Listener& listener, WaitConditionType waitConditionType); + + void stop(); + + Stats getStats() const; + + void enqueueJob(JobIt job); + + void removeJob(JobIt job); + + private: std::reference_wrapper mSettings; std::reference_wrapper mRecastMeshManager; std::reference_wrapper mOffMeshConnectionsManager; @@ -101,40 +182,51 @@ namespace DetourNavigator std::condition_variable mHasJob; std::condition_variable mDone; std::condition_variable mProcessed; - Jobs mJobs; - std::map> mPushed; + std::list mJobs; + std::deque mWaiting; + std::set> mPushed; Misc::ScopeGuarded mPlayerTile; - Misc::ScopeGuarded> mFirstStart; NavMeshTilesCache mNavMeshTilesCache; - Misc::ScopeGuarded>> mProcessingTiles; - std::map> mLastUpdates; - std::map mThreadsQueues; + Misc::ScopeGuarded>> mProcessingTiles; + std::map, std::chrono::steady_clock::time_point> mLastUpdates; + std::set> mPresentTiles; std::vector mThreads; + std::unique_ptr mDbWorker; + std::atomic_size_t mDbGetTileHits {0}; void process() noexcept; - bool processJob(const Job& job); + JobStatus processJob(Job& job); - std::optional getNextJob(); + inline JobStatus processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem); - std::optional getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate); + inline JobStatus processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem); - void postThreadJob(Job&& job, Queue& queue); + inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job, + const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh); - void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const; + JobIt getNextJob(); + + void postThreadJob(JobIt job, std::deque& queue); - std::chrono::steady_clock::time_point setFirstStart(const std::chrono::steady_clock::time_point& value); + void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const; - void repost(Job&& job); + void repost(JobIt job); - std::thread::id lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); + bool lockTile(const AgentBounds& agentBounds, const TilePosition& changedTile); - void unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); + void unlockTile(const AgentBounds& agentBounds, const TilePosition& changedTile); - inline std::size_t getTotalThreadJobsUnsafe() const; + inline std::size_t getTotalJobs() const; void cleanupLastUpdates(); + + int waitUntilJobsDoneForNotPresentTiles(const std::size_t initialJobsLeft, std::size_t& maxJobsLeft, Loading::Listener& listener); + + void waitUntilAllJobsDone(); }; + + void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out); } #endif diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index 90b4266104..e350c5591f 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -3,17 +3,16 @@ namespace DetourNavigator { - CachedRecastMeshManager::CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, - std::size_t generation) - : mImpl(settings, bounds, generation) + CachedRecastMeshManager::CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation) + : mImpl(bounds, generation) {} - bool CachedRecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, + bool CachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { if (!mImpl.addObject(id, shape, transform, areaType)) return false; - mCached.reset(); + mOutdatedCache = true; return true; } @@ -21,44 +20,87 @@ namespace DetourNavigator { if (!mImpl.updateObject(id, transform, areaType)) return false; - mCached.reset(); + mOutdatedCache = true; return true; } std::optional CachedRecastMeshManager::removeObject(const ObjectId id) { - const auto object = mImpl.removeObject(id); + auto object = mImpl.removeObject(id); if (object) - mCached.reset(); + mOutdatedCache = true; return object; } - bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - if (!mImpl.addWater(cellPosition, cellSize, transform)) + if (!mImpl.addWater(cellPosition, cellSize, level)) return false; - mCached.reset(); + mOutdatedCache = true; return true; } - std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto water = mImpl.removeWater(cellPosition); if (water) - mCached.reset(); + mOutdatedCache = true; return water; } + bool CachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, + const HeightfieldShape& shape) + { + if (!mImpl.addHeightfield(cellPosition, cellSize, shape)) + return false; + mOutdatedCache = true; + return true; + } + + std::optional CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const auto heightfield = mImpl.removeHeightfield(cellPosition); + if (heightfield) + mOutdatedCache = true; + return heightfield; + } + std::shared_ptr CachedRecastMeshManager::getMesh() { - if (!mCached) - mCached = mImpl.getMesh(); - return mCached; + bool outdated = true; + if (!mOutdatedCache.compare_exchange_strong(outdated, false)) + { + std::shared_ptr cached = getCachedMesh(); + if (cached != nullptr) + return cached; + } + std::shared_ptr mesh = mImpl.getMesh(); + *mCached.lock() = mesh; + return mesh; + } + + std::shared_ptr CachedRecastMeshManager::getCachedMesh() const + { + return *mCached.lockConst(); + } + + std::shared_ptr CachedRecastMeshManager::getNewMesh() const + { + return mImpl.getMesh(); } bool CachedRecastMeshManager::isEmpty() const { return mImpl.isEmpty(); } + + void CachedRecastMeshManager::reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion) + { + mImpl.reportNavMeshChange(recastMeshVersion, navMeshVersion); + } + + Version CachedRecastMeshManager::getVersion() const + { + return mImpl.getVersion(); + } } diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index 54574aa99d..b92d39efa4 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -2,32 +2,51 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_CACHEDRECASTMESHMANAGER_H #include "recastmeshmanager.hpp" +#include "version.hpp" +#include "heightfieldshape.hpp" + +#include + +#include namespace DetourNavigator { class CachedRecastMeshManager { public: - CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); + explicit CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + std::optional removeObject(const ObjectId id); - std::optional removeWater(const osg::Vec2i& cellPosition); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); - std::optional removeObject(const ObjectId id); + std::optional removeWater(const osg::Vec2i& cellPosition); + + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); + + std::optional removeHeightfield(const osg::Vec2i& cellPosition); std::shared_ptr getMesh(); + std::shared_ptr getCachedMesh() const; + + std::shared_ptr getNewMesh() const; + bool isEmpty() const; + void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion); + + Version getVersion() const; + private: RecastMeshManager mImpl; - std::shared_ptr mCached; + Misc::ScopeGuarded> mCached; + std::atomic_bool mOutdatedCache {true}; }; } diff --git a/components/detournavigator/changetype.hpp b/components/detournavigator/changetype.hpp new file mode 100644 index 0000000000..15b3056e55 --- /dev/null +++ b/components/detournavigator/changetype.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_CHANGETYPE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_CHANGETYPE_H + +namespace DetourNavigator +{ + enum class ChangeType + { + remove = 0, + mixed = 1, + add = 2, + update = 3, + }; +} + +#endif diff --git a/components/detournavigator/chunkytrimesh.cpp b/components/detournavigator/chunkytrimesh.cpp deleted file mode 100644 index 3a8fc34802..0000000000 --- a/components/detournavigator/chunkytrimesh.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include "chunkytrimesh.hpp" -#include "exceptions.hpp" - -#include - -#include - -namespace DetourNavigator -{ - namespace - { - struct BoundsItem - { - Rect mBounds; - std::ptrdiff_t mOffset; - unsigned char mAreaTypes; - }; - - template - struct LessBoundsItem - { - bool operator ()(const BoundsItem& lhs, const BoundsItem& rhs) const - { - return lhs.mBounds.mMinBound[axis] < rhs.mBounds.mMinBound[axis]; - } - }; - - void calcExtends(const std::vector& items, const std::size_t imin, const std::size_t imax, - Rect& bounds) - { - bounds = items[imin].mBounds; - - std::for_each( - items.begin() + static_cast(imin) + 1, - items.begin() + static_cast(imax), - [&] (const BoundsItem& item) - { - for (int i = 0; i < 2; ++i) - { - bounds.mMinBound[i] = std::min(bounds.mMinBound[i], item.mBounds.mMinBound[i]); - bounds.mMaxBound[i] = std::max(bounds.mMaxBound[i], item.mBounds.mMaxBound[i]); - } - }); - } - - void subdivide(std::vector& items, const std::size_t imin, const std::size_t imax, - const std::size_t trisPerChunk, const std::vector& inIndices, const std::vector& inAreaTypes, - std::size_t& curNode, std::vector& nodes, std::size_t& curTri, - std::vector& outIndices, std::vector& outAreaTypes) - { - const auto inum = imax - imin; - const auto icur = curNode; - - if (curNode > nodes.size()) - return; - - ChunkyTriMeshNode& node = nodes[curNode++]; - - if (inum <= trisPerChunk) - { - // Leaf - calcExtends(items, imin, imax, node.mBounds); - - // Copy triangles. - node.mOffset = static_cast(curTri); - node.mSize = inum; - - for (std::size_t i = imin; i < imax; ++i) - { - std::copy( - inIndices.begin() + items[i].mOffset * 3, - inIndices.begin() + items[i].mOffset * 3 + 3, - outIndices.begin() + static_cast(curTri) * 3 - ); - outAreaTypes[curTri] = inAreaTypes[static_cast(items[i].mOffset)]; - curTri++; - } - } - else - { - // Split - calcExtends(items, imin, imax, node.mBounds); - - if (node.mBounds.mMaxBound.x() - node.mBounds.mMinBound.x() - >= node.mBounds.mMaxBound.y() - node.mBounds.mMinBound.y()) - { - // Sort along x-axis - std::sort( - items.begin() + static_cast(imin), - items.begin() + static_cast(imax), - LessBoundsItem<0> {} - ); - } - else - { - // Sort along y-axis - std::sort( - items.begin() + static_cast(imin), - items.begin() + static_cast(imax), - LessBoundsItem<1> {} - ); - } - - const auto isplit = imin + inum / 2; - - // Left - subdivide(items, imin, isplit, trisPerChunk, inIndices, inAreaTypes, curNode, nodes, curTri, outIndices, outAreaTypes); - // Right - subdivide(items, isplit, imax, trisPerChunk, inIndices, inAreaTypes, curNode, nodes, curTri, outIndices, outAreaTypes); - - const auto iescape = static_cast(curNode) - static_cast(icur); - // Negative index means escape. - node.mOffset = -iescape; - } - } - } - - ChunkyTriMesh::ChunkyTriMesh(const std::vector& verts, const std::vector& indices, - const std::vector& flags, const std::size_t trisPerChunk) - : mMaxTrisPerChunk(0) - { - const auto trianglesCount = indices.size() / 3; - - if (trianglesCount == 0) - return; - - const auto nchunks = (trianglesCount + trisPerChunk - 1) / trisPerChunk; - - mNodes.resize(nchunks * 4); - mIndices.resize(trianglesCount * 3); - mAreaTypes.resize(trianglesCount); - - // Build tree - std::vector items(trianglesCount); - - for (std::size_t i = 0; i < trianglesCount; i++) - { - auto& item = items[i]; - - item.mOffset = static_cast(i); - item.mAreaTypes = flags[i]; - - // Calc triangle XZ bounds. - const auto baseIndex = static_cast(indices[i * 3]) * 3; - - item.mBounds.mMinBound.x() = item.mBounds.mMaxBound.x() = verts[baseIndex + 0]; - item.mBounds.mMinBound.y() = item.mBounds.mMaxBound.y() = verts[baseIndex + 2]; - - for (std::size_t j = 1; j < 3; ++j) - { - const auto index = static_cast(indices[i * 3 + j]) * 3; - - item.mBounds.mMinBound.x() = std::min(item.mBounds.mMinBound.x(), verts[index + 0]); - item.mBounds.mMinBound.y() = std::min(item.mBounds.mMinBound.y(), verts[index + 2]); - - item.mBounds.mMaxBound.x() = std::max(item.mBounds.mMaxBound.x(), verts[index + 0]); - item.mBounds.mMaxBound.y() = std::max(item.mBounds.mMaxBound.y(), verts[index + 2]); - } - } - - std::size_t curTri = 0; - std::size_t curNode = 0; - subdivide(items, 0, trianglesCount, trisPerChunk, indices, flags, curNode, mNodes, curTri, mIndices, mAreaTypes); - - items.clear(); - - mNodes.resize(curNode); - - // Calc max tris per node. - for (auto& node : mNodes) - { - const bool isLeaf = node.mOffset >= 0; - if (!isLeaf) - continue; - if (node.mSize > mMaxTrisPerChunk) - mMaxTrisPerChunk = node.mSize; - } - } -} diff --git a/components/detournavigator/chunkytrimesh.hpp b/components/detournavigator/chunkytrimesh.hpp deleted file mode 100644 index 9f6275ec87..0000000000 --- a/components/detournavigator/chunkytrimesh.hpp +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_CHUNKYTRIMESH_H -#define OPENMW_COMPONENTS_DETOURNAVIGATOR_CHUNKYTRIMESH_H - -#include "areatype.hpp" - -#include - -#include -#include - -namespace DetourNavigator -{ - struct Rect - { - osg::Vec2f mMinBound; - osg::Vec2f mMaxBound; - }; - - struct ChunkyTriMeshNode - { - Rect mBounds; - std::ptrdiff_t mOffset; - std::size_t mSize; - }; - - struct Chunk - { - const int* const mIndices; - const AreaType* const mAreaTypes; - const std::size_t mSize; - }; - - inline bool checkOverlapRect(const Rect& lhs, const Rect& rhs) - { - bool overlap = true; - overlap = (lhs.mMinBound.x() > rhs.mMaxBound.x() || lhs.mMaxBound.x() < rhs.mMinBound.x()) ? false : overlap; - overlap = (lhs.mMinBound.y() > rhs.mMaxBound.y() || lhs.mMaxBound.y() < rhs.mMinBound.y()) ? false : overlap; - return overlap; - } - - class ChunkyTriMesh - { - public: - /// Creates partitioned triangle mesh (AABB tree), - /// where each node contains at max trisPerChunk triangles. - ChunkyTriMesh(const std::vector& verts, const std::vector& tris, - const std::vector& flags, const std::size_t trisPerChunk); - - ChunkyTriMesh(const ChunkyTriMesh&) = delete; - ChunkyTriMesh& operator=(const ChunkyTriMesh&) = delete; - - /// Returns the chunk indices which overlap the input rectable. - template - void forEachChunksOverlappingRect(const Rect& rect, Function&& function) const - { - // Traverse tree - for (std::size_t i = 0; i < mNodes.size(); ) - { - const ChunkyTriMeshNode* node = &mNodes[i]; - const bool overlap = checkOverlapRect(rect, node->mBounds); - const bool isLeafNode = node->mOffset >= 0; - - if (isLeafNode && overlap) - function(i); - - if (overlap || isLeafNode) - i++; - else - { - const auto escapeIndex = -node->mOffset; - i += static_cast(escapeIndex); - } - } - } - - Chunk getChunk(const std::size_t chunkId) const - { - const auto& node = mNodes[chunkId]; - return Chunk { - mIndices.data() + node.mOffset * 3, - mAreaTypes.data() + node.mOffset, - node.mSize - }; - } - - std::size_t getMaxTrisPerChunk() const - { - return mMaxTrisPerChunk; - } - - private: - std::vector mNodes; - std::vector mIndices; - std::vector mAreaTypes; - std::size_t mMaxTrisPerChunk; - }; -} - -#endif diff --git a/components/detournavigator/collisionshapetype.hpp b/components/detournavigator/collisionshapetype.hpp new file mode 100644 index 0000000000..3964840ecf --- /dev/null +++ b/components/detournavigator/collisionshapetype.hpp @@ -0,0 +1,17 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_COLLISIONSHAPETYPE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_COLLISIONSHAPETYPE_H + +#include + +namespace DetourNavigator +{ + enum class CollisionShapeType : std::uint8_t + { + Aabb = 0, + RotatingBox = 1, + }; + + inline constexpr CollisionShapeType defaultCollisionShapeType = CollisionShapeType::Aabb; +} + +#endif diff --git a/components/detournavigator/dbrefgeometryobject.hpp b/components/detournavigator/dbrefgeometryobject.hpp new file mode 100644 index 0000000000..acf2a58b19 --- /dev/null +++ b/components/detournavigator/dbrefgeometryobject.hpp @@ -0,0 +1,61 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H + +#include "objecttransform.hpp" +#include "recastmesh.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator +{ + struct DbRefGeometryObject + { + std::int64_t mShapeId; + ObjectTransform mObjectTransform; + + friend inline auto tie(const DbRefGeometryObject& v) + { + return std::tie(v.mShapeId, v.mObjectTransform); + } + + friend inline bool operator<(const DbRefGeometryObject& l, const DbRefGeometryObject& r) + { + return tie(l) < tie(r); + } + }; + + template + inline auto makeDbRefGeometryObjects(const std::vector& meshSources, ResolveMeshSource&& resolveMeshSource) + -> std::conditional_t< + Misc::isOptional>, + std::optional>, + std::vector + > + { + std::vector result; + result.reserve(meshSources.size()); + for (const MeshSource& meshSource : meshSources) + { + const auto shapeId = resolveMeshSource(meshSource); + if constexpr (Misc::isOptional>) + { + if (!shapeId.has_value()) + return std::nullopt; + result.push_back(DbRefGeometryObject {*shapeId, meshSource.mObjectTransform}); + } + else + result.push_back(DbRefGeometryObject {shapeId, meshSource.mObjectTransform}); + } + std::sort(result.begin(), result.end()); + return result; + } +} + +#endif diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp index c3d67b1848..c76e2bc562 100644 --- a/components/detournavigator/debug.cpp +++ b/components/detournavigator/debug.cpp @@ -1,37 +1,213 @@ #include "debug.hpp" #include "exceptions.hpp" #include "recastmesh.hpp" +#include "settings.hpp" +#include "settingsutils.hpp" + +#include #include +#include + +#include -#include -#include +#include +#include +#include +#include +#include namespace DetourNavigator { - void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision) + std::ostream& operator<<(std::ostream& stream, const TileBounds& value) + { + return stream << "TileBounds {" << value.mMin << ", " << value.mMax << "}"; + } + + std::ostream& operator<<(std::ostream& stream, Status value) + { +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(name) \ + case Status::name: return stream << "DetourNavigator::Status::"#name; + switch (value) + { + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(Success) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(PartialPath) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(NavMeshNotFound) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(StartPolygonNotFound) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(EndPolygonNotFound) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(MoveAlongSurfaceFailed) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(FindPathOverPolygonsFailed) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(GetPolyHeightFailed) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(InitNavMeshQueryFailed) + } +#undef OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE + return stream << "DetourNavigator::Error::" << static_cast(value); + } + + std::ostream& operator<<(std::ostream& s, const Water& v) + { + return s << "Water {" << v.mCellSize << ", " << v.mLevel << "}"; + } + + std::ostream& operator<<(std::ostream& s, const CellWater& v) + { + return s << "CellWater {" << v.mCellPosition << ", " << v.mWater << "}"; + } + + std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v) + { + return s << "FlatHeightfield {" << v.mCellPosition << ", " << v.mCellSize << ", " << v.mHeight << "}"; + } + + std::ostream& operator<<(std::ostream& s, const Heightfield& v) + { + s << "Heightfield {.mCellPosition=" << v.mCellPosition + << ", .mCellSize=" << v.mCellSize + << ", .mLength=" << static_cast(v.mLength) + << ", .mMinHeight=" << v.mMinHeight + << ", .mMaxHeight=" << v.mMaxHeight + << ", .mHeights={"; + for (float h : v.mHeights) + s << h << ", "; + s << "}"; + return s << ", .mOriginalSize=" << v.mOriginalSize << "}"; + } + + std::ostream& operator<<(std::ostream& s, CollisionShapeType v) + { + switch (v) + { + case CollisionShapeType::Aabb: return s << "AgentShapeType::Aabb"; + case CollisionShapeType::RotatingBox: return s << "AgentShapeType::RotatingBox"; + } + return s << "AgentShapeType::" << static_cast>(v); + } + + std::ostream& operator<<(std::ostream& s, const AgentBounds& v) + { + return s << "AgentBounds {" << v.mShapeType << ", " << v.mHalfExtents << "}"; + } + + namespace + { + struct StatusString + { + dtStatus mStatus; + std::string_view mString; + }; + } + + static constexpr std::array dtStatuses { + StatusString {DT_FAILURE, "DT_FAILURE"}, + StatusString {DT_SUCCESS, "DT_SUCCESS"}, + StatusString {DT_IN_PROGRESS, "DT_IN_PROGRESS"}, + StatusString {DT_WRONG_MAGIC, "DT_WRONG_MAGIC"}, + StatusString {DT_WRONG_VERSION, "DT_WRONG_VERSION"}, + StatusString {DT_OUT_OF_MEMORY, "DT_OUT_OF_MEMORY"}, + StatusString {DT_INVALID_PARAM, "DT_INVALID_PARAM"}, + StatusString {DT_BUFFER_TOO_SMALL, "DT_BUFFER_TOO_SMALL"}, + StatusString {DT_OUT_OF_NODES, "DT_OUT_OF_NODES"}, + StatusString {DT_PARTIAL_RESULT, "DT_PARTIAL_RESULT"}, + }; + + std::ostream& operator<<(std::ostream& stream, const WriteDtStatus& value) + { + for (const auto& status : dtStatuses) + if (value.mStatus & status.mStatus) + stream << status.mString; + return stream; + } + + std::ostream& operator<<(std::ostream& stream, const Flag value) + { + switch (value) + { + case Flag_none: + return stream << "none"; + case Flag_walk: + return stream << "walk"; + case Flag_swim: + return stream << "swim"; + case Flag_openDoor: + return stream << "openDoor"; + case Flag_usePathgrid: + return stream << "usePathgrid"; + } + + return stream; + } + + std::ostream& operator<<(std::ostream& stream, const WriteFlags& value) + { + if (value.mValue == Flag_none) + { + return stream << Flag_none; + } + else + { + bool first = true; + for (const auto flag : {Flag_walk, Flag_swim, Flag_openDoor, Flag_usePathgrid}) + { + if (value.mValue & flag) + { + if (!first) + stream << " | "; + first = false; + stream << flag; + } + } + + return stream; + } + } + + std::ostream& operator<<(std::ostream& stream, AreaType value) + { + switch (value) + { + case AreaType_null: return stream << "null"; + case AreaType_water: return stream << "water"; + case AreaType_door: return stream << "door"; + case AreaType_pathgrid: return stream << "pathgrid"; + case AreaType_ground: return stream << "ground"; + } + return stream << "unknown area type (" << static_cast>(value) << ")"; + } + + std::ostream& operator<<(std::ostream& stream, ChangeType value) + { + switch (value) + { + case ChangeType::remove: + return stream << "ChangeType::remove"; + case ChangeType::mixed: + return stream << "ChangeType::mixed"; + case ChangeType::add: + return stream << "ChangeType::add"; + case ChangeType::update: + return stream << "ChangeType::update"; + } + return stream << "ChangeType::" << static_cast(value); + } + + void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, + const std::string& revision, const RecastSettings& settings) { const auto path = pathPrefix + "recastmesh" + revision + ".obj"; - boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out); + std::ofstream file(std::filesystem::path(path), std::ios::out); if (!file.is_open()) throw NavigatorException("Open file failed: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); file.precision(std::numeric_limits::max_exponent10); - std::size_t count = 0; - for (auto v : recastMesh.getVertices()) + std::vector vertices = recastMesh.getMesh().getVertices(); + for (std::size_t i = 0; i < vertices.size(); i += 3) { - if (count % 3 == 0) - { - if (count != 0) - file << '\n'; - file << 'v'; - } - file << ' ' << v; - ++count; + file << "v " << toNavMeshCoordinates(settings, vertices[i]) << ' ' + << toNavMeshCoordinates(settings, vertices[i + 2]) << ' ' + << toNavMeshCoordinates(settings, vertices[i + 1]) << '\n'; } - file << '\n'; - count = 0; - for (auto v : recastMesh.getIndices()) + std::size_t count = 0; + for (int v : recastMesh.getMesh().getIndices()) { if (count % 3 == 0) { @@ -65,7 +241,7 @@ namespace DetourNavigator }; const auto path = pathPrefix + "all_tiles_navmesh" + revision + ".bin"; - boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out | std::ios::binary); + std::ofstream file(std::filesystem::path(path), std::ios::out | std::ios::binary); if (!file.is_open()) throw NavigatorException("Open file failed: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); diff --git a/components/detournavigator/debug.hpp b/components/detournavigator/debug.hpp index a17eec16a7..818be0616c 100644 --- a/components/detournavigator/debug.hpp +++ b/components/detournavigator/debug.hpp @@ -3,52 +3,62 @@ #include "tilebounds.hpp" #include "status.hpp" +#include "recastmesh.hpp" +#include "agentbounds.hpp" +#include "flags.hpp" +#include "areatype.hpp" +#include "changetype.hpp" -#include +#include -#include -#include - -#include -#include -#include -#include -#include -#include #include -#include +#include class dtNavMesh; namespace DetourNavigator { - inline std::ostream& operator <<(std::ostream& stream, const TileBounds& value) + std::ostream& operator<<(std::ostream& stream, const TileBounds& value); + + std::ostream& operator<<(std::ostream& stream, Status value); + + std::ostream& operator<<(std::ostream& s, const Water& v); + + std::ostream& operator<<(std::ostream& s, const CellWater& v); + + std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v); + + std::ostream& operator<<(std::ostream& s, const Heightfield& v); + + std::ostream& operator<<(std::ostream& s, CollisionShapeType v); + + std::ostream& operator<<(std::ostream& s, const AgentBounds& v); + + struct WriteDtStatus { - return stream << "TileBounds {" << value.mMin << ", " << value.mMax << "}"; - } + dtStatus mStatus; + }; + + std::ostream& operator<<(std::ostream& stream, const WriteDtStatus& value); + + std::ostream& operator<<(std::ostream& stream, const Flag value); - inline std::ostream& operator <<(std::ostream& stream, Status value) + struct WriteFlags { -#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(name) \ - case Status::name: return stream << "DetourNavigator::Status::"#name; - switch (value) - { - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(Success) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(NavMeshNotFound) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(StartPolygonNotFound) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(EndPolygonNotFound) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(MoveAlongSurfaceFailed) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(FindPathOverPolygonsFailed) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(GetPolyHeightFailed) - OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(InitNavMeshQueryFailed) - } -#undef OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE - return stream << "DetourNavigator::Error::" << static_cast(value); - } + Flags mValue; + }; + + std::ostream& operator<<(std::ostream& stream, const WriteFlags& value); + + std::ostream& operator<<(std::ostream& stream, AreaType value); + + std::ostream& operator<<(std::ostream& stream, ChangeType value); class RecastMesh; + struct RecastSettings; - void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision); + void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, + const std::string& revision, const RecastSettings& settings); void writeToFile(const dtNavMesh& navMesh, const std::string& pathPrefix, const std::string& revision); } diff --git a/components/detournavigator/dtstatus.hpp b/components/detournavigator/dtstatus.hpp deleted file mode 100644 index a73d33be1e..0000000000 --- a/components/detournavigator/dtstatus.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DTSTATUS_H -#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DTSTATUS_H - -#include - -#include -#include - -namespace DetourNavigator -{ - struct WriteDtStatus - { - dtStatus status; - }; - - static const std::vector> dtStatuses { - {DT_FAILURE, "DT_FAILURE"}, - {DT_SUCCESS, "DT_SUCCESS"}, - {DT_IN_PROGRESS, "DT_IN_PROGRESS"}, - {DT_WRONG_MAGIC, "DT_WRONG_MAGIC"}, - {DT_WRONG_VERSION, "DT_WRONG_VERSION"}, - {DT_OUT_OF_MEMORY, "DT_OUT_OF_MEMORY"}, - {DT_INVALID_PARAM, "DT_INVALID_PARAM"}, - {DT_BUFFER_TOO_SMALL, "DT_BUFFER_TOO_SMALL"}, - {DT_OUT_OF_NODES, "DT_OUT_OF_NODES"}, - {DT_PARTIAL_RESULT, "DT_PARTIAL_RESULT"}, - }; - - inline std::ostream& operator <<(std::ostream& stream, const WriteDtStatus& value) - { - for (const auto& status : dtStatuses) - if (value.status & status.first) - stream << status.second << " "; - return stream; - } -} - -#endif diff --git a/components/detournavigator/findrandompointaroundcircle.cpp b/components/detournavigator/findrandompointaroundcircle.cpp index f2e815c918..6bd761c817 100644 --- a/components/detournavigator/findrandompointaroundcircle.cpp +++ b/components/detournavigator/findrandompointaroundcircle.cpp @@ -4,14 +4,13 @@ #include -#include #include #include namespace DetourNavigator { std::optional findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings) + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const DetourSettings& settings, float(*prng)()) { dtNavMeshQuery navMeshQuery; if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes)) @@ -20,23 +19,15 @@ namespace DetourNavigator dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); - dtPolyRef startRef = 0; - osg::Vec3f startPolygonPosition; - for (int i = 0; i < 3; ++i) - { - const auto status = navMeshQuery.findNearestPoly(start.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter, - &startRef, startPolygonPosition.ptr()); - if (!dtStatusFailed(status) && startRef != 0) - break; - } - + dtPolyRef startRef = findNearestPoly(navMeshQuery, queryFilter, start, halfExtents * 4); if (startRef == 0) return std::optional(); dtPolyRef resultRef = 0; osg::Vec3f resultPosition; + navMeshQuery.findRandomPointAroundCircle(startRef, start.ptr(), maxRadius, &queryFilter, - []() { return Misc::Rng::rollProbability(); }, &resultRef, resultPosition.ptr()); + prng, &resultRef, resultPosition.ptr()); if (resultRef == 0) return std::optional(); diff --git a/components/detournavigator/findrandompointaroundcircle.hpp b/components/detournavigator/findrandompointaroundcircle.hpp index d0dc2bbbc0..346ad36e4e 100644 --- a/components/detournavigator/findrandompointaroundcircle.hpp +++ b/components/detournavigator/findrandompointaroundcircle.hpp @@ -10,10 +10,10 @@ class dtNavMesh; namespace DetourNavigator { - struct Settings; + struct DetourSettings; std::optional findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings); + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const DetourSettings& settings, float(*prng)()); } #endif diff --git a/components/detournavigator/findsmoothpath.cpp b/components/detournavigator/findsmoothpath.cpp index 8e443def98..cbaf12305c 100644 --- a/components/detournavigator/findsmoothpath.cpp +++ b/components/detournavigator/findsmoothpath.cpp @@ -1,16 +1,22 @@ #include "findsmoothpath.hpp" +#include + #include #include namespace DetourNavigator { - std::vector fixupCorridor(const std::vector& path, const std::vector& visited) + std::size_t fixupCorridor(std::vector& path, std::size_t pathSize, const std::vector& visited) { std::vector::const_reverse_iterator furthestVisited; // Find furthest common polygon. - const auto it = std::find_if(path.rbegin(), path.rend(), [&] (dtPolyRef pathValue) + const auto begin = path.begin(); + const auto end = path.begin() + pathSize; + const std::reverse_iterator rbegin(end); + const std::reverse_iterator rend(begin); + const auto it = std::find_if(rbegin, rend, [&] (dtPolyRef pathValue) { const auto it = std::find(visited.rbegin(), visited.rend(), pathValue); if (it == visited.rend()) @@ -20,48 +26,35 @@ namespace DetourNavigator }); // If no intersection found just return current path. - if (it == path.rend()) - return path; + if (it == rend) + return pathSize; const auto furthestPath = it.base() - 1; // Concatenate paths. // visited: a_1 ... a_n x b_1 ... b_n // furthestVisited ^ - // path: C x D - // ^ furthestPath + // path: C x D E + // ^ furthestPath ^ path.size() - (furthestVisited + 1 - visited.rbegin()) // result: x b_n ... b_1 D - std::vector result; - result.reserve(static_cast(furthestVisited - visited.rbegin()) - + static_cast(path.end() - furthestPath) - 1); - std::copy(visited.rbegin(), furthestVisited + 1, std::back_inserter(result)); - std::copy(furthestPath + 1, path.end(), std::back_inserter(result)); + const std::size_t required = static_cast(furthestVisited + 1 - visited.rbegin()); + const auto newEnd = std::copy(furthestPath + 1, std::min(begin + path.size(), end), begin + required); + std::copy(visited.rbegin(), furthestVisited + 1, begin); - return result; + return static_cast(newEnd - begin); } - // This function checks if the path has a small U-turn, that is, - // a polygon further in the path is adjacent to the first polygon - // in the path. If that happens, a shortcut is taken. - // This can happen if the target (T) location is at tile boundary, - // and we're (S) approaching it parallel to the tile edge. - // The choice at the vertex can be arbitrary, - // +---+---+ - // |:::|:::| - // +-S-+-T-+ - // |:::| | <-- the step can end up in here, resulting U-turn path. - // +---+---+ - std::vector fixupShortcuts(const std::vector& path, const dtNavMeshQuery& navQuery) + std::size_t fixupShortcuts(dtPolyRef* path, std::size_t pathSize, const dtNavMeshQuery& navQuery) { - if (path.size() < 3) - return path; + if (pathSize < 3) + return pathSize; // Get connected polygons - const dtMeshTile* tile = 0; - const dtPoly* poly = 0; + const dtMeshTile* tile = nullptr; + const dtPoly* poly = nullptr; if (dtStatusFailed(navQuery.getAttachedNavMesh()->getTileAndPolyByRef(path[0], &tile, &poly))) - return path; + return pathSize; const std::size_t maxNeis = 16; std::array neis; @@ -81,7 +74,7 @@ namespace DetourNavigator // in the path, short cut to that polygon directly. const std::size_t maxLookAhead = 6; std::size_t cut = 0; - for (std::size_t i = std::min(maxLookAhead, path.size()) - 1; i > 1 && cut == 0; i--) + for (std::size_t i = std::min(maxLookAhead, pathSize) - 1; i > 1 && cut == 0; i--) { for (std::size_t j = 0; j < nneis; j++) { @@ -93,28 +86,28 @@ namespace DetourNavigator } } if (cut <= 1) - return path; + return pathSize; - std::vector result; - const auto offset = cut - 1; - result.reserve(1 + path.size() - offset); - result.push_back(path.front()); - std::copy(path.begin() + std::ptrdiff_t(offset), path.end(), std::back_inserter(result)); - return result; + const std::ptrdiff_t offset = static_cast(cut) - 1; + std::copy(path + offset, path + pathSize, path); + return pathSize - offset; } - std::optional getSteerTarget(const dtNavMeshQuery& navQuery, const osg::Vec3f& startPos, - const osg::Vec3f& endPos, const float minTargetDist, const std::vector& path) + std::optional getSteerTarget(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& startPos, + const osg::Vec3f& endPos, const float minTargetDist, const dtPolyRef* path, const std::size_t pathSize) { // Find steer target. SteerTarget result; - const int MAX_STEER_POINTS = 3; - std::array steerPath; - std::array steerPathFlags; - std::array steerPathPolys; + constexpr int maxSteerPoints = 3; + std::array steerPath; + std::array steerPathFlags; + std::array steerPathPolys; int nsteerPath = 0; - navQuery.findStraightPath(startPos.ptr(), endPos.ptr(), path.data(), int(path.size()), steerPath.data(), - steerPathFlags.data(), steerPathPolys.data(), &nsteerPath, MAX_STEER_POINTS); + const dtStatus status = navMeshQuery.findStraightPath(startPos.ptr(), endPos.ptr(), path, + static_cast(pathSize), steerPath.data(), steerPathFlags.data(), steerPathPolys.data(), + &nsteerPath, maxSteerPoints); + if (dtStatusFailed(status)) + return std::nullopt; assert(nsteerPath >= 0); if (!nsteerPath) return std::nullopt; @@ -125,7 +118,7 @@ namespace DetourNavigator { // Stop at Off-Mesh link or when point is further than slop away. if ((steerPathFlags[ns] & DT_STRAIGHTPATH_OFFMESH_CONNECTION) || - !inRange(Misc::Convert::makeOsgVec3f(&steerPath[ns * 3]), startPos, minTargetDist, 1000.0f)) + !inRange(Misc::Convert::makeOsgVec3f(&steerPath[ns * 3]), startPos, minTargetDist)) break; ns++; } @@ -133,11 +126,21 @@ namespace DetourNavigator if (ns >= static_cast(nsteerPath)) return std::nullopt; - dtVcopy(result.steerPos.ptr(), &steerPath[ns * 3]); - result.steerPos.y() = startPos[1]; - result.steerPosFlag = steerPathFlags[ns]; - result.steerPosRef = steerPathPolys[ns]; + dtVcopy(result.mSteerPos.ptr(), &steerPath[ns * 3]); + result.mSteerPos.y() = startPos[1]; + result.mSteerPosFlag = steerPathFlags[ns]; + result.mSteerPosRef = steerPathPolys[ns]; return result; } + + dtPolyRef findNearestPoly(const dtNavMeshQuery& query, const dtQueryFilter& filter, + const osg::Vec3f& center, const osg::Vec3f& halfExtents) + { + dtPolyRef ref = 0; + const dtStatus status = query.findNearestPoly(center.ptr(), halfExtents.ptr(), &filter, &ref, nullptr); + if (!dtStatusSucceed(status)) + return 0; + return ref; + } } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index dcdddce8a6..aade62ad62 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -1,12 +1,10 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDSMOOTHPATH_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDSMOOTHPATH_H -#include "dtstatus.hpp" #include "exceptions.hpp" #include "flags.hpp" #include "settings.hpp" #include "settingsutils.hpp" -#include "debug.hpp" #include "status.hpp" #include "areatype.hpp" @@ -14,13 +12,11 @@ #include #include -#include - -#include - #include + #include #include +#include class dtNavMesh; @@ -28,13 +24,12 @@ namespace DetourNavigator { struct Settings; - inline bool inRange(const osg::Vec3f& v1, const osg::Vec3f& v2, const float r, const float h) + inline bool inRange(const osg::Vec3f& v1, const osg::Vec3f& v2, const float r) { - const auto d = v2 - v1; - return (d.x() * d.x() + d.z() * d.z()) < r * r && std::abs(d.y()) < h; + return (osg::Vec2f(v1.x(), v1.z()) - osg::Vec2f(v2.x(), v2.z())).length() < r; } - std::vector fixupCorridor(const std::vector& path, const std::vector& visited); + std::size_t fixupCorridor(std::vector& path, std::size_t pathSize, const std::vector& visited); // This function checks if the path has a small U-turn, that is, // a polygon further in the path is adjacent to the first polygon @@ -47,23 +42,23 @@ namespace DetourNavigator // +-S-+-T-+ // |:::| | <-- the step can end up in here, resulting U-turn path. // +---+---+ - std::vector fixupShortcuts(const std::vector& path, const dtNavMeshQuery& navQuery); + std::size_t fixupShortcuts(dtPolyRef* path, std::size_t pathSize, const dtNavMeshQuery& navQuery); struct SteerTarget { - osg::Vec3f steerPos; - unsigned char steerPosFlag; - dtPolyRef steerPosRef; + osg::Vec3f mSteerPos; + unsigned char mSteerPosFlag; + dtPolyRef mSteerPosRef; }; std::optional getSteerTarget(const dtNavMeshQuery& navQuery, const osg::Vec3f& startPos, - const osg::Vec3f& endPos, const float minTargetDist, const std::vector& path); + const osg::Vec3f& endPos, const float minTargetDist, const dtPolyRef* path, const std::size_t pathSize); template class OutputTransformIterator { public: - OutputTransformIterator(OutputIterator& impl, const Settings& settings) + explicit OutputTransformIterator(OutputIterator& impl, const RecastSettings& settings) : mImpl(impl), mSettings(settings) { } @@ -94,7 +89,7 @@ namespace DetourNavigator private: std::reference_wrapper mImpl; - std::reference_wrapper mSettings; + std::reference_wrapper mSettings; }; inline bool initNavMeshQuery(dtNavMeshQuery& value, const dtNavMesh& navMesh, const int maxNodes) @@ -103,6 +98,9 @@ namespace DetourNavigator return dtStatusSucceed(status); } + dtPolyRef findNearestPoly(const dtNavMeshQuery& query, const dtQueryFilter& filter, + const osg::Vec3f& center, const osg::Vec3f& halfExtents); + struct MoveAlongSurfaceResult { osg::Vec3f mResultPos; @@ -126,44 +124,33 @@ namespace DetourNavigator return {std::move(result)}; } - inline std::optional> findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, + inline std::optional findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, - const std::size_t maxSize) + dtPolyRef* path, const std::size_t maxSize) { int pathLen = 0; - std::vector result(maxSize); const auto status = navMeshQuery.findPath(startRef, endRef, startPos.ptr(), endPos.ptr(), &queryFilter, - result.data(), &pathLen, static_cast(maxSize)); + path, &pathLen, static_cast(maxSize)); if (!dtStatusSucceed(status)) return {}; assert(pathLen >= 0); assert(static_cast(pathLen) <= maxSize); - result.resize(static_cast(pathLen)); - return {std::move(result)}; - } - - inline std::optional getPolyHeight(const dtNavMeshQuery& navMeshQuery, const dtPolyRef ref, const osg::Vec3f& pos) - { - float result = 0.0f; - const auto status = navMeshQuery.getPolyHeight(ref, pos.ptr(), &result); - if (!dtStatusSucceed(status)) - return {}; - return result; + return static_cast(pathLen); } template Status makeSmoothPath(const dtNavMesh& navMesh, const dtNavMeshQuery& navMeshQuery, const dtQueryFilter& filter, const osg::Vec3f& start, const osg::Vec3f& end, const float stepSize, - std::vector polygonPath, std::size_t maxSmoothPathSize, OutputIterator& out) + std::vector& polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, OutputIterator& out) { // Iterate over the path to find smooth path on the detail mesh surface. osg::Vec3f iterPos; - navMeshQuery.closestPointOnPoly(polygonPath.front(), start.ptr(), iterPos.ptr(), 0); + navMeshQuery.closestPointOnPoly(polygonPath.front(), start.ptr(), iterPos.ptr(), nullptr); osg::Vec3f targetPos; - navMeshQuery.closestPointOnPoly(polygonPath.back(), end.ptr(), targetPos.ptr(), 0); + navMeshQuery.closestPointOnPoly(polygonPath[polygonPathSize - 1], end.ptr(), targetPos.ptr(), nullptr); - const float SLOP = 0.01f; + constexpr float slop = 0.01f; *out++ = iterPos; @@ -171,19 +158,19 @@ namespace DetourNavigator // Move towards target a small advancement at a time until target reached or // when ran out of memory to store the path. - while (!polygonPath.empty() && smoothPathSize < maxSmoothPathSize) + while (polygonPathSize > 0 && smoothPathSize < maxSmoothPathSize) { // Find location to steer towards. - const auto steerTarget = getSteerTarget(navMeshQuery, iterPos, targetPos, SLOP, polygonPath); + const auto steerTarget = getSteerTarget(navMeshQuery, iterPos, targetPos, slop, polygonPath.data(), polygonPathSize); if (!steerTarget) break; - const bool endOfPath = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_END); - const bool offMeshConnection = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION); + const bool endOfPath = bool(steerTarget->mSteerPosFlag & DT_STRAIGHTPATH_END); + const bool offMeshConnection = bool(steerTarget->mSteerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION); // Find movement delta. - const osg::Vec3f delta = steerTarget->steerPos - iterPos; + const osg::Vec3f delta = steerTarget->mSteerPos - iterPos; float len = delta.length(); // If the steer target is end of path or off-mesh link, do not move past the location. if ((endOfPath || offMeshConnection) && len < stepSize) @@ -197,16 +184,11 @@ namespace DetourNavigator if (!result) return Status::MoveAlongSurfaceFailed; - polygonPath = fixupCorridor(polygonPath, result->mVisited); - polygonPath = fixupShortcuts(polygonPath, navMeshQuery); - - float h = 0; - navMeshQuery.getPolyHeight(polygonPath.front(), result->mResultPos.ptr(), &h); - iterPos = result->mResultPos; - iterPos.y() = h; + polygonPathSize = fixupCorridor(polygonPath, polygonPathSize, result->mVisited); + polygonPathSize = fixupShortcuts(polygonPath.data(), polygonPathSize, navMeshQuery); // Handle end of path and off-mesh links when close enough. - if (endOfPath && inRange(iterPos, steerTarget->steerPos, SLOP, 1.0f)) + if (endOfPath && inRange(result->mResultPos, steerTarget->mSteerPos, slop)) { // Reached end of path. iterPos = targetPos; @@ -214,20 +196,26 @@ namespace DetourNavigator ++smoothPathSize; break; } - else if (offMeshConnection && inRange(iterPos, steerTarget->steerPos, SLOP, 1.0f)) + + dtPolyRef polyRef = polygonPath.front(); + osg::Vec3f polyPos = result->mResultPos; + + if (offMeshConnection && inRange(polyPos, steerTarget->mSteerPos, slop)) { // Advance the path up to and over the off-mesh connection. dtPolyRef prevRef = 0; - dtPolyRef polyRef = polygonPath.front(); std::size_t npos = 0; - while (npos < polygonPath.size() && polyRef != steerTarget->steerPosRef) + while (npos < polygonPathSize && polyRef != steerTarget->mSteerPosRef) { prevRef = polyRef; polyRef = polygonPath[npos]; ++npos; } - std::copy(polygonPath.begin() + std::ptrdiff_t(npos), polygonPath.end(), polygonPath.begin()); - polygonPath.resize(polygonPath.size() - npos); + if (npos > 0) + { + std::copy(polygonPath.begin() + npos, polygonPath.begin() + polygonPathSize, polygonPath.begin()); + polygonPathSize -= npos; + } // Reached off-mesh connection. osg::Vec3f startPos; @@ -248,16 +236,15 @@ namespace DetourNavigator } // Move position at the other side of the off-mesh link. - iterPos = endPos; - const auto height = getPolyHeight(navMeshQuery, polygonPath.front(), iterPos); - - if (!height) - return Status::GetPolyHeightFailed; - - iterPos.y() = *height; + polyPos = endPos; } } + if (dtStatusFailed(navMeshQuery.getPolyHeight(polyRef, polyPos.ptr(), &iterPos.y()))) + return Status::GetPolyHeightFailed; + iterPos.x() = result->mResultPos.x(); + iterPos.z() = result->mResultPos.z(); + // Store results. *out++ = iterPos; ++smoothPathSize; @@ -269,10 +256,10 @@ namespace DetourNavigator template Status findSmoothPath(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, const float stepSize, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, - const Settings& settings, OutputIterator& out) + const Settings& settings, float endTolerance, OutputIterator& out) { dtNavMeshQuery navMeshQuery; - if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes)) + if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mDetour.mMaxNavMeshQueryNodes)) return Status::InitNavMeshQueryFailed; dtQueryFilter queryFilter; @@ -282,44 +269,37 @@ namespace DetourNavigator queryFilter.setAreaCost(AreaType_pathgrid, areaCosts.mPathgrid); queryFilter.setAreaCost(AreaType_ground, areaCosts.mGround); - dtPolyRef startRef = 0; - osg::Vec3f startPolygonPosition; - for (int i = 0; i < 3; ++i) - { - const auto status = navMeshQuery.findNearestPoly(start.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter, - &startRef, startPolygonPosition.ptr()); - if (!dtStatusFailed(status) && startRef != 0) - break; - } + constexpr float polyDistanceFactor = 4; + const osg::Vec3f polyHalfExtents = halfExtents * polyDistanceFactor; + const dtPolyRef startRef = findNearestPoly(navMeshQuery, queryFilter, start, polyHalfExtents); if (startRef == 0) return Status::StartPolygonNotFound; - dtPolyRef endRef = 0; - osg::Vec3f endPolygonPosition; - for (int i = 0; i < 3; ++i) - { - const auto status = navMeshQuery.findNearestPoly(end.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter, - &endRef, endPolygonPosition.ptr()); - if (!dtStatusFailed(status) && endRef != 0) - break; - } - + const dtPolyRef endRef = findNearestPoly(navMeshQuery, queryFilter, end, + polyHalfExtents + osg::Vec3f(endTolerance, endTolerance, endTolerance)); if (endRef == 0) return Status::EndPolygonNotFound; - const auto polygonPath = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter, - settings.mMaxPolygonPathSize); + std::vector polygonPath(settings.mDetour.mMaxPolygonPathSize); + const auto polygonPathSize = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter, + polygonPath.data(), polygonPath.size()); - if (!polygonPath) + if (!polygonPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (polygonPath->empty() || polygonPath->back() != endRef) + if (*polygonPathSize == 0) return Status::Success; - auto outTransform = OutputTransformIterator(out, settings); - return makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize, std::move(*polygonPath), - settings.mMaxSmoothPathSize, outTransform); + const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; + auto outTransform = OutputTransformIterator(out, settings.mRecast); + const Status smoothStatus = makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize, + polygonPath, *polygonPathSize, settings.mDetour.mMaxSmoothPathSize, outTransform); + + if (smoothStatus != Status::Success) + return smoothStatus; + + return partialPath ? Status::PartialPath : Status::Success; } } diff --git a/components/detournavigator/flags.hpp b/components/detournavigator/flags.hpp index 887fd42640..981895ca94 100644 --- a/components/detournavigator/flags.hpp +++ b/components/detournavigator/flags.hpp @@ -1,8 +1,6 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_FLAGS_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_FLAGS_H -#include - namespace DetourNavigator { using Flags = unsigned short; @@ -15,54 +13,6 @@ namespace DetourNavigator Flag_openDoor = 1 << 2, Flag_usePathgrid = 1 << 3, }; - - inline std::ostream& operator <<(std::ostream& stream, const Flag value) - { - switch (value) - { - case Flag_none: - return stream << "none"; - case Flag_walk: - return stream << "walk"; - case Flag_swim: - return stream << "swim"; - case Flag_openDoor: - return stream << "openDoor"; - case Flag_usePathgrid: - return stream << "usePathgrid"; - } - - return stream; - } - - struct WriteFlags - { - Flags mValue; - - friend inline std::ostream& operator <<(std::ostream& stream, const WriteFlags& value) - { - if (value.mValue == Flag_none) - { - return stream << Flag_none; - } - else - { - bool first = true; - for (const auto flag : {Flag_walk, Flag_swim, Flag_openDoor, Flag_usePathgrid}) - { - if (value.mValue & flag) - { - if (!first) - stream << " | "; - first = false; - stream << flag; - } - } - - return stream; - } - } - }; } #endif diff --git a/components/detournavigator/generatenavmeshtile.cpp b/components/detournavigator/generatenavmeshtile.cpp new file mode 100644 index 0000000000..2beb9d6468 --- /dev/null +++ b/components/detournavigator/generatenavmeshtile.cpp @@ -0,0 +1,102 @@ +#include "generatenavmeshtile.hpp" + +#include "dbrefgeometryobject.hpp" +#include "makenavmesh.hpp" +#include "offmeshconnectionsmanager.hpp" +#include "preparednavmeshdata.hpp" +#include "serialization.hpp" +#include "settings.hpp" +#include "tilecachedrecastmeshmanager.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace + { + struct Ignore + { + std::string_view mWorldspace; + const TilePosition& mTilePosition; + std::shared_ptr mConsumer; + + ~Ignore() noexcept + { + if (mConsumer != nullptr) + mConsumer->ignore(mWorldspace, mTilePosition); + } + }; + } + + GenerateNavMeshTile::GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition, + RecastMeshProvider recastMeshProvider, const AgentBounds& agentBounds, + const DetourNavigator::Settings& settings, std::weak_ptr consumer) + : mWorldspace(std::move(worldspace)) + , mTilePosition(tilePosition) + , mRecastMeshProvider(recastMeshProvider) + , mAgentBounds(agentBounds) + , mSettings(settings) + , mConsumer(std::move(consumer)) {} + + void GenerateNavMeshTile::doWork() + { + impl(); + } + + void GenerateNavMeshTile::impl() noexcept + { + const auto consumer = mConsumer.lock(); + + if (consumer == nullptr) + return; + + try + { + Ignore ignore {mWorldspace, mTilePosition, consumer}; + + const std::shared_ptr recastMesh = mRecastMeshProvider.getMesh(mWorldspace, mTilePosition); + + if (recastMesh == nullptr || isEmpty(*recastMesh)) + return; + + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return consumer->resolveMeshSource(v); }); + std::vector input = serialize(mSettings.mRecast, mAgentBounds, *recastMesh, objects); + const std::optional info = consumer->find(mWorldspace, mTilePosition, input); + + if (info.has_value() && info->mVersion == navMeshFormatVersion) + { + consumer->identity(mWorldspace, mTilePosition, info->mTileId); + ignore.mConsumer = nullptr; + return; + } + + const auto data = prepareNavMeshTileData(*recastMesh, mTilePosition, mAgentBounds, mSettings.mRecast); + + if (data == nullptr) + return; + + if (info.has_value()) + consumer->update(mWorldspace, mTilePosition, info->mTileId, navMeshFormatVersion, *data); + else + consumer->insert(mWorldspace, mTilePosition, navMeshFormatVersion, input, *data); + + ignore.mConsumer = nullptr; + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to generate navmesh for worldspace \"" << mWorldspace + << "\" tile " << mTilePosition << ": " << e.what(); + consumer->cancel(e.what()); + } + } +} diff --git a/components/detournavigator/generatenavmeshtile.hpp b/components/detournavigator/generatenavmeshtile.hpp new file mode 100644 index 0000000000..0fc7341ff1 --- /dev/null +++ b/components/detournavigator/generatenavmeshtile.hpp @@ -0,0 +1,78 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H + +#include "recastmeshprovider.hpp" +#include "tileposition.hpp" +#include "agentbounds.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator +{ + class OffMeshConnectionsManager; + class RecastMesh; + struct NavMeshTileConsumer; + struct OffMeshConnection; + struct PreparedNavMeshData; + struct Settings; + + struct NavMeshTileInfo + { + std::int64_t mTileId; + std::int64_t mVersion; + }; + + struct NavMeshTileConsumer + { + virtual ~NavMeshTileConsumer() = default; + + virtual std::int64_t resolveMeshSource(const MeshSource& source) = 0; + + virtual std::optional find(std::string_view worldspace, const TilePosition& tilePosition, + const std::vector& input) = 0; + + virtual void ignore(std::string_view worldspace, const TilePosition& tilePosition) = 0; + + virtual void identity(std::string_view worldspace, const TilePosition& tilePosition, + std::int64_t tileId) = 0; + + virtual void insert(std::string_view worldspace, const TilePosition& tilePosition, + std::int64_t version, const std::vector& input, PreparedNavMeshData& data) = 0; + + virtual void update(std::string_view worldspace, const TilePosition& tilePosition, + std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) = 0; + + virtual void cancel(std::string_view reason) = 0; + }; + + class GenerateNavMeshTile final : public SceneUtil::WorkItem + { + public: + GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition, + RecastMeshProvider recastMeshProvider, const AgentBounds& agentBounds, const Settings& settings, + std::weak_ptr consumer); + + void doWork() final; + + private: + const std::string mWorldspace; + const TilePosition mTilePosition; + const RecastMeshProvider mRecastMeshProvider; + const AgentBounds mAgentBounds; + const Settings& mSettings; + std::weak_ptr mConsumer; + + inline void impl() noexcept; + }; +} + +#endif diff --git a/components/detournavigator/gettilespositions.cpp b/components/detournavigator/gettilespositions.cpp new file mode 100644 index 0000000000..2d42ec25bb --- /dev/null +++ b/components/detournavigator/gettilespositions.cpp @@ -0,0 +1,79 @@ +#include "gettilespositions.hpp" +#include "settings.hpp" +#include "settingsutils.hpp" +#include "tileposition.hpp" +#include "tilebounds.hpp" + +#include + +#include + +namespace DetourNavigator +{ + TilesPositionsRange makeTilesPositionsRange(const osg::Vec2f& aabbMin, const osg::Vec2f& aabbMax, + const RecastSettings& settings) + { + osg::Vec2f min = toNavMeshCoordinates(settings, aabbMin); + osg::Vec2f max = toNavMeshCoordinates(settings, aabbMax); + + const float border = getBorderSize(settings); + min -= osg::Vec2f(border, border); + max += osg::Vec2f(border, border); + + TilePosition minTile = getTilePosition(settings, min); + TilePosition maxTile = getTilePosition(settings, max); + + if (minTile.x() > maxTile.x()) + std::swap(minTile.x(), maxTile.x()); + + if (minTile.y() > maxTile.y()) + std::swap(minTile.y(), maxTile.y()); + + return {minTile, maxTile + osg::Vec2i(1, 1)}; + } + + TilesPositionsRange makeTilesPositionsRange(const btCollisionShape& shape, const btTransform& transform, + const RecastSettings& settings) + { + const TileBounds bounds = makeObjectTileBounds(shape, transform); + return makeTilesPositionsRange(bounds.mMin, bounds.mMax, settings); + } + + TilesPositionsRange makeTilesPositionsRange(const btCollisionShape& shape, const btTransform& transform, + const TileBounds& bounds, const RecastSettings& settings) + { + if (const auto intersection = getIntersection(bounds, makeObjectTileBounds(shape, transform))) + return makeTilesPositionsRange(intersection->mMin, intersection->mMax, settings); + return {}; + } + + TilesPositionsRange makeTilesPositionsRange(const int cellSize, const btVector3& shift, + const RecastSettings& settings) + { + const int halfCellSize = cellSize / 2; + const btTransform transform(btMatrix3x3::getIdentity(), shift); + btVector3 aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); + btVector3 aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0)); + + aabbMin.setX(std::min(aabbMin.x(), aabbMax.x())); + aabbMin.setY(std::min(aabbMin.y(), aabbMax.y())); + + aabbMax.setX(std::max(aabbMin.x(), aabbMax.x())); + aabbMax.setY(std::max(aabbMin.y(), aabbMax.y())); + + return makeTilesPositionsRange(Misc::Convert::toOsgXY(aabbMin), Misc::Convert::toOsgXY(aabbMax), settings); + } + + TilesPositionsRange getIntersection(const TilesPositionsRange& a, const TilesPositionsRange& b) noexcept + { + const int beginX = std::max(a.mBegin.x(), b.mBegin.x()); + const int endX = std::min(a.mEnd.x(), b.mEnd.x()); + if (beginX > endX) + return {}; + const int beginY = std::max(a.mBegin.y(), b.mBegin.y()); + const int endY = std::min(a.mEnd.y(), b.mEnd.y()); + if (beginY > endY) + return {}; + return TilesPositionsRange {TilePosition(beginX, beginY), TilePosition(endX, endY)}; + } +} diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index e233795e68..33c1131176 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -1,70 +1,55 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GETTILESPOSITIONS_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_GETTILESPOSITIONS_H -#include "settings.hpp" -#include "settingsutils.hpp" +#include "tilebounds.hpp" #include "tileposition.hpp" +#include "tilespositionsrange.hpp" -#include +class btVector3; +class btTransform; +class btCollisionShape; -#include - -#include +namespace osg +{ + class Vec2f; +} namespace DetourNavigator { - template - void getTilesPositions(const osg::Vec3f& aabbMin, const osg::Vec3f& aabbMax, - const Settings& settings, Callback&& callback) - { - auto min = toNavMeshCoordinates(settings, aabbMin); - auto max = toNavMeshCoordinates(settings, aabbMax); + struct RecastSettings; - const auto border = getBorderSize(settings); - min -= osg::Vec3f(border, border, border); - max += osg::Vec3f(border, border, border); + TilesPositionsRange makeTilesPositionsRange(const osg::Vec2f& aabbMin, + const osg::Vec2f& aabbMax, const RecastSettings& settings); - auto minTile = getTilePosition(settings, min); - auto maxTile = getTilePosition(settings, max); + TilesPositionsRange makeTilesPositionsRange(const btCollisionShape& shape, + const btTransform& transform, const RecastSettings& settings); - if (minTile.x() > maxTile.x()) - std::swap(minTile.x(), maxTile.x()); + TilesPositionsRange makeTilesPositionsRange(const btCollisionShape& shape, + const btTransform& transform, const TileBounds& bounds, const RecastSettings& settings); - if (minTile.y() > maxTile.y()) - std::swap(minTile.y(), maxTile.y()); + TilesPositionsRange makeTilesPositionsRange(const int cellSize, const btVector3& shift, + const RecastSettings& settings); - for (int tileX = minTile.x(); tileX <= maxTile.x(); ++tileX) - for (int tileY = minTile.y(); tileY <= maxTile.y(); ++tileY) + template + inline void getTilesPositions(const TilesPositionsRange& range, Callback&& callback) + { + for (int tileX = range.mBegin.x(); tileX < range.mEnd.x(); ++tileX) + for (int tileY = range.mBegin.y(); tileY < range.mEnd.y(); ++tileY) callback(TilePosition {tileX, tileY}); } - template - void getTilesPositions(const btCollisionShape& shape, const btTransform& transform, - const Settings& settings, Callback&& callback) + inline bool isInTilesPositionsRange(int begin, int end, int coordinate) { - btVector3 aabbMin; - btVector3 aabbMax; - shape.getAabb(transform, aabbMin, aabbMax); - - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + return begin <= coordinate && coordinate < end; } - template - void getTilesPositions(const int cellSize, const btTransform& transform, - const Settings& settings, Callback&& callback) + inline bool isInTilesPositionsRange(const TilesPositionsRange& range, const TilePosition& position) { - const auto halfCellSize = cellSize / 2; - auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); - auto aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0)); - - aabbMin.setX(std::min(aabbMin.x(), aabbMax.x())); - aabbMin.setY(std::min(aabbMin.y(), aabbMax.y())); - - aabbMax.setX(std::max(aabbMin.x(), aabbMax.x())); - aabbMax.setY(std::max(aabbMin.y(), aabbMax.y())); - - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + return isInTilesPositionsRange(range.mBegin.x(), range.mEnd.x(), position.x()) + && isInTilesPositionsRange(range.mBegin.y(), range.mEnd.y(), position.y()); } + + TilesPositionsRange getIntersection(const TilesPositionsRange& a, const TilesPositionsRange& b) noexcept; } #endif diff --git a/components/detournavigator/heightfieldshape.hpp b/components/detournavigator/heightfieldshape.hpp new file mode 100644 index 0000000000..06770e9b3d --- /dev/null +++ b/components/detournavigator/heightfieldshape.hpp @@ -0,0 +1,44 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H + +#include + +#include + +#include +#include + +namespace DetourNavigator +{ + struct HeightfieldPlane + { + float mHeight; + }; + + struct HeightfieldSurface + { + const float* mHeights; + std::size_t mSize; + float mMinHeight; + float mMaxHeight; + }; + + using HeightfieldShape = std::variant; + + inline btVector3 getHeightfieldShift(const HeightfieldPlane& v, const osg::Vec2i& cellPosition, int cellSize) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mHeight, v.mHeight); + } + + inline btVector3 getHeightfieldShift(const HeightfieldSurface& v, const osg::Vec2i& cellPosition, int cellSize) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mMinHeight, v.mMaxHeight); + } + + inline btVector3 getHeightfieldShift(const HeightfieldShape& v, const osg::Vec2i& cellPosition, int cellSize) + { + return std::visit([&] (const auto& w) { return getHeightfieldShift(w, cellPosition, cellSize); }, v); + } +} + +#endif diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 7c7dcf1864..3b26ad696f 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -1,7 +1,5 @@ #include "makenavmesh.hpp" -#include "chunkytrimesh.hpp" #include "debug.hpp" -#include "dtstatus.hpp" #include "exceptions.hpp" #include "recastmesh.hpp" #include "settings.hpp" @@ -9,8 +7,19 @@ #include "sharednavmesh.hpp" #include "flags.hpp" #include "navmeshtilescache.hpp" +#include "preparednavmeshdata.hpp" +#include "navmeshdata.hpp" +#include "recastmeshbuilder.hpp" +#include "navmeshdb.hpp" +#include "serialization.hpp" +#include "dbrefgeometryobject.hpp" +#include "navmeshdbutils.hpp" +#include "recastparams.hpp" #include +#include +#include +#include #include #include @@ -22,60 +31,18 @@ #include #include #include +#include +namespace DetourNavigator +{ namespace { - using namespace DetourNavigator; - - void initPolyMeshDetail(rcPolyMeshDetail& value) - { - value.meshes = nullptr; - value.verts = nullptr; - value.tris = nullptr; - } - - struct PolyMeshDetailStackDeleter - { - void operator ()(rcPolyMeshDetail* value) const - { - rcFree(value->meshes); - rcFree(value->verts); - rcFree(value->tris); - } - }; - - using PolyMeshDetailStackPtr = std::unique_ptr; - - struct WaterBounds + struct Rectangle { - osg::Vec3f mMin; - osg::Vec3f mMax; + TileBounds mBounds; + float mHeight; }; - WaterBounds getWaterBounds(const RecastMesh::Water& water, const Settings& settings, - const osg::Vec3f& agentHalfExtents) - { - if (water.mCellSize == std::numeric_limits::max()) - { - const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z()); - const auto min = toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(-1, -1, 0)))); - const auto max = toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(1, 1, 0)))); - return WaterBounds { - osg::Vec3f(-std::numeric_limits::max(), min.y(), -std::numeric_limits::max()), - osg::Vec3f(std::numeric_limits::max(), max.y(), std::numeric_limits::max()) - }; - } - else - { - const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z()); - const auto halfCellSize = water.mCellSize / 2.0f; - return WaterBounds { - toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(-halfCellSize, -halfCellSize, 0)))), - toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(halfCellSize, halfCellSize, 0)))) - }; - } - } - std::vector getOffMeshVerts(const std::vector& connections) { std::vector result; @@ -134,177 +101,192 @@ namespace return result; } - rcConfig makeConfig(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax, - const Settings& settings) - { - rcConfig config; - - config.cs = settings.mCellSize; - config.ch = settings.mCellHeight; - config.walkableSlopeAngle = settings.mMaxSlope; - config.walkableHeight = static_cast(std::ceil(getHeight(settings, agentHalfExtents) / config.ch)); - config.walkableClimb = static_cast(std::floor(getMaxClimb(settings) / config.ch)); - config.walkableRadius = static_cast(std::ceil(getRadius(settings, agentHalfExtents) / config.cs)); - config.maxEdgeLen = static_cast(std::round(settings.mMaxEdgeLen / config.cs)); - config.maxSimplificationError = settings.mMaxSimplificationError; - config.minRegionArea = settings.mRegionMinSize * settings.mRegionMinSize; - config.mergeRegionArea = settings.mRegionMergeSize * settings.mRegionMergeSize; - config.maxVertsPerPoly = settings.mMaxVertsPerPoly; - config.detailSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : config.cs * settings.mDetailSampleDist; - config.detailSampleMaxError = config.ch * settings.mDetailSampleMaxError; - config.borderSize = settings.mBorderSize; - config.width = settings.mTileSize + config.borderSize * 2; - config.height = settings.mTileSize + config.borderSize * 2; - rcVcopy(config.bmin, boundsMin.ptr()); - rcVcopy(config.bmax, boundsMax.ptr()); - config.bmin[0] -= getBorderSize(settings); - config.bmin[2] -= getBorderSize(settings); - config.bmax[0] += getBorderSize(settings); - config.bmax[2] += getBorderSize(settings); - config.tileSize = settings.mTileSize; - - return config; + float getHeight(const RecastSettings& settings,const AgentBounds& agentBounds) + { + return getAgentHeight(agentBounds) * settings.mRecastScaleFactor; } - void createHeightfield(rcContext& context, rcHeightfield& solid, int width, int height, const float* bmin, - const float* bmax, const float cs, const float ch) + float getMaxClimb(const RecastSettings& settings) { - const auto result = rcCreateHeightfield(&context, solid, width, height, bmin, bmax, cs, ch); + return settings.mMaxClimb * settings.mRecastScaleFactor; + } - if (!result) - throw NavigatorException("Failed to create heightfield for navmesh"); + float getRadius(const RecastSettings& settings, const AgentBounds& agentBounds) + { + return getAgentRadius(agentBounds) * settings.mRecastScaleFactor; } - bool rasterizeSolidObjectsTriangles(rcContext& context, const RecastMesh& recastMesh, const rcConfig& config, - rcHeightfield& solid) + float getSwimLevel(const RecastSettings& settings, const float waterLevel, const float agentHalfExtentsZ) { - const auto& chunkyMesh = recastMesh.getChunkyTriMesh(); - std::vector areas(chunkyMesh.getMaxTrisPerChunk(), AreaType_null); - const osg::Vec2f tileBoundsMin(config.bmin[0], config.bmin[2]); - const osg::Vec2f tileBoundsMax(config.bmax[0], config.bmax[2]); - bool result = false; + return waterLevel - settings.mSwimHeightScale * agentHalfExtentsZ - agentHalfExtentsZ; + } - chunkyMesh.forEachChunksOverlappingRect(Rect {tileBoundsMin, tileBoundsMax}, - [&] (const std::size_t cid) - { - const auto chunk = chunkyMesh.getChunk(cid); - - std::fill( - areas.begin(), - std::min(areas.begin() + static_cast(chunk.mSize), - areas.end()), - AreaType_null - ); - - rcMarkWalkableTriangles( - &context, - config.walkableSlopeAngle, - recastMesh.getVertices().data(), - static_cast(recastMesh.getVerticesCount()), - chunk.mIndices, - static_cast(chunk.mSize), - areas.data() - ); - - for (std::size_t i = 0; i < chunk.mSize; ++i) - areas[i] = chunk.mAreaTypes[i]; - - rcClearUnwalkableTriangles( - &context, - config.walkableSlopeAngle, - recastMesh.getVertices().data(), - static_cast(recastMesh.getVerticesCount()), - chunk.mIndices, - static_cast(chunk.mSize), - areas.data() - ); - - const auto trianglesRasterized = rcRasterizeTriangles( - &context, - recastMesh.getVertices().data(), - static_cast(recastMesh.getVerticesCount()), - chunk.mIndices, - areas.data(), - static_cast(chunk.mSize), - solid, - config.walkableClimb - ); - - if (!trianglesRasterized) - throw NavigatorException("Failed to create rasterize triangles from recast mesh for navmesh"); - - result = true; - }); + struct RecastParams + { + float mSampleDist = 0; + float mSampleMaxError = 0; + int mMaxEdgeLen = 0; + int mWalkableClimb = 0; + int mWalkableHeight = 0; + int mWalkableRadius = 0; + }; + + RecastParams makeRecastParams(const RecastSettings& settings, const AgentBounds& agentBounds) + { + RecastParams result; + + result.mWalkableHeight = static_cast(std::ceil(getHeight(settings, agentBounds) / settings.mCellHeight)); + result.mWalkableClimb = static_cast(std::floor(getMaxClimb(settings) / settings.mCellHeight)); + result.mWalkableRadius = static_cast(std::ceil(getRadius(settings, agentBounds) / settings.mCellSize)); + result.mMaxEdgeLen = static_cast(std::round(static_cast(settings.mMaxEdgeLen) / settings.mCellSize)); + result.mSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : settings.mCellSize * settings.mDetailSampleDist; + result.mSampleMaxError = settings.mCellHeight * settings.mDetailSampleMaxError; return result; } - void rasterizeWaterTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, - const Settings& settings, const rcConfig& config, rcHeightfield& solid) + void initHeightfield(rcContext& context, const TilePosition& tilePosition, float minZ, float maxZ, + const RecastSettings& settings, rcHeightfield& solid) { - const std::array areas {{AreaType_water, AreaType_water}}; + const int size = settings.mTileSize + settings.mBorderSize * 2; + const int width = size; + const int height = size; + const float halfBoundsSize = size * settings.mCellSize * 0.5f; + const osg::Vec2f shift = osg::Vec2f(tilePosition.x() + 0.5f, tilePosition.y() + 0.5f) * getTileSize(settings); + const osg::Vec3f bmin(shift.x() - halfBoundsSize, minZ, shift.y() - halfBoundsSize); + const osg::Vec3f bmax(shift.x() + halfBoundsSize, maxZ, shift.y() + halfBoundsSize); - for (const auto& water : recastMesh.getWater()) - { - const auto bounds = getWaterBounds(water, settings, agentHalfExtents); - - const osg::Vec2f tileBoundsMin( - std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMin.x())), - std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMin.z())) - ); - const osg::Vec2f tileBoundsMax( - std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMax.x())), - std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMax.z())) - ); - - if (tileBoundsMax == tileBoundsMin) - continue; + const auto result = rcCreateHeightfield(&context, solid, width, height, bmin.ptr(), bmax.ptr(), + settings.mCellSize, settings.mCellHeight); + + if (!result) + throw NavigatorException("Failed to create heightfield for navmesh"); + } + + bool rasterizeTriangles(rcContext& context, const Mesh& mesh, const RecastSettings& settings, + const RecastParams& params, rcHeightfield& solid) + { + std::vector areas(mesh.getAreaTypes().begin(), mesh.getAreaTypes().end()); + std::vector vertices = mesh.getVertices(); - const std::array vertices {{ - osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMin.y()), - osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMax.y()), - osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMax.y()), - osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMin.y()), - }}; - - std::array convertedVertices; - auto convertedVerticesIt = convertedVertices.begin(); - - for (const auto& vertex : vertices) - convertedVerticesIt = std::copy(vertex.ptr(), vertex.ptr() + 3, convertedVerticesIt); - - const std::array indices {{ - 0, 1, 2, - 0, 2, 3, - }}; - - const auto trianglesRasterized = rcRasterizeTriangles( - &context, - convertedVertices.data(), - static_cast(convertedVertices.size() / 3), - indices.data(), - areas.data(), - static_cast(areas.size()), - solid, - config.walkableClimb - ); - - if (!trianglesRasterized) - throw NavigatorException("Failed to create rasterize water triangles for navmesh"); + for (std::size_t i = 0; i < vertices.size(); i += 3) + { + for (std::size_t j = 0; j < 3; ++j) + vertices[i + j] = toNavMeshCoordinates(settings, vertices[i + j]); + std::swap(vertices[i + 1], vertices[i + 2]); } + + rcClearUnwalkableTriangles( + &context, + settings.mMaxSlope, + vertices.data(), + static_cast(mesh.getVerticesCount()), + mesh.getIndices().data(), + static_cast(areas.size()), + areas.data() + ); + + return rcRasterizeTriangles( + &context, + vertices.data(), + static_cast(mesh.getVerticesCount()), + mesh.getIndices().data(), + areas.data(), + static_cast(areas.size()), + solid, + params.mWalkableClimb + ); } - bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, - const rcConfig& config, const Settings& settings, rcHeightfield& solid) + bool rasterizeTriangles(rcContext& context, const Rectangle& rectangle, AreaType areaType, + const RecastParams& params, rcHeightfield& solid) { - if (!rasterizeSolidObjectsTriangles(context, recastMesh, config, solid)) - return false; + const std::array vertices { + rectangle.mBounds.mMin.x(), rectangle.mHeight, rectangle.mBounds.mMin.y(), + rectangle.mBounds.mMin.x(), rectangle.mHeight, rectangle.mBounds.mMax.y(), + rectangle.mBounds.mMax.x(), rectangle.mHeight, rectangle.mBounds.mMax.y(), + rectangle.mBounds.mMax.x(), rectangle.mHeight, rectangle.mBounds.mMin.y(), + }; + + const std::array indices { + 0, 1, 2, + 0, 2, 3, + }; + + const std::array areas {areaType, areaType}; + + return rcRasterizeTriangles( + &context, + vertices.data(), + static_cast(vertices.size() / 3), + indices.data(), + areas.data(), + static_cast(areas.size()), + solid, + params.mWalkableClimb + ); + } - rasterizeWaterTriangles(context, agentHalfExtents, recastMesh, settings, config, solid); + bool rasterizeTriangles(rcContext& context, float agentHalfExtentsZ, const std::vector& water, + const RecastSettings& settings, const RecastParams& params, const TileBounds& realTileBounds, rcHeightfield& solid) + { + for (const CellWater& cellWater : water) + { + const TileBounds cellTileBounds = maxCellTileBounds(cellWater.mCellPosition, cellWater.mWater.mCellSize); + if (auto intersection = getIntersection(realTileBounds, cellTileBounds)) + { + const Rectangle rectangle { + toNavMeshCoordinates(settings, *intersection), + toNavMeshCoordinates(settings, getSwimLevel(settings, cellWater.mWater.mLevel, agentHalfExtentsZ)) + }; + if (!rasterizeTriangles(context, rectangle, AreaType_water, params, solid)) + return false; + } + } + return true; + } + bool rasterizeTriangles(rcContext& context, const TileBounds& realTileBounds, const std::vector& heightfields, + const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid) + { + for (const FlatHeightfield& heightfield : heightfields) + { + const TileBounds cellTileBounds = maxCellTileBounds(heightfield.mCellPosition, heightfield.mCellSize); + if (auto intersection = getIntersection(realTileBounds, cellTileBounds)) + { + const Rectangle rectangle { + toNavMeshCoordinates(settings, *intersection), + toNavMeshCoordinates(settings, heightfield.mHeight) + }; + if (!rasterizeTriangles(context, rectangle, AreaType_ground, params, solid)) + return false; + } + } return true; } + bool rasterizeTriangles(rcContext& context, const std::vector& heightfields, + const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid) + { + for (const Heightfield& heightfield : heightfields) + { + const Mesh mesh = makeMesh(heightfield); + if (!rasterizeTriangles(context, mesh, settings, params, solid)) + return false; + } + return true; + } + + bool rasterizeTriangles(rcContext& context, const TilePosition& tilePosition, float agentHalfExtentsZ, + const RecastMesh& recastMesh, const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid) + { + const TileBounds realTileBounds = makeRealTileBoundsWithBorder(settings, tilePosition); + return rasterizeTriangles(context, recastMesh.getMesh(), settings, params, solid) + && rasterizeTriangles(context, agentHalfExtentsZ, recastMesh.getWater(), settings, params, realTileBounds, solid) + && rasterizeTriangles(context, recastMesh.getHeightfields(), settings, params, solid) + && rasterizeTriangles(context, realTileBounds, recastMesh.getFlatHeightfields(), settings, params, solid); + } + void buildCompactHeightfield(rcContext& context, const int walkableHeight, const int walkableClimb, rcHeightfield& solid, rcCompactHeightfield& compact) { @@ -373,75 +355,135 @@ namespace polyMesh.flags[i] = getFlag(static_cast(polyMesh.areas[i])); } - bool fillPolyMesh(rcContext& context, const rcConfig& config, rcHeightfield& solid, rcPolyMesh& polyMesh, - rcPolyMeshDetail& polyMeshDetail) + bool fillPolyMesh(rcContext& context, const RecastSettings& settings, const RecastParams& params, + rcHeightfield& solid, rcPolyMesh& polyMesh, rcPolyMeshDetail& polyMeshDetail) { rcCompactHeightfield compact; - buildCompactHeightfield(context, config.walkableHeight, config.walkableClimb, solid, compact); + buildCompactHeightfield(context, params.mWalkableHeight, params.mWalkableClimb, solid, compact); - erodeWalkableArea(context, config.walkableRadius, compact); + erodeWalkableArea(context, params.mWalkableRadius, compact); buildDistanceField(context, compact); - buildRegions(context, compact, config.borderSize, config.minRegionArea, config.mergeRegionArea); + buildRegions(context, compact, settings.mBorderSize, settings.mRegionMinArea, settings.mRegionMergeArea); rcContourSet contourSet; - buildContours(context, compact, config.maxSimplificationError, config.maxEdgeLen, contourSet); + buildContours(context, compact, settings.mMaxSimplificationError, params.mMaxEdgeLen, contourSet); if (contourSet.nconts == 0) return false; - buildPolyMesh(context, contourSet, config.maxVertsPerPoly, polyMesh); + buildPolyMesh(context, contourSet, settings.mMaxVertsPerPoly, polyMesh); - buildPolyMeshDetail(context, polyMesh, compact, config.detailSampleDist, config.detailSampleMaxError, - polyMeshDetail); + buildPolyMeshDetail(context, polyMesh, compact, params.mSampleDist, params.mSampleMaxError, polyMeshDetail); setPolyMeshFlags(polyMesh); return true; } - NavMeshData makeNavMeshTileData(const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, - const std::vector& offMeshConnections, const TilePosition& tile, - const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax, const Settings& settings) + template + unsigned long getMinValuableBitsNumber(const T value) + { + unsigned long power = 0; + while (power < sizeof(T) * 8 && (static_cast(1) << power) < value) + ++power; + return power; + } + + std::pair getBoundsByZ(const RecastMesh& recastMesh, float agentHalfExtentsZ, const RecastSettings& settings) + { + float minZ = 0; + float maxZ = 0; + + const std::vector& vertices = recastMesh.getMesh().getVertices(); + for (std::size_t i = 0, n = vertices.size(); i < n; i += 3) + { + minZ = std::min(minZ, vertices[i + 2]); + maxZ = std::max(maxZ, vertices[i + 2]); + } + + for (const CellWater& water : recastMesh.getWater()) + { + const float swimLevel = getSwimLevel(settings, water.mWater.mLevel, agentHalfExtentsZ); + minZ = std::min(minZ, swimLevel); + maxZ = std::max(maxZ, swimLevel); + } + + for (const Heightfield& heightfield : recastMesh.getHeightfields()) + { + if (heightfield.mHeights.empty()) + continue; + const auto [minHeight, maxHeight] = std::minmax_element(heightfield.mHeights.begin(), heightfield.mHeights.end()); + minZ = std::min(minZ, *minHeight); + maxZ = std::max(maxZ, *maxHeight); + } + + for (const FlatHeightfield& heightfield : recastMesh.getFlatHeightfields()) + { + minZ = std::min(minZ, heightfield.mHeight); + maxZ = std::max(maxZ, heightfield.mHeight); + } + + return {minZ, maxZ}; + } +} +} // namespace DetourNavigator + +namespace DetourNavigator +{ + std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, + const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings) { rcContext context; - const auto config = makeConfig(agentHalfExtents, boundsMin, boundsMax, settings); + + const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentBounds.mHalfExtents.z(), settings); rcHeightfield solid; - createHeightfield(context, solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch); + initHeightfield(context, tilePosition, toNavMeshCoordinates(settings, minZ), + toNavMeshCoordinates(settings, maxZ), settings, solid); - if (!rasterizeTriangles(context, agentHalfExtents, recastMesh, config, settings, solid)) - return NavMeshData(); + const RecastParams params = makeRecastParams(settings, agentBounds); - rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, solid); - rcFilterLedgeSpans(&context, config.walkableHeight, config.walkableClimb, solid); - rcFilterWalkableLowHeightSpans(&context, config.walkableHeight, solid); + if (!rasterizeTriangles(context, tilePosition, agentBounds.mHalfExtents.z(), recastMesh, settings, params, solid)) + return nullptr; - rcPolyMesh polyMesh; - rcPolyMeshDetail polyMeshDetail; - initPolyMeshDetail(polyMeshDetail); - const PolyMeshDetailStackPtr polyMeshDetailPtr(&polyMeshDetail); - if (!fillPolyMesh(context, config, solid, polyMesh, polyMeshDetail)) - return NavMeshData(); + rcFilterLowHangingWalkableObstacles(&context, params.mWalkableClimb, solid); + rcFilterLedgeSpans(&context, params.mWalkableHeight, params.mWalkableClimb, solid); + rcFilterWalkableLowHeightSpans(&context, params.mWalkableHeight, solid); + std::unique_ptr result = std::make_unique(); + + if (!fillPolyMesh(context, settings, params, solid, result->mPolyMesh, result->mPolyMeshDetail)) + return nullptr; + + result->mCellSize = settings.mCellSize; + result->mCellHeight = settings.mCellHeight; + + return result; + } + + NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data, + const std::vector& offMeshConnections, const AgentBounds& agentBounds, + const TilePosition& tile, const RecastSettings& settings) + { const auto offMeshConVerts = getOffMeshVerts(offMeshConnections); - const std::vector offMeshConRad(offMeshConnections.size(), getRadius(settings, agentHalfExtents)); - const std::vector offMeshConDir(offMeshConnections.size(), DT_OFFMESH_CON_BIDIR); + const std::vector offMeshConRad(offMeshConnections.size(), getRadius(settings, agentBounds)); + const std::vector offMeshConDir(offMeshConnections.size(), 0); const std::vector offMeshConAreas = getOffMeshConAreas(offMeshConnections); const std::vector offMeshConFlags = getOffMeshFlags(offMeshConnections); dtNavMeshCreateParams params; - params.verts = polyMesh.verts; - params.vertCount = polyMesh.nverts; - params.polys = polyMesh.polys; - params.polyAreas = polyMesh.areas; - params.polyFlags = polyMesh.flags; - params.polyCount = polyMesh.npolys; - params.nvp = polyMesh.nvp; - params.detailMeshes = polyMeshDetail.meshes; - params.detailVerts = polyMeshDetail.verts; - params.detailVertsCount = polyMeshDetail.nverts; - params.detailTris = polyMeshDetail.tris; - params.detailTriCount = polyMeshDetail.ntris; + params.verts = data.mPolyMesh.verts; + params.vertCount = data.mPolyMesh.nverts; + params.polys = data.mPolyMesh.polys; + params.polyAreas = data.mPolyMesh.areas; + params.polyFlags = data.mPolyMesh.flags; + params.polyCount = data.mPolyMesh.npolys; + params.nvp = data.mPolyMesh.nvp; + params.detailMeshes = data.mPolyMeshDetail.meshes; + params.detailVerts = data.mPolyMeshDetail.verts; + params.detailVertsCount = data.mPolyMeshDetail.nverts; + params.detailTris = data.mPolyMeshDetail.tris; + params.detailTriCount = data.mPolyMeshDetail.ntris; params.offMeshConVerts = offMeshConVerts.data(); params.offMeshConRad = offMeshConRad.data(); params.offMeshConDir = offMeshConDir.data(); @@ -449,15 +491,15 @@ namespace params.offMeshConFlags = offMeshConFlags.data(); params.offMeshConUserID = nullptr; params.offMeshConCount = static_cast(offMeshConnections.size()); - params.walkableHeight = getHeight(settings, agentHalfExtents); - params.walkableRadius = getRadius(settings, agentHalfExtents); + params.walkableHeight = getHeight(settings, agentBounds); + params.walkableRadius = getRadius(settings, agentBounds); params.walkableClimb = getMaxClimb(settings); - rcVcopy(params.bmin, polyMesh.bmin); - rcVcopy(params.bmax, polyMesh.bmax); - params.cs = config.cs; - params.ch = config.ch; + rcVcopy(params.bmin, data.mPolyMesh.bmin); + rcVcopy(params.bmax, data.mPolyMesh.bmax); + params.cs = data.mCellSize; + params.ch = data.mCellHeight; params.buildBvTree = true; - params.userId = 0; + params.userId = data.mUserId; params.tileX = tile.x(); params.tileY = tile.y(); params.tileLayer = 0; @@ -472,26 +514,12 @@ namespace return NavMeshData(navMeshData, navMeshDataSize); } - - - template - unsigned long getMinValuableBitsNumber(const T value) - { - unsigned long power = 0; - while (power < sizeof(T) * 8 && (static_cast(1) << power) < value) - ++power; - return power; - } -} - -namespace DetourNavigator -{ NavMeshPtr makeEmptyNavMesh(const Settings& settings) { // Max tiles and max polys affect how the tile IDs are caculated. // There are 22 bits available for identifying a tile and a polygon. const int polysAndTilesBits = 22; - const auto polysBits = getMinValuableBitsNumber(settings.mMaxPolys); + const auto polysBits = getMinValuableBitsNumber(settings.mDetour.mMaxPolys); if (polysBits >= polysAndTilesBits) throw InvalidArgument("Too many polygons per tile"); @@ -500,12 +528,16 @@ namespace DetourNavigator dtNavMeshParams params; std::fill_n(params.orig, 3, 0.0f); - params.tileWidth = settings.mTileSize * settings.mCellSize; - params.tileHeight = settings.mTileSize * settings.mCellSize; + params.tileWidth = settings.mRecast.mTileSize * settings.mRecast.mCellSize; + params.tileHeight = settings.mRecast.mTileSize * settings.mRecast.mCellSize; params.maxTiles = 1 << tilesBits; params.maxPolys = 1 << polysBits; NavMeshPtr navMesh(dtAllocNavMesh(), &dtFreeNavMesh); + + if (navMesh == nullptr) + throw NavigatorException("Failed to allocate navmesh"); + const auto status = navMesh->init(¶ms); if (!dtStatusSucceed(status)) @@ -513,90 +545,4 @@ namespace DetourNavigator return navMesh; } - - UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, - const TilePosition& changedTile, const TilePosition& playerTile, - const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache) - { - Log(Debug::Debug) << std::fixed << std::setprecision(2) << - "Update NavMesh with multiple tiles:" << - " agentHeight=" << getHeight(settings, agentHalfExtents) << - " agentMaxClimb=" << getMaxClimb(settings) << - " agentRadius=" << getRadius(settings, agentHalfExtents) << - " changedTile=(" << changedTile << ")" << - " playerTile=(" << playerTile << ")" << - " changedTileDistance=" << getDistance(changedTile, playerTile); - - const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - const osg::Vec3f origin(params.orig[0], params.orig[1], params.orig[2]); - - if (!recastMesh) - { - Log(Debug::Debug) << "Ignore add tile: recastMesh is null"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - auto recastMeshBounds = recastMesh->getBounds(); - - for (const auto& water : recastMesh->getWater()) - { - const auto waterBounds = getWaterBounds(water, settings, agentHalfExtents); - recastMeshBounds.mMin.y() = std::min(recastMeshBounds.mMin.y(), waterBounds.mMin.y()); - recastMeshBounds.mMax.y() = std::max(recastMeshBounds.mMax.y(), waterBounds.mMax.y()); - } - - if (isEmpty(recastMeshBounds)) - { - Log(Debug::Debug) << "Ignore add tile: recastMesh is empty"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - if (!shouldAddTile(changedTile, playerTile, std::min(settings.mMaxTilesNumber, params.maxTiles))) - { - Log(Debug::Debug) << "Ignore add tile: too far from player"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh, offMeshConnections); - bool cached = static_cast(cachedNavMeshData); - - if (!cachedNavMeshData) - { - const auto tileBounds = makeTileBounds(settings, changedTile); - const osg::Vec3f tileBorderMin(tileBounds.mMin.x(), recastMeshBounds.mMin.y() - 1, tileBounds.mMin.y()); - const osg::Vec3f tileBorderMax(tileBounds.mMax.x(), recastMeshBounds.mMax.y() + 1, tileBounds.mMax.y()); - - auto navMeshData = makeNavMeshTileData(agentHalfExtents, *recastMesh, offMeshConnections, changedTile, - tileBorderMin, tileBorderMax, settings); - - if (!navMeshData.mValue) - { - Log(Debug::Debug) << "Ignore add tile: NavMeshData is null"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - try - { - cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, - offMeshConnections, std::move(navMeshData)); - } - catch (const InvalidArgument&) - { - cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh, - offMeshConnections); - cached = static_cast(cachedNavMeshData); - } - - if (!cachedNavMeshData) - { - Log(Debug::Debug) << "Navigator cache overflow"; - return navMeshCacheItem->lock()->updateTile(changedTile, std::move(navMeshData)); - } - } - - const auto updateStatus = navMeshCacheItem->lock()->updateTile(changedTile, std::move(cachedNavMeshData)); - - return UpdateNavMeshStatusBuilder(updateStatus).cached(cached).getResult(); - } } diff --git a/components/detournavigator/makenavmesh.hpp b/components/detournavigator/makenavmesh.hpp index f9cf68a736..b890c5fcff 100644 --- a/components/detournavigator/makenavmesh.hpp +++ b/components/detournavigator/makenavmesh.hpp @@ -2,23 +2,29 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_MAKENAVMESH_H #include "offmeshconnectionsmanager.hpp" -#include "settings.hpp" #include "navmeshcacheitem.hpp" #include "tileposition.hpp" -#include "tilebounds.hpp" #include "sharednavmesh.hpp" #include "navmeshtilescache.hpp" +#include "offmeshconnection.hpp" +#include "navmeshdb.hpp" + +#include #include #include +#include class dtNavMesh; +struct rcConfig; namespace DetourNavigator { class RecastMesh; struct Settings; + struct PreparedNavMeshData; + struct NavMeshData; inline float getLength(const osg::Vec2i& value) { @@ -36,12 +42,22 @@ namespace DetourNavigator return expectedTilesCount <= maxTiles; } - NavMeshPtr makeEmptyNavMesh(const Settings& settings); + inline bool isEmpty(const RecastMesh& recastMesh) + { + return recastMesh.getMesh().getIndices().empty() + && recastMesh.getWater().empty() + && recastMesh.getHeightfields().empty() + && recastMesh.getFlatHeightfields().empty(); + } - UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, - const TilePosition& changedTile, const TilePosition& playerTile, - const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache); + std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, + const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings); + + NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data, + const std::vector& offMeshConnections, const AgentBounds& agentBounds, + const TilePosition& tile, const RecastSettings& settings); + + NavMeshPtr makeEmptyNavMesh(const Settings& settings); } #endif diff --git a/components/detournavigator/navigator.cpp b/components/detournavigator/navigator.cpp index 658e539ad9..d40f330771 100644 --- a/components/detournavigator/navigator.cpp +++ b/components/detournavigator/navigator.cpp @@ -1,20 +1,34 @@ -#include "findrandompointaroundcircle.hpp" #include "navigator.hpp" +#include "navigatorimpl.hpp" +#include "navigatorstub.hpp" +#include "recastglobalallocator.hpp" + +#include namespace DetourNavigator { - std::optional Navigator::findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const + std::unique_ptr makeNavigator(const Settings& settings, const std::string& userDataPath) + { + DetourNavigator::RecastGlobalAllocator::init(); + + std::unique_ptr db; + if (settings.mEnableNavMeshDiskCache) + { + try + { + db = std::make_unique(userDataPath + "/navmesh.db", settings.mMaxDbFileSize); + } + catch (const std::exception& e) + { + Log(Debug::Error) << e.what() << ", navigation mesh disk cache will be disabled"; + } + } + + return std::make_unique(settings, std::move(db)); + } + + std::unique_ptr makeNavigatorStub() { - const auto navMesh = getNavMesh(agentHalfExtents); - if (!navMesh) - return std::optional(); - const auto settings = getSettings(); - const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(), - toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, maxRadius), includeFlags, settings); - if (!result) - return std::optional(); - return std::optional(fromNavMeshCoordinates(settings, *result)); + return std::make_unique(); } } diff --git a/components/detournavigator/navigator.hpp b/components/detournavigator/navigator.hpp index a79aa59d42..df2b202cdd 100644 --- a/components/detournavigator/navigator.hpp +++ b/components/detournavigator/navigator.hpp @@ -1,13 +1,16 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H -#include "findsmoothpath.hpp" -#include "flags.hpp" -#include "settings.hpp" #include "objectid.hpp" #include "navmeshcacheitem.hpp" -#include "recastmesh.hpp" #include "recastmeshtiles.hpp" +#include "waitconditiontype.hpp" +#include "heightfieldshape.hpp" +#include "objecttransform.hpp" + +#include + +#include namespace ESM { @@ -15,16 +18,27 @@ namespace ESM struct Pathgrid; } +namespace Loading +{ + class Listener; +} + namespace DetourNavigator { + struct Settings; + struct AgentBounds; + struct ObjectShapes { - const btCollisionShape& mShape; - const btCollisionShape* mAvoid; + osg::ref_ptr mShapeInstance; + ObjectTransform mTransform; - ObjectShapes(const btCollisionShape& shape, const btCollisionShape* avoid) - : mShape(shape), mAvoid(avoid) - {} + ObjectShapes(const osg::ref_ptr& shapeInstance, const ObjectTransform& transform) + : mShapeInstance(shapeInstance) + , mTransform(transform) + { + assert(mShapeInstance != nullptr); + } }; struct DoorShapes : ObjectShapes @@ -32,9 +46,9 @@ namespace DetourNavigator osg::Vec3f mConnectionStart; osg::Vec3f mConnectionEnd; - DoorShapes(const btCollisionShape& shape, const btCollisionShape* avoid, - const osg::Vec3f& connectionStart,const osg::Vec3f& connectionEnd) - : ObjectShapes(shape, avoid) + DoorShapes(const osg::ref_ptr& shapeInstance, + const ObjectTransform& transform, const osg::Vec3f& connectionStart, const osg::Vec3f& connectionEnd) + : ObjectShapes(shapeInstance, transform) , mConnectionStart(connectionStart) , mConnectionEnd(connectionEnd) {} @@ -53,25 +67,28 @@ namespace DetourNavigator /** * @brief addAgent should be called for each agent even if all of them has same half extents. - * @param agentHalfExtents allows to setup bounding cylinder for each agent, for each different half extents + * @param agentBounds allows to setup bounding cylinder for each agent, for each different half extents * there is different navmesh. */ - virtual void addAgent(const osg::Vec3f& agentHalfExtents) = 0; + virtual void addAgent(const AgentBounds& agentBounds) = 0; /** * @brief removeAgent should be called for each agent even if all of them has same half extents - * @param agentHalfExtents allows determine which agent to remove + * @param agentBounds allows determine which agent to remove */ - virtual void removeAgent(const osg::Vec3f& agentHalfExtents) = 0; + virtual void removeAgent(const AgentBounds& agentBounds) = 0; /** - * @brief addObject is used to add object represented by single btCollisionShape and btTransform. - * @param id is used to distinguish different objects. - * @param shape must live until object is updated by another shape removed from Navigator. - * @param transform allows to setup object geometry according to its world state. - * @return true if object is added, false if there is already object with given id. + * @brief setWorldspace should be called before adding object from new worldspace + * @param worldspace + */ + virtual void setWorldspace(std::string_view worldspace) = 0; + + /** + * @brief updateBounds should be called before adding object from loading cell + * @param playerPosition corresponds to the bounds center */ - virtual bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) = 0; + virtual void updateBounds(const osg::Vec3f& playerPosition) = 0; /** * @brief addObject is used to add complex object with allowed to walk and avoided to walk shapes @@ -91,15 +108,6 @@ namespace DetourNavigator */ virtual bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) = 0; - /** - * @brief updateObject replace object geometry by given data. - * @param id is used to find object. - * @param shape must live until object is updated by another shape removed from Navigator. - * @param transform allows to setup objects geometry according to its world state. - * @return true if object is updated, false if there is no object with given id. - */ - virtual bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) = 0; - /** * @brief updateObject replace object geometry by given data. * @param id is used to find object. @@ -129,14 +137,12 @@ namespace DetourNavigator * @brief addWater is used to set water level at given world cell. * @param cellPosition allows to distinguish cells if there is many in current world. * @param cellSize set cell borders. std::numeric_limits::max() disables cell borders. - * @param level set z coordinate of water surface at the scene. - * @param transform set global shift of cell center. + * @param shift set global shift of cell center. * @return true if there was no water at given cell if cellSize != std::numeric_limits::max() or there is * at least single object is added to the scene, false if there is already water for given cell or there is no * any other objects. */ - virtual bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) = 0; + virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) = 0; /** * @brief removeWater to make it no more available at the scene. @@ -145,86 +151,61 @@ namespace DetourNavigator */ virtual bool removeWater(const osg::Vec2i& cellPosition) = 0; + virtual bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) = 0; + + virtual bool removeHeightfield(const osg::Vec2i& cellPosition) = 0; + virtual void addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) = 0; virtual void removePathgrid(const ESM::Pathgrid& pathgrid) = 0; /** - * @brief update start background navmesh update using current scene state. + * @brief update starts background navmesh update using current scene state. * @param playerPosition setup initial point to order build tiles of navmesh. */ virtual void update(const osg::Vec3f& playerPosition) = 0; /** - * @brief disable navigator updates + * @brief updatePlayerPosition starts background navmesh update using current scene state only when player position has been changed. + * @param playerPosition setup initial point to order build tiles of navmesh. */ - virtual void setUpdatesEnabled(bool enabled) = 0; + virtual void updatePlayerPosition(const osg::Vec3f& playerPosition) = 0; /** - * @brief wait locks thread until all tiles are updated from last update call. + * @brief disable navigator updates */ - virtual void wait() = 0; + virtual void setUpdatesEnabled(bool enabled) = 0; /** - * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. - * @param agentHalfExtents allows to find navmesh for given actor. - * @param start path from given point. - * @param end path at given point. - * @param includeFlags setup allowed surfaces for actor to walk. - * @param out the beginning of the destination range. - * @return Output iterator to the element in the destination range, one past the last element of found path. - * Equal to out if no path is found. + * @brief wait locks thread until tiles are updated from last update call based on passed condition type. + * @param waitConditionType defines when waiting will stop */ - template - Status findPath(const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start, - const osg::Vec3f& end, const Flags includeFlags, const DetourNavigator::AreaCosts& areaCosts, - OutputIterator& out) const - { - static_assert( - std::is_same< - typename std::iterator_traits::iterator_category, - std::output_iterator_tag - >::value, - "out is not an OutputIterator" - ); - const auto navMesh = getNavMesh(agentHalfExtents); - if (!navMesh) - return Status::NavMeshNotFound; - const auto settings = getSettings(); - return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings, agentHalfExtents), - toNavMeshCoordinates(settings, stepSize), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, out); - } + virtual void wait(Loading::Listener& listener, WaitConditionType waitConditionType) = 0; /** * @brief getNavMesh returns navmesh for specific agent half extents * @return navmesh */ - virtual SharedNavMeshCacheItem getNavMesh(const osg::Vec3f& agentHalfExtents) const = 0; + virtual SharedNavMeshCacheItem getNavMesh(const AgentBounds& agentBounds) const = 0; /** * @brief getNavMeshes returns all current navmeshes * @return map of agent half extents to navmesh */ - virtual std::map getNavMeshes() const = 0; + virtual std::map getNavMeshes() const = 0; virtual const Settings& getSettings() const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; - /** - * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. - * @param agentHalfExtents allows to find navmesh for given actor. - * @param start path from given point. - * @param maxRadius limit maximum distance from start. - * @param includeFlags setup allowed surfaces for actor to walk. - * @return not empty optional with position if point is found and empty optional if point is not found. - */ - std::optional findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const; + virtual RecastMeshTiles getRecastMeshTiles() const = 0; - virtual RecastMeshTiles getRecastMeshTiles() = 0; + virtual float getMaxNavmeshAreaRealRadius() const = 0; }; + + std::unique_ptr makeNavigator(const Settings& settings, const std::string& userDataPath); + + std::unique_ptr makeNavigatorStub(); } #endif diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index c47cf97662..e1b2df55ed 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -2,47 +2,56 @@ #include "debug.hpp" #include "settingsutils.hpp" -#include +#include +#include #include - -#include +#include namespace DetourNavigator { - NavigatorImpl::NavigatorImpl(const Settings& settings) + NavigatorImpl::NavigatorImpl(const Settings& settings, std::unique_ptr&& db) : mSettings(settings) - , mNavMeshManager(mSettings) + , mNavMeshManager(mSettings, std::move(db)) , mUpdatesEnabled(true) { } - void NavigatorImpl::addAgent(const osg::Vec3f& agentHalfExtents) + void NavigatorImpl::addAgent(const AgentBounds& agentBounds) { - ++mAgents[agentHalfExtents]; - mNavMeshManager.addAgent(agentHalfExtents); + if(agentBounds.mHalfExtents.length2() <= 0) + return; + ++mAgents[agentBounds]; + mNavMeshManager.addAgent(agentBounds); } - void NavigatorImpl::removeAgent(const osg::Vec3f& agentHalfExtents) + void NavigatorImpl::removeAgent(const AgentBounds& agentBounds) { - const auto it = mAgents.find(agentHalfExtents); + const auto it = mAgents.find(agentBounds); if (it == mAgents.end()) return; if (it->second > 0) --it->second; } - bool NavigatorImpl::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) + void NavigatorImpl::setWorldspace(std::string_view worldspace) + { + mNavMeshManager.setWorldspace(worldspace); + } + + void NavigatorImpl::updateBounds(const osg::Vec3f& playerPosition) { - return mNavMeshManager.addObject(id, shape, transform, AreaType_ground); + mNavMeshManager.updateBounds(playerPosition); } bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - bool result = addObject(id, shapes.mShape, transform); - if (shapes.mAvoid) + const CollisionShape collisionShape(shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape, shapes.mTransform); + bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground); + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { - const ObjectId avoidId(shapes.mAvoid); - if (mNavMeshManager.addObject(avoidId, *shapes.mAvoid, transform, AreaType_null)) + const ObjectId avoidId(avoidShape); + const CollisionShape avoidCollisionShape(shapes.mShapeInstance, *avoidShape, shapes.mTransform); + if (mNavMeshManager.addObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; @@ -55,29 +64,24 @@ namespace DetourNavigator { if (addObject(id, static_cast(shapes), transform)) { - mNavMeshManager.addOffMeshConnection( - id, - toNavMeshCoordinates(mSettings, shapes.mConnectionStart), - toNavMeshCoordinates(mSettings, shapes.mConnectionEnd), - AreaType_door - ); + const osg::Vec3f start = toNavMeshCoordinates(mSettings.mRecast, shapes.mConnectionStart); + const osg::Vec3f end = toNavMeshCoordinates(mSettings.mRecast, shapes.mConnectionEnd); + mNavMeshManager.addOffMeshConnection(id, start, end, AreaType_door); + mNavMeshManager.addOffMeshConnection(id, end, start, AreaType_door); return true; } return false; } - bool NavigatorImpl::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) - { - return mNavMeshManager.updateObject(id, shape, transform, AreaType_ground); - } - bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - bool result = updateObject(id, shapes.mShape, transform); - if (shapes.mAvoid) + const CollisionShape collisionShape(shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape, shapes.mTransform); + bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground); + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { - const ObjectId avoidId(shapes.mAvoid); - if (mNavMeshManager.updateObject(avoidId, *shapes.mAvoid, transform, AreaType_null)) + const ObjectId avoidId(avoidShape); + const CollisionShape avoidCollisionShape(shapes.mShapeInstance, *avoidShape, shapes.mTransform); + if (mNavMeshManager.updateObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; @@ -104,11 +108,9 @@ namespace DetourNavigator return result; } - bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) + bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - return mNavMeshManager.addWater(cellPosition, cellSize, - btTransform(transform.getBasis(), btVector3(transform.getOrigin().x(), transform.getOrigin().y(), level))); + return mNavMeshManager.addWater(cellPosition, cellSize, level); } bool NavigatorImpl::removeWater(const osg::Vec2i& cellPosition) @@ -116,6 +118,16 @@ namespace DetourNavigator return mNavMeshManager.removeWater(cellPosition); } + bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) + { + return mNavMeshManager.addHeightfield(cellPosition, cellSize, shape); + } + + bool NavigatorImpl::removeHeightfield(const osg::Vec2i& cellPosition) + { + return mNavMeshManager.removeHeightfield(cellPosition); + } + void NavigatorImpl::addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) { Misc::CoordinateConverter converter(&cell); @@ -125,8 +137,8 @@ namespace DetourNavigator const auto dst = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid.mPoints[edge.mV1])); mNavMeshManager.addOffMeshConnection( ObjectId(&pathgrid), - toNavMeshCoordinates(mSettings, src), - toNavMeshCoordinates(mSettings, dst), + toNavMeshCoordinates(mSettings.mRecast, src), + toNavMeshCoordinates(mSettings.mRecast, dst), AreaType_pathgrid ); } @@ -146,22 +158,32 @@ namespace DetourNavigator mNavMeshManager.update(playerPosition, v.first); } + void NavigatorImpl::updatePlayerPosition(const osg::Vec3f& playerPosition) + { + const TilePosition tilePosition = getTilePosition(mSettings.mRecast, toNavMeshCoordinates(mSettings.mRecast, playerPosition)); + if (mLastPlayerPosition.has_value() && *mLastPlayerPosition == tilePosition) + return; + mNavMeshManager.updateBounds(playerPosition); + update(playerPosition); + mLastPlayerPosition = tilePosition; + } + void NavigatorImpl::setUpdatesEnabled(bool enabled) { mUpdatesEnabled = enabled; } - void NavigatorImpl::wait() + void NavigatorImpl::wait(Loading::Listener& listener, WaitConditionType waitConditionType) { - mNavMeshManager.wait(); + mNavMeshManager.wait(listener, waitConditionType); } - SharedNavMeshCacheItem NavigatorImpl::getNavMesh(const osg::Vec3f& agentHalfExtents) const + SharedNavMeshCacheItem NavigatorImpl::getNavMesh(const AgentBounds& agentBounds) const { - return mNavMeshManager.getNavMesh(agentHalfExtents); + return mNavMeshManager.getNavMesh(agentBounds); } - std::map NavigatorImpl::getNavMeshes() const + std::map NavigatorImpl::getNavMeshes() const { return mNavMeshManager.getNavMeshes(); } @@ -176,7 +198,7 @@ namespace DetourNavigator mNavMeshManager.reportStats(frameNumber, stats); } - RecastMeshTiles NavigatorImpl::getRecastMeshTiles() + RecastMeshTiles NavigatorImpl::getRecastMeshTiles() const { return mNavMeshManager.getRecastMeshTiles(); } @@ -211,4 +233,10 @@ namespace DetourNavigator ++it; } } + + float NavigatorImpl::getMaxNavmeshAreaRealRadius() const + { + const auto& settings = getSettings(); + return getRealTileSize(settings.mRecast) * getMaxNavmeshAreaRadius(settings); + } } diff --git a/components/detournavigator/navigatorimpl.hpp b/components/detournavigator/navigatorimpl.hpp index e197c71b78..f7b2d6d8c8 100644 --- a/components/detournavigator/navigatorimpl.hpp +++ b/components/detournavigator/navigatorimpl.hpp @@ -5,6 +5,7 @@ #include "navmeshmanager.hpp" #include +#include namespace DetourNavigator { @@ -15,56 +16,64 @@ namespace DetourNavigator * @brief Navigator constructor initializes all internal data. Constructed object is ready to build a scene. * @param settings allows to customize navigator work. Constructor is only place to set navigator settings. */ - explicit NavigatorImpl(const Settings& settings); + explicit NavigatorImpl(const Settings& settings, std::unique_ptr&& db); - void addAgent(const osg::Vec3f& agentHalfExtents) override; + void addAgent(const AgentBounds& agentBounds) override; - void removeAgent(const osg::Vec3f& agentHalfExtents) override; + void removeAgent(const AgentBounds& agentBounds) override; - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) override; + void setWorldspace(std::string_view worldspace) override; + + void updateBounds(const osg::Vec3f& playerPosition) override; bool addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) override; bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) override; - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) override; - bool updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) override; bool updateObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) override; bool removeObject(const ObjectId id) override; - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) override; + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) override; bool removeWater(const osg::Vec2i& cellPosition) override; + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) override; + + bool removeHeightfield(const osg::Vec2i& cellPosition) override; + void addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) override; void removePathgrid(const ESM::Pathgrid& pathgrid) override; void update(const osg::Vec3f& playerPosition) override; + void updatePlayerPosition(const osg::Vec3f& playerPosition) override; + void setUpdatesEnabled(bool enabled) override; - void wait() override; + void wait(Loading::Listener& listener, WaitConditionType waitConditionType) override; - SharedNavMeshCacheItem getNavMesh(const osg::Vec3f& agentHalfExtents) const override; + SharedNavMeshCacheItem getNavMesh(const AgentBounds& agentBounds) const override; - std::map getNavMeshes() const override; + std::map getNavMeshes() const override; const Settings& getSettings() const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; - RecastMeshTiles getRecastMeshTiles() override; + RecastMeshTiles getRecastMeshTiles() const override; + + float getMaxNavmeshAreaRealRadius() const override; private: Settings mSettings; NavMeshManager mNavMeshManager; bool mUpdatesEnabled; - std::map mAgents; + std::optional mLastPlayerPosition; + std::map mAgents; std::unordered_map mAvoidIds; std::unordered_map mWaterIds; diff --git a/components/detournavigator/navigatorstub.hpp b/components/detournavigator/navigatorstub.hpp index 8b81bde197..1eb1860217 100644 --- a/components/detournavigator/navigatorstub.hpp +++ b/components/detournavigator/navigatorstub.hpp @@ -2,6 +2,12 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORSTUB_H #include "navigator.hpp" +#include "settings.hpp" + +namespace Loading +{ + class Listener; +} namespace DetourNavigator { @@ -10,52 +16,53 @@ namespace DetourNavigator public: NavigatorStub() = default; - void addAgent(const osg::Vec3f& /*agentHalfExtents*/) override {} + void addAgent(const AgentBounds& /*agentBounds*/) override {} - void removeAgent(const osg::Vec3f& /*agentHalfExtents*/) override {} + void removeAgent(const AgentBounds& /*agentBounds*/) override {} - bool addObject(const ObjectId /*id*/, const btCollisionShape& /*shape*/, const btTransform& /*transform*/) override + void setWorldspace(std::string_view /*worldspace*/) override {} + + bool addObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; } - bool addObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override + bool addObject(const ObjectId /*id*/, const DoorShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; } - bool addObject(const ObjectId /*id*/, const DoorShapes& /*shapes*/, const btTransform& /*transform*/) override + bool updateObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; } - bool updateObject(const ObjectId /*id*/, const btCollisionShape& /*shape*/, const btTransform& /*transform*/) override + bool updateObject(const ObjectId /*id*/, const DoorShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; } - bool updateObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override + bool removeObject(const ObjectId /*id*/) override { return false; } - bool updateObject(const ObjectId /*id*/, const DoorShapes& /*shapes*/, const btTransform& /*transform*/) override + bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, float /*level*/) override { return false; } - bool removeObject(const ObjectId /*id*/) override + bool removeWater(const osg::Vec2i& /*cellPosition*/) override { return false; } - bool addWater(const osg::Vec2i& /*cellPosition*/, const int /*cellSize*/, const btScalar /*level*/, - const btTransform& /*transform*/) override + bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const HeightfieldShape& /*height*/) override { return false; } - bool removeWater(const osg::Vec2i& /*cellPosition*/) override + bool removeHeightfield(const osg::Vec2i& /*cellPosition*/) override { return false; } @@ -66,18 +73,22 @@ namespace DetourNavigator void update(const osg::Vec3f& /*playerPosition*/) override {} - void setUpdatesEnabled(bool enabled) override {} + void updateBounds(const osg::Vec3f& /*playerPosition*/) override {} + + void updatePlayerPosition(const osg::Vec3f& /*playerPosition*/) override {}; - void wait() override {} + void setUpdatesEnabled(bool /*enabled*/) override {} - SharedNavMeshCacheItem getNavMesh(const osg::Vec3f& /*agentHalfExtents*/) const override + void wait(Loading::Listener& /*listener*/, WaitConditionType /*waitConditionType*/) override {} + + SharedNavMeshCacheItem getNavMesh(const AgentBounds& /*agentBounds*/) const override { return mEmptyNavMeshCacheItem; } - std::map getNavMeshes() const override + std::map getNavMeshes() const override { - return std::map(); + return {}; } const Settings& getSettings() const override @@ -87,11 +98,16 @@ namespace DetourNavigator void reportStats(unsigned int /*frameNumber*/, osg::Stats& /*stats*/) const override {} - RecastMeshTiles getRecastMeshTiles() override + RecastMeshTiles getRecastMeshTiles() const override { return {}; } + float getMaxNavmeshAreaRealRadius() const override + { + return std::numeric_limits::max(); + } + private: Settings mDefaultSettings {}; SharedNavMeshCacheItem mEmptyNavMeshCacheItem; diff --git a/components/detournavigator/navigatorutils.cpp b/components/detournavigator/navigatorutils.cpp new file mode 100644 index 0000000000..450fab6a6d --- /dev/null +++ b/components/detournavigator/navigatorutils.cpp @@ -0,0 +1,37 @@ +#include "navigatorutils.hpp" +#include "findrandompointaroundcircle.hpp" +#include "navigator.hpp" +#include "raycast.hpp" + +namespace DetourNavigator +{ + std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, float(*prng)()) + { + const auto navMesh = navigator.getNavMesh(agentBounds); + if (!navMesh) + return std::nullopt; + const auto& settings = navigator.getSettings(); + const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(), + toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), + toNavMeshCoordinates(settings.mRecast, maxRadius), includeFlags, settings.mDetour, prng); + if (!result) + return std::nullopt; + return std::optional(fromNavMeshCoordinates(settings.mRecast, *result)); + } + + std::optional raycast(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, + const osg::Vec3f& end, const Flags includeFlags) + { + const auto navMesh = navigator.getNavMesh(agentBounds); + if (navMesh == nullptr) + return std::nullopt; + const auto& settings = navigator.getSettings(); + const auto result = DetourNavigator::raycast(navMesh->lockConst()->getImpl(), + toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), + toNavMeshCoordinates(settings.mRecast, end), includeFlags, settings.mDetour); + if (!result) + return std::nullopt; + return fromNavMeshCoordinates(settings.mRecast, *result); + } +} diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp new file mode 100644 index 0000000000..44db7b40fe --- /dev/null +++ b/components/detournavigator/navigatorutils.hpp @@ -0,0 +1,68 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H + +#include "findsmoothpath.hpp" +#include "flags.hpp" +#include "settings.hpp" +#include "navigator.hpp" + +#include + +namespace DetourNavigator +{ + /** + * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. + * @param agentBounds allows to find navmesh for given actor. + * @param start path from given point. + * @param end path at given point. + * @param includeFlags setup allowed surfaces for actor to walk. + * @param out the beginning of the destination range. + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents + * @return Output iterator to the element in the destination range, one past the last element of found path. + * Equal to out if no path is found. + */ + template + inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const float stepSize, + const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, + float endTolerance, OutputIterator& out) + { + static_assert( + std::is_same< + typename std::iterator_traits::iterator_category, + std::output_iterator_tag + >::value, + "out is not an OutputIterator" + ); + const auto navMesh = navigator.getNavMesh(agentBounds); + if (navMesh == nullptr) + return Status::NavMeshNotFound; + const auto settings = navigator.getSettings(); + return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), + toNavMeshCoordinates(settings.mRecast, stepSize), toNavMeshCoordinates(settings.mRecast, start), + toNavMeshCoordinates(settings.mRecast, end), includeFlags, areaCosts, settings, endTolerance, out); + } + + /** + * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. + * @param agentBounds allows to find navmesh for given actor. + * @param start path from given point. + * @param maxRadius limit maximum distance from start. + * @param includeFlags setup allowed surfaces for actor to walk. + * @return not empty optional with position if point is found and empty optional if point is not found. + */ + std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, float(*prng)()); + + /** + * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start. + * @param agentBounds allows to find navmesh for given actor. + * @param start of the line + * @param end of the line + * @param includeFlags setup allowed surfaces for actor to walk. + * @return not empty optional with position if point is found and empty optional if point is not found. + */ + std::optional raycast(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, + const osg::Vec3f& end, const Flags includeFlags); +} + +#endif diff --git a/components/detournavigator/navmeshcacheitem.cpp b/components/detournavigator/navmeshcacheitem.cpp new file mode 100644 index 0000000000..41a177a367 --- /dev/null +++ b/components/detournavigator/navmeshcacheitem.cpp @@ -0,0 +1,139 @@ +#include "tileposition.hpp" +#include "navmeshtilescache.hpp" +#include "navmeshtileview.hpp" +#include "navmeshcacheitem.hpp" +#include "navmeshdata.hpp" + +#include + +#include + +#include + +namespace +{ + using DetourNavigator::TilePosition; + + bool removeTile(dtNavMesh& navMesh, const TilePosition& position) + { + const int layer = 0; + const auto tileRef = navMesh.getTileRefAt(position.x(), position.y(), layer); + if (tileRef == 0) + return false; + unsigned char** const data = nullptr; + int* const dataSize = nullptr; + return dtStatusSucceed(navMesh.removeTile(tileRef, data, dataSize)); + } + + dtStatus addTile(dtNavMesh& navMesh, unsigned char* data, int size) + { + const int doNotTransferOwnership = 0; + const dtTileRef lastRef = 0; + dtTileRef* const result = nullptr; + return navMesh.addTile(data, size, doNotTransferOwnership, lastRef, result); + } +} + +namespace DetourNavigator +{ + std::ostream& operator<<(std::ostream& stream, UpdateNavMeshStatus value) + { + switch (value) + { + case UpdateNavMeshStatus::ignored: + return stream << "ignore"; + case UpdateNavMeshStatus::removed: + return stream << "removed"; + case UpdateNavMeshStatus::added: + return stream << "add"; + case UpdateNavMeshStatus::replaced: + return stream << "replaced"; + case UpdateNavMeshStatus::failed: + return stream << "failed"; + case UpdateNavMeshStatus::lost: + return stream << "lost"; + case UpdateNavMeshStatus::cached: + return stream << "cached"; + case UpdateNavMeshStatus::unchanged: + return stream << "unchanged"; + case UpdateNavMeshStatus::restored: + return stream << "restored"; + } + return stream << "unknown(" << static_cast(value) << ")"; + } + + const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position) + { + const int layer = 0; + return navMesh.getTileAt(position.x(), position.y(), layer); + } + + UpdateNavMeshStatus NavMeshCacheItem::updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, + NavMeshData&& navMeshData) + { + const dtMeshTile* currentTile = getTile(*mImpl, position); + if (currentTile != nullptr + && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(navMeshData.mValue.get())) + { + return UpdateNavMeshStatus::ignored; + } + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.erase(position) > 0 || removed; + const auto addStatus = addTile(*mImpl, navMeshData.mValue.get(), navMeshData.mSize); + if (dtStatusSucceed(addStatus)) + { + auto tile = mUsedTiles.find(position); + if (tile == mUsedTiles.end()) + { + mUsedTiles.emplace_hint(tile, position, + Tile {Version {mVersion.mRevision, 1}, std::move(cached), std::move(navMeshData)}); + } + else + { + ++tile->second.mVersion.mRevision; + tile->second.mCached = std::move(cached); + tile->second.mData = std::move(navMeshData); + } + ++mVersion.mRevision; + return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult(); + } + else + { + if (removed) + { + mUsedTiles.erase(position); + ++mVersion.mRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult(); + } + } + + UpdateNavMeshStatus NavMeshCacheItem::removeTile(const TilePosition& position) + { + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.erase(position) > 0 || removed; + if (removed) + { + mUsedTiles.erase(position); + ++mVersion.mRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).getResult(); + } + + UpdateNavMeshStatus NavMeshCacheItem::markAsEmpty(const TilePosition& position) + { + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.insert(position).second || removed; + if (removed) + { + mUsedTiles.erase(position); + ++mVersion.mRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).getResult(); + } + + bool NavMeshCacheItem::isEmptyTile(const TilePosition& position) const + { + return mEmptyTiles.find(position) != mEmptyTiles.end(); + } +} diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index 76f74f2667..97ad68f1c7 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -4,13 +4,16 @@ #include "sharednavmesh.hpp" #include "tileposition.hpp" #include "navmeshtilescache.hpp" -#include "dtstatus.hpp" +#include "navmeshdata.hpp" +#include "version.hpp" #include -#include - #include +#include +#include + +struct dtMeshTile; namespace DetourNavigator { @@ -32,6 +35,8 @@ namespace DetourNavigator return (static_cast(value) & static_cast(UpdateNavMeshStatus::failed)) == 0; } + std::ostream& operator<<(std::ostream& stream, UpdateNavMeshStatus value); + class UpdateNavMeshStatusBuilder { public: @@ -95,31 +100,14 @@ namespace DetourNavigator } }; - inline unsigned char* getRawData(NavMeshData& navMeshData) - { - return navMeshData.mValue.get(); - } - - inline unsigned char* getRawData(NavMeshTilesCache::Value& cachedNavMeshData) - { - return cachedNavMeshData.get().mValue; - } - - inline int getSize(const NavMeshData& navMeshData) - { - return navMeshData.mSize; - } - - inline int getSize(const NavMeshTilesCache::Value& cachedNavMeshData) - { - return cachedNavMeshData.get().mSize; - } + const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position); class NavMeshCacheItem { public: NavMeshCacheItem(const NavMeshPtr& impl, std::size_t generation) - : mImpl(impl), mGeneration(generation), mNavMeshRevision(0) + : mImpl(impl) + , mVersion {generation, 0} { } @@ -128,84 +116,37 @@ namespace DetourNavigator return *mImpl; } - std::size_t getGeneration() const - { - return mGeneration; - } - - std::size_t getNavMeshRevision() const - { - return mNavMeshRevision; - } - - template - UpdateNavMeshStatus updateTile(const TilePosition& position, T&& navMeshData) - { - const auto removed = removeTileImpl(position); - const auto addStatus = addTileImpl(getRawData(navMeshData), getSize(navMeshData)); - if (dtStatusSucceed(addStatus)) - { - setUsedTile(position, std::forward(navMeshData)); - return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult(); - } - else - { - if (removed) - removeUsedTile(position); - return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult(); - } - } + const Version& getVersion() const { return mVersion; } - UpdateNavMeshStatus removeTile(const TilePosition& position) - { - const auto removed = removeTileImpl(position); - if (removed) - removeUsedTile(position); - return UpdateNavMeshStatusBuilder().removed(removed).getResult(); - } + UpdateNavMeshStatus updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, + NavMeshData&& navMeshData); - private: - NavMeshPtr mImpl; - std::size_t mGeneration; - std::size_t mNavMeshRevision; - std::map> mUsedTiles; + UpdateNavMeshStatus removeTile(const TilePosition& position); - void setUsedTile(const TilePosition& tilePosition, NavMeshTilesCache::Value value) - { - mUsedTiles[tilePosition] = std::make_pair(std::move(value), NavMeshData()); - ++mNavMeshRevision; - } + UpdateNavMeshStatus markAsEmpty(const TilePosition& position); - void setUsedTile(const TilePosition& tilePosition, NavMeshData value) - { - mUsedTiles[tilePosition] = std::make_pair(NavMeshTilesCache::Value(), std::move(value)); - ++mNavMeshRevision; - } + bool isEmptyTile(const TilePosition& position) const; - void removeUsedTile(const TilePosition& tilePosition) + template + void forEachUsedTile(Function&& function) const { - mUsedTiles.erase(tilePosition); - ++mNavMeshRevision; + for (const auto& [position, tile] : mUsedTiles) + if (const dtMeshTile* meshTile = getTile(*mImpl, position)) + function(position, tile.mVersion, *meshTile); } - dtStatus addTileImpl(unsigned char* data, int size) + private: + struct Tile { - const int doNotTransferOwnership = 0; - const dtTileRef lastRef = 0; - dtTileRef* const result = nullptr; - return mImpl->addTile(data, size, doNotTransferOwnership, lastRef, result); - } + Version mVersion; + NavMeshTilesCache::Value mCached; + NavMeshData mData; + }; - bool removeTileImpl(const TilePosition& position) - { - const int layer = 0; - const auto tileRef = mImpl->getTileRefAt(position.x(), position.y(), layer); - if (tileRef == 0) - return false; - unsigned char** const data = nullptr; - int* const dataSize = nullptr; - return dtStatusSucceed(mImpl->removeTile(tileRef, data, dataSize)); - } + NavMeshPtr mImpl; + Version mVersion; + std::map mUsedTiles; + std::set mEmptyTiles; }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshdata.hpp b/components/detournavigator/navmeshdata.hpp index 8ce79614b3..6e400af3bc 100644 --- a/components/detournavigator/navmeshdata.hpp +++ b/components/detournavigator/navmeshdata.hpp @@ -3,7 +3,6 @@ #include -#include #include namespace DetourNavigator @@ -21,7 +20,7 @@ namespace DetourNavigator struct NavMeshData { NavMeshDataValue mValue; - int mSize; + int mSize = 0; NavMeshData() = default; diff --git a/components/detournavigator/navmeshdb.cpp b/components/detournavigator/navmeshdb.cpp new file mode 100644 index 0000000000..93439387d7 --- /dev/null +++ b/components/detournavigator/navmeshdb.cpp @@ -0,0 +1,424 @@ +#include "navmeshdb.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +namespace DetourNavigator +{ + namespace + { + constexpr const char schema[] = R"( + BEGIN TRANSACTION; + + CREATE TABLE IF NOT EXISTS tiles ( + tile_id INTEGER PRIMARY KEY, + revision INTEGER NOT NULL DEFAULT 1, + worldspace TEXT NOT NULL, + tile_position_x INTEGER NOT NULL, + tile_position_y INTEGER NOT NULL, + version INTEGER NOT NULL, + input BLOB, + data BLOB + ); + + CREATE UNIQUE INDEX IF NOT EXISTS index_unique_tiles_by_worldspace_and_tile_position_and_input + ON tiles (worldspace, tile_position_x, tile_position_y, input); + + CREATE INDEX IF NOT EXISTS index_tiles_by_worldspace_and_tile_position + ON tiles (worldspace, tile_position_x, tile_position_y); + + CREATE TABLE IF NOT EXISTS shapes ( + shape_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + type INTEGER NOT NULL, + hash BLOB NOT NULL + ); + + CREATE UNIQUE INDEX IF NOT EXISTS index_unique_shapes_by_name_and_type_and_hash + ON shapes (name, type, hash); + + COMMIT; + )"; + + constexpr std::string_view getMaxTileIdQuery = R"( + SELECT max(tile_id) FROM tiles + )"; + + constexpr std::string_view findTileQuery = R"( + SELECT tile_id, version + FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + AND input = :input + )"; + + constexpr std::string_view getTileDataQuery = R"( + SELECT tile_id, version, data + FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + AND input = :input + )"; + + constexpr std::string_view insertTileQuery = R"( + INSERT INTO tiles ( tile_id, worldspace, version, tile_position_x, tile_position_y, input, data) + VALUES (:tile_id, :worldspace, :version, :tile_position_x, :tile_position_y, :input, :data) + )"; + + constexpr std::string_view updateTileQuery = R"( + UPDATE tiles + SET version = :version, + data = :data, + revision = revision + 1 + WHERE tile_id = :tile_id + )"; + + constexpr std::string_view deleteTilesAtQuery = R"( + DELETE FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + )"; + + constexpr std::string_view deleteTilesAtExceptQuery = R"( + DELETE FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + AND tile_id != :exclude_tile_id + )"; + + constexpr std::string_view deleteTilesOutsideRangeQuery = R"( + DELETE FROM tiles + WHERE worldspace = :worldspace + AND ( tile_position_x < :begin_tile_position_x + OR tile_position_y < :begin_tile_position_y + OR tile_position_x >= :end_tile_position_x + OR tile_position_y >= :end_tile_position_y + ) + )"; + + constexpr std::string_view getMaxShapeIdQuery = R"( + SELECT max(shape_id) FROM shapes + )"; + + constexpr std::string_view findShapeIdQuery = R"( + SELECT shape_id + FROM shapes + WHERE name = :name + AND type = :type + AND hash = :hash + )"; + + constexpr std::string_view insertShapeQuery = R"( + INSERT INTO shapes ( shape_id, name, type, hash) + VALUES (:shape_id, :name, :type, :hash) + )"; + + constexpr std::string_view vacuumQuery = R"( + VACUUM; + )"; + + struct GetPageSize + { + static std::string_view text() noexcept { return "pragma page_size;"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + std::uint64_t getPageSize(sqlite3& db) + { + Sqlite3::Statement statement(db); + std::uint64_t value = 0; + request(db, statement, &value, 1); + return value; + } + + void setMaxPageCount(sqlite3& db, std::uint64_t value) + { + const auto query = Misc::StringUtils::format("pragma max_page_count = %lu;", value); + if (const int ec = sqlite3_exec(&db, query.c_str(), nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed set max page count: " + std::string(sqlite3_errmsg(&db))); + } + } + + std::ostream& operator<<(std::ostream& stream, ShapeType value) + { + switch (value) + { + case ShapeType::Collision: return stream << "collision"; + case ShapeType::Avoid: return stream << "avoid"; + } + return stream << "unknown shape type (" << static_cast>(value) << ")"; + } + + NavMeshDb::NavMeshDb(std::string_view path, std::uint64_t maxFileSize) + : mDb(Sqlite3::makeDb(path, schema)) + , mGetMaxTileId(*mDb, DbQueries::GetMaxTileId {}) + , mFindTile(*mDb, DbQueries::FindTile {}) + , mGetTileData(*mDb, DbQueries::GetTileData {}) + , mInsertTile(*mDb, DbQueries::InsertTile {}) + , mUpdateTile(*mDb, DbQueries::UpdateTile {}) + , mDeleteTilesAt(*mDb, DbQueries::DeleteTilesAt {}) + , mDeleteTilesAtExcept(*mDb, DbQueries::DeleteTilesAtExcept {}) + , mDeleteTilesOutsideRange(*mDb, DbQueries::DeleteTilesOutsideRange {}) + , mGetMaxShapeId(*mDb, DbQueries::GetMaxShapeId {}) + , mFindShapeId(*mDb, DbQueries::FindShapeId {}) + , mInsertShape(*mDb, DbQueries::InsertShape {}) + , mVacuum(*mDb, DbQueries::Vacuum {}) + { + const std::uint64_t dbPageSize = getPageSize(*mDb); + if (dbPageSize == 0) + throw std::runtime_error("NavMeshDb page size is zero"); + setMaxPageCount(*mDb, maxFileSize / dbPageSize + static_cast((maxFileSize % dbPageSize) != 0)); + } + + Sqlite3::Transaction NavMeshDb::startTransaction(Sqlite3::TransactionMode mode) + { + return Sqlite3::Transaction(*mDb, mode); + } + + TileId NavMeshDb::getMaxTileId() + { + TileId tileId {0}; + request(*mDb, mGetMaxTileId, &tileId, 1); + return tileId; + } + + std::optional NavMeshDb::findTile(std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Tile result; + auto row = std::tie(result.mTileId, result.mVersion); + const std::vector compressedInput = Misc::compress(input); + if (&row == request(*mDb, mFindTile, &row, 1, worldspace, tilePosition, compressedInput)) + return {}; + return result; + } + + std::optional NavMeshDb::getTileData(std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + TileData result; + auto row = std::tie(result.mTileId, result.mVersion, result.mData); + const std::vector compressedInput = Misc::compress(input); + if (&row == request(*mDb, mGetTileData, &row, 1, worldspace, tilePosition, compressedInput)) + return {}; + result.mData = Misc::decompress(result.mData); + return result; + } + + int NavMeshDb::insertTile(TileId tileId, std::string_view worldspace, const TilePosition& tilePosition, + TileVersion version, const std::vector& input, const std::vector& data) + { + const std::vector compressedInput = Misc::compress(input); + const std::vector compressedData = Misc::compress(data); + return execute(*mDb, mInsertTile, tileId, worldspace, tilePosition, version, compressedInput, compressedData); + } + + int NavMeshDb::updateTile(TileId tileId, TileVersion version, const std::vector& data) + { + const std::vector compressedData = Misc::compress(data); + return execute(*mDb, mUpdateTile, tileId, version, compressedData); + } + + int NavMeshDb::deleteTilesAt(std::string_view worldspace, const TilePosition& tilePosition) + { + return execute(*mDb, mDeleteTilesAt, worldspace, tilePosition); + } + + int NavMeshDb::deleteTilesAtExcept(std::string_view worldspace, const TilePosition& tilePosition, TileId excludeTileId) + { + return execute(*mDb, mDeleteTilesAtExcept, worldspace, tilePosition, excludeTileId); + } + + int NavMeshDb::deleteTilesOutsideRange(std::string_view worldspace, const TilesPositionsRange& range) + { + return execute(*mDb, mDeleteTilesOutsideRange, worldspace, range); + } + + ShapeId NavMeshDb::getMaxShapeId() + { + ShapeId shapeId {0}; + request(*mDb, mGetMaxShapeId, &shapeId, 1); + return shapeId; + } + + std::optional NavMeshDb::findShapeId(std::string_view name, ShapeType type, + const Sqlite3::ConstBlob& hash) + { + ShapeId shapeId; + if (&shapeId == request(*mDb, mFindShapeId, &shapeId, 1, name, type, hash)) + return {}; + return shapeId; + } + + int NavMeshDb::insertShape(ShapeId shapeId, std::string_view name, ShapeType type, + const Sqlite3::ConstBlob& hash) + { + return execute(*mDb, mInsertShape, shapeId, name, type, hash); + } + + void NavMeshDb::vacuum() + { + execute(*mDb, mVacuum); + } + + namespace DbQueries + { + std::string_view GetMaxTileId::text() noexcept + { + return getMaxTileIdQuery; + } + + std::string_view FindTile::text() noexcept + { + return findTileQuery; + } + + void FindTile::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":input", input); + } + + std::string_view GetTileData::text() noexcept + { + return getTileDataQuery; + } + + void GetTileData::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":input", input); + } + + std::string_view InsertTile::text() noexcept + { + return insertTileQuery; + } + + void InsertTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, std::string_view worldspace, + const TilePosition& tilePosition, TileVersion version, const std::vector& input, + const std::vector& data) + { + Sqlite3::bindParameter(db, statement, ":tile_id", tileId); + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":version", version); + Sqlite3::bindParameter(db, statement, ":input", input); + Sqlite3::bindParameter(db, statement, ":data", data); + } + + std::string_view UpdateTile::text() noexcept + { + return updateTileQuery; + } + + void UpdateTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version, + const std::vector& data) + { + Sqlite3::bindParameter(db, statement, ":tile_id", tileId); + Sqlite3::bindParameter(db, statement, ":version", version); + Sqlite3::bindParameter(db, statement, ":data", data); + } + + std::string_view DeleteTilesAt::text() noexcept + { + return deleteTilesAtQuery; + } + + void DeleteTilesAt::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + } + + std::string_view DeleteTilesAtExcept::text() noexcept + { + return deleteTilesAtExceptQuery; + } + + void DeleteTilesAtExcept::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, TileId excludeTileId) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":exclude_tile_id", excludeTileId); + } + + std::string_view DeleteTilesOutsideRange::text() noexcept + { + return deleteTilesOutsideRangeQuery; + } + + void DeleteTilesOutsideRange::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilesPositionsRange& range) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":begin_tile_position_x", range.mBegin.x()); + Sqlite3::bindParameter(db, statement, ":begin_tile_position_y", range.mBegin.y()); + Sqlite3::bindParameter(db, statement, ":end_tile_position_x", range.mEnd.x()); + Sqlite3::bindParameter(db, statement, ":end_tile_position_y", range.mEnd.y()); + } + + std::string_view GetMaxShapeId::text() noexcept + { + return getMaxShapeIdQuery; + } + + std::string_view FindShapeId::text() noexcept + { + return findShapeIdQuery; + } + + void FindShapeId::bind(sqlite3& db, sqlite3_stmt& statement, std::string_view name, + ShapeType type, const Sqlite3::ConstBlob& hash) + { + Sqlite3::bindParameter(db, statement, ":name", name); + Sqlite3::bindParameter(db, statement, ":type", static_cast(type)); + Sqlite3::bindParameter(db, statement, ":hash", hash); + } + + std::string_view InsertShape::text() noexcept + { + return insertShapeQuery; + } + + void InsertShape::bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, std::string_view name, + ShapeType type, const Sqlite3::ConstBlob& hash) + { + Sqlite3::bindParameter(db, statement, ":shape_id", shapeId); + Sqlite3::bindParameter(db, statement, ":name", name); + Sqlite3::bindParameter(db, statement, ":type", static_cast(type)); + Sqlite3::bindParameter(db, statement, ":hash", hash); + } + + std::string_view Vacuum::text() noexcept + { + return vacuumQuery; + } + } +} diff --git a/components/detournavigator/navmeshdb.hpp b/components/detournavigator/navmeshdb.hpp new file mode 100644 index 0000000000..812452206e --- /dev/null +++ b/components/detournavigator/navmeshdb.hpp @@ -0,0 +1,192 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H + +#include "tileposition.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace DetourNavigator +{ + using TileId = Misc::StrongTypedef; + using TileRevision = Misc::StrongTypedef; + using TileVersion = Misc::StrongTypedef; + using ShapeId = Misc::StrongTypedef; + + struct Tile + { + TileId mTileId; + TileVersion mVersion; + }; + + struct TileData + { + TileId mTileId; + TileVersion mVersion; + std::vector mData; + }; + + enum class ShapeType + { + Collision = 1, + Avoid = 2, + }; + + std::ostream& operator<<(std::ostream& stream, ShapeType value); + + namespace DbQueries + { + struct GetMaxTileId + { + static std::string_view text() noexcept; + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct FindTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input); + }; + + struct GetTileData + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input); + }; + + struct InsertTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, std::string_view worldspace, + const TilePosition& tilePosition, TileVersion version, const std::vector& input, + const std::vector& data); + }; + + struct UpdateTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version, + const std::vector& data); + }; + + struct DeleteTilesAt + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition); + }; + + struct DeleteTilesAtExcept + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilePosition& tilePosition, TileId excludeTileId); + }; + + struct DeleteTilesOutsideRange + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view worldspace, + const TilesPositionsRange& range); + }; + + struct GetMaxShapeId + { + static std::string_view text() noexcept; + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct FindShapeId + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view name, + ShapeType type, const Sqlite3::ConstBlob& hash); + }; + + struct InsertShape + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, std::string_view name, + ShapeType type, const Sqlite3::ConstBlob& hash); + }; + + struct Vacuum + { + static std::string_view text() noexcept; + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + } + + class NavMeshDb + { + public: + explicit NavMeshDb(std::string_view path, std::uint64_t maxFileSize); + + Sqlite3::Transaction startTransaction(Sqlite3::TransactionMode mode = Sqlite3::TransactionMode::Default); + + TileId getMaxTileId(); + + std::optional findTile(std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input); + + std::optional getTileData(std::string_view worldspace, + const TilePosition& tilePosition, const std::vector& input); + + int insertTile(TileId tileId, std::string_view worldspace, const TilePosition& tilePosition, + TileVersion version, const std::vector& input, const std::vector& data); + + int updateTile(TileId tileId, TileVersion version, const std::vector& data); + + int deleteTilesAt(std::string_view worldspace, const TilePosition& tilePosition); + + int deleteTilesAtExcept(std::string_view worldspace, const TilePosition& tilePosition, TileId excludeTileId); + + int deleteTilesOutsideRange(std::string_view worldspace, const TilesPositionsRange& range); + + ShapeId getMaxShapeId(); + + std::optional findShapeId(std::string_view name, ShapeType type, const Sqlite3::ConstBlob& hash); + + int insertShape(ShapeId shapeId, std::string_view name, ShapeType type, const Sqlite3::ConstBlob& hash); + + void vacuum(); + + private: + Sqlite3::Db mDb; + Sqlite3::Statement mGetMaxTileId; + Sqlite3::Statement mFindTile; + Sqlite3::Statement mGetTileData; + Sqlite3::Statement mInsertTile; + Sqlite3::Statement mUpdateTile; + Sqlite3::Statement mDeleteTilesAt; + Sqlite3::Statement mDeleteTilesAtExcept; + Sqlite3::Statement mDeleteTilesOutsideRange; + Sqlite3::Statement mGetMaxShapeId; + Sqlite3::Statement mFindShapeId; + Sqlite3::Statement mInsertShape; + Sqlite3::Statement mVacuum; + }; +} + +#endif diff --git a/components/detournavigator/navmeshdbutils.cpp b/components/detournavigator/navmeshdbutils.cpp new file mode 100644 index 0000000000..d2379c4e53 --- /dev/null +++ b/components/detournavigator/navmeshdbutils.cpp @@ -0,0 +1,64 @@ +#include "navmeshdbutils.hpp" +#include "navmeshdb.hpp" +#include "recastmesh.hpp" + +#include + +#include +#include +#include + +namespace DetourNavigator +{ + namespace + { + std::optional findShapeId(NavMeshDb& db, std::string_view name, ShapeType type, + const std::string& hash) + { + const Sqlite3::ConstBlob hashData {hash.data(), static_cast(hash.size())}; + return db.findShapeId(name, type, hashData); + } + + ShapeId getShapeId(NavMeshDb& db, std::string_view name, ShapeType type, + const std::string& hash, ShapeId& nextShapeId) + { + const Sqlite3::ConstBlob hashData {hash.data(), static_cast(hash.size())}; + if (const auto existingShapeId = db.findShapeId(name, type, hashData)) + return *existingShapeId; + const ShapeId newShapeId = nextShapeId; + db.insertShape(newShapeId, name, type, hashData); + Log(Debug::Verbose) << "Added " << name << " " << type << " shape to navmeshdb with id " << newShapeId; + ++nextShapeId; + return newShapeId; + } + } + + ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId) + { + switch (source.mAreaType) + { + case AreaType_null: + return getShapeId(db, source.mShape->mFileName, ShapeType::Avoid, source.mShape->mFileHash, nextShapeId); + case AreaType_ground: + return getShapeId(db, source.mShape->mFileName, ShapeType::Collision, source.mShape->mFileHash, nextShapeId); + default: + Log(Debug::Warning) << "Trying to resolve recast mesh source with unsupported area type: " << source.mAreaType; + assert(false); + return ShapeId(0); + } + } + + std::optional resolveMeshSource(NavMeshDb& db, const MeshSource& source) + { + switch (source.mAreaType) + { + case AreaType_null: + return findShapeId(db, source.mShape->mFileName, ShapeType::Avoid, source.mShape->mFileHash); + case AreaType_ground: + return findShapeId(db, source.mShape->mFileName, ShapeType::Collision, source.mShape->mFileHash); + default: + Log(Debug::Warning) << "Trying to resolve recast mesh source with unsupported area type: " << source.mAreaType; + return std::nullopt; + } + } +} diff --git a/components/detournavigator/navmeshdbutils.hpp b/components/detournavigator/navmeshdbutils.hpp new file mode 100644 index 0000000000..aafde3307c --- /dev/null +++ b/components/detournavigator/navmeshdbutils.hpp @@ -0,0 +1,17 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H + +#include "navmeshdb.hpp" + +#include + +namespace DetourNavigator +{ + struct MeshSource; + + ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId); + + std::optional resolveMeshSource(NavMeshDb& db, const MeshSource& source); +} + +#endif diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index 43d3306481..672741e089 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -5,11 +5,18 @@ #include "makenavmesh.hpp" #include "navmeshcacheitem.hpp" #include "settings.hpp" +#include "waitconditiontype.hpp" #include +#include +#include + +#include #include +#include + namespace { using DetourNavigator::ChangeType; @@ -37,27 +44,66 @@ namespace namespace DetourNavigator { - NavMeshManager::NavMeshManager(const Settings& settings) + namespace + { + TileBounds makeBounds(const RecastSettings& settings, const osg::Vec2f& center, int maxTiles) + { + const float radius = fromNavMeshCoordinates(settings, std::ceil(std::sqrt(static_cast(maxTiles) / osg::PIf) + 1) * getTileSize(settings)); + TileBounds result; + result.mMin = center - osg::Vec2f(radius, radius); + result.mMax = center + osg::Vec2f(radius, radius); + return result; + } + } + + NavMeshManager::NavMeshManager(const Settings& settings, std::unique_ptr&& db) : mSettings(settings) - , mRecastMeshManager(settings) - , mOffMeshConnectionsManager(settings) - , mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager) + , mRecastMeshManager(settings.mRecast) + , mOffMeshConnectionsManager(settings.mRecast) + , mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)) {} - bool NavMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + void NavMeshManager::setWorldspace(std::string_view worldspace) + { + if (worldspace == mWorldspace) + return; + mRecastMeshManager.setWorldspace(worldspace); + for (auto& [agent, cache] : mCache) + cache = std::make_shared(makeEmptyNavMesh(mSettings), ++mGenerationCounter); + mWorldspace = worldspace; + } + + void NavMeshManager::updateBounds(const osg::Vec3f& playerPosition) + { + const TileBounds bounds = makeBounds(mSettings.mRecast, osg::Vec2f(playerPosition.x(), playerPosition.y()), + mSettings.mMaxTilesNumber); + const auto changedTiles = mRecastMeshManager.setBounds(bounds); + for (const auto& [agent, cache] : mCache) + { + auto& tiles = mChangedTiles[agent]; + for (const auto& [tilePosition, changeType] : changedTiles) + { + auto tile = tiles.find(tilePosition); + if (tile == tiles.end()) + tiles.emplace_hint(tile, tilePosition, changeType); + else + tile->second = addChangeType(tile->second, changeType); + } + } + } + + bool NavMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { - if (!mRecastMeshManager.addObject(id, shape, transform, areaType)) - return false; - addChangedTiles(shape, transform, ChangeType::add); - return true; + return mRecastMeshManager.addObject(id, shape, transform, areaType, + [&] (const TilePosition& tile) { addChangedTile(tile, ChangeType::add); }); } - bool NavMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool NavMeshManager::updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { return mRecastMeshManager.updateObject(id, shape, transform, areaType, - [&] (const TilePosition& tile) { addChangedTile(tile, ChangeType::update); }); + [&] (const TilePosition& tile, ChangeType changeType) { addChangedTile(tile, changeType); }); } bool NavMeshManager::removeObject(const ObjectId id) @@ -69,11 +115,12 @@ namespace DetourNavigator return true; } - bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform) + bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - if (!mRecastMeshManager.addWater(cellPosition, cellSize, transform)) + if (!mRecastMeshManager.addWater(cellPosition, cellSize, level)) return false; - addChangedTiles(cellSize, transform, ChangeType::add); + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level)); + addChangedTiles(cellSize, shift, ChangeType::add); return true; } @@ -82,31 +129,51 @@ namespace DetourNavigator const auto water = mRecastMeshManager.removeWater(cellPosition); if (!water) return false; - addChangedTiles(water->mCellSize, water->mTransform, ChangeType::remove); + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, water->mCellSize, water->mLevel)); + addChangedTiles(water->mCellSize, shift, ChangeType::remove); + return true; + } + + bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) + { + if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shape)) + return false; + const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize); + addChangedTiles(cellSize, shift, ChangeType::add); + return true; + } + + bool NavMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const auto heightfield = mRecastMeshManager.removeHeightfield(cellPosition); + if (!heightfield) + return false; + const btVector3 shift = getHeightfieldShift(heightfield->mShape, cellPosition, heightfield->mCellSize); + addChangedTiles(heightfield->mCellSize, shift, ChangeType::remove); return true; } - void NavMeshManager::addAgent(const osg::Vec3f& agentHalfExtents) + void NavMeshManager::addAgent(const AgentBounds& agentBounds) { - auto cached = mCache.find(agentHalfExtents); + auto cached = mCache.find(agentBounds); if (cached != mCache.end()) return; - mCache.insert(std::make_pair(agentHalfExtents, + mCache.insert(std::make_pair(agentBounds, std::make_shared(makeEmptyNavMesh(mSettings), ++mGenerationCounter))); - Log(Debug::Debug) << "cache add for agent=" << agentHalfExtents; + Log(Debug::Debug) << "cache add for agent=" << agentBounds; } - bool NavMeshManager::reset(const osg::Vec3f& agentHalfExtents) + bool NavMeshManager::reset(const AgentBounds& agentBounds) { - const auto it = mCache.find(agentHalfExtents); + const auto it = mCache.find(agentBounds); if (it == mCache.end()) return true; if (!resetIfUnique(it->second)) return false; - mCache.erase(agentHalfExtents); - mChangedTiles.erase(agentHalfExtents); - mPlayerTile.erase(agentHalfExtents); - mLastRecastMeshManagerRevision.erase(agentHalfExtents); + mCache.erase(agentBounds); + mChangedTiles.erase(agentBounds); + mPlayerTile.erase(agentBounds); + mLastRecastMeshManagerRevision.erase(agentBounds); return true; } @@ -114,8 +181,8 @@ namespace DetourNavigator { mOffMeshConnectionsManager.add(id, OffMeshConnection {start, end, areaType}); - const auto startTilePosition = getTilePosition(mSettings, start); - const auto endTilePosition = getTilePosition(mSettings, end); + const auto startTilePosition = getTilePosition(mSettings.mRecast, start); + const auto endTilePosition = getTilePosition(mSettings.mRecast, end); addChangedTile(startTilePosition, ChangeType::add); @@ -130,28 +197,28 @@ namespace DetourNavigator addChangedTile(tile, ChangeType::update); } - void NavMeshManager::update(osg::Vec3f playerPosition, const osg::Vec3f& agentHalfExtents) + void NavMeshManager::update(const osg::Vec3f& playerPosition, const AgentBounds& agentBounds) { - const auto playerTile = getTilePosition(mSettings, toNavMeshCoordinates(mSettings, playerPosition)); - auto& lastRevision = mLastRecastMeshManagerRevision[agentHalfExtents]; - auto lastPlayerTile = mPlayerTile.find(agentHalfExtents); + const auto playerTile = getTilePosition(mSettings.mRecast, toNavMeshCoordinates(mSettings.mRecast, playerPosition)); + auto& lastRevision = mLastRecastMeshManagerRevision[agentBounds]; + auto lastPlayerTile = mPlayerTile.find(agentBounds); if (lastRevision == mRecastMeshManager.getRevision() && lastPlayerTile != mPlayerTile.end() && lastPlayerTile->second == playerTile) return; lastRevision = mRecastMeshManager.getRevision(); if (lastPlayerTile == mPlayerTile.end()) - lastPlayerTile = mPlayerTile.insert(std::make_pair(agentHalfExtents, playerTile)).first; + lastPlayerTile = mPlayerTile.insert(std::make_pair(agentBounds, playerTile)).first; else lastPlayerTile->second = playerTile; std::map tilesToPost; - const auto cached = getCached(agentHalfExtents); + const auto cached = getCached(agentBounds); if (!cached) { std::ostringstream stream; - stream << "Agent with half extents is not found: " << agentHalfExtents; + stream << "Agent with half extents is not found: " << agentBounds; throw InvalidArgument(stream.str()); } - const auto changedTiles = mChangedTiles.find(agentHalfExtents); + const auto changedTiles = mChangedTiles.find(agentBounds); { const auto locked = cached->lockConst(); const auto& navMesh = locked->getImpl(); @@ -168,71 +235,76 @@ namespace DetourNavigator } } const auto maxTiles = std::min(mSettings.mMaxTilesNumber, navMesh.getParams()->maxTiles); - mRecastMeshManager.forEachTilePosition([&] (const TilePosition& tile) + mRecastMeshManager.forEachTile([&] (const TilePosition& tile, CachedRecastMeshManager& recastMeshManager) { if (tilesToPost.count(tile)) return; const auto shouldAdd = shouldAddTile(tile, playerTile, maxTiles); const auto presentInNavMesh = bool(navMesh.getTileAt(tile.x(), tile.y(), 0)); if (shouldAdd && !presentInNavMesh) - tilesToPost.insert(std::make_pair(tile, ChangeType::add)); + tilesToPost.insert(std::make_pair(tile, locked->isEmptyTile(tile) ? ChangeType::update : ChangeType::add)); else if (!shouldAdd && presentInNavMesh) tilesToPost.insert(std::make_pair(tile, ChangeType::mixed)); + else + recastMeshManager.reportNavMeshChange(recastMeshManager.getVersion(), Version {0, 0}); }); } - mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, tilesToPost); + mAsyncNavMeshUpdater.post(agentBounds, cached, playerTile, mRecastMeshManager.getWorldspace(), tilesToPost); if (changedTiles != mChangedTiles.end()) changedTiles->second.clear(); - Log(Debug::Debug) << "Cache update posted for agent=" << agentHalfExtents << + Log(Debug::Debug) << "Cache update posted for agent=" << agentBounds << " playerTile=" << lastPlayerTile->second << " recastMeshManagerRevision=" << lastRevision; } - void NavMeshManager::wait() + void NavMeshManager::wait(Loading::Listener& listener, WaitConditionType waitConditionType) { - mAsyncNavMeshUpdater.wait(); + mAsyncNavMeshUpdater.wait(listener, waitConditionType); } - SharedNavMeshCacheItem NavMeshManager::getNavMesh(const osg::Vec3f& agentHalfExtents) const + SharedNavMeshCacheItem NavMeshManager::getNavMesh(const AgentBounds& agentBounds) const { - return getCached(agentHalfExtents); + return getCached(agentBounds); } - std::map NavMeshManager::getNavMeshes() const + std::map NavMeshManager::getNavMeshes() const { return mCache; } void NavMeshManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const { - mAsyncNavMeshUpdater.reportStats(frameNumber, stats); + DetourNavigator::reportStats(mAsyncNavMeshUpdater.getStats(), frameNumber, stats); } - RecastMeshTiles NavMeshManager::getRecastMeshTiles() + RecastMeshTiles NavMeshManager::getRecastMeshTiles() const { std::vector tiles; - mRecastMeshManager.forEachTilePosition( - [&tiles] (const TilePosition& tile) { tiles.push_back(tile); }); + mRecastMeshManager.forEachTile( + [&tiles] (const TilePosition& tile, const CachedRecastMeshManager&) { tiles.push_back(tile); }); + const std::string worldspace = mRecastMeshManager.getWorldspace(); RecastMeshTiles result; - std::transform(tiles.begin(), tiles.end(), std::inserter(result, result.end()), - [this] (const TilePosition& tile) { return std::make_pair(tile, mRecastMeshManager.getMesh(tile)); }); + for (const TilePosition& tile : tiles) + if (auto mesh = mRecastMeshManager.getCachedMesh(worldspace, tile)) + result.emplace(tile, std::move(mesh)); return result; } void NavMeshManager::addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType) { - getTilesPositions(shape, transform, mSettings, + const auto bounds = mRecastMeshManager.getBounds(); + getTilesPositions(makeTilesPositionsRange(shape, transform, bounds, mSettings.mRecast), [&] (const TilePosition& v) { addChangedTile(v, changeType); }); } - void NavMeshManager::addChangedTiles(const int cellSize, const btTransform& transform, + void NavMeshManager::addChangedTiles(const int cellSize, const btVector3& shift, const ChangeType changeType) { if (cellSize == std::numeric_limits::max()) return; - getTilesPositions(cellSize, transform, mSettings, + getTilesPositions(makeTilesPositionsRange(cellSize, shift, mSettings.mRecast), [&] (const TilePosition& v) { addChangedTile(v, changeType); }); } @@ -249,9 +321,9 @@ namespace DetourNavigator } } - SharedNavMeshCacheItem NavMeshManager::getCached(const osg::Vec3f& agentHalfExtents) const + SharedNavMeshCacheItem NavMeshManager::getCached(const AgentBounds& agentBounds) const { - const auto cached = mCache.find(agentHalfExtents); + const auto cached = mCache.find(agentBounds); if (cached != mCache.end()) return cached->second; return SharedNavMeshCacheItem(); diff --git a/components/detournavigator/navmeshmanager.hpp b/components/detournavigator/navmeshmanager.hpp index f3861f8f27..d4a5a58122 100644 --- a/components/detournavigator/navmeshmanager.hpp +++ b/components/detournavigator/navmeshmanager.hpp @@ -4,10 +4,10 @@ #include "asyncnavmeshupdater.hpp" #include "cachedrecastmeshmanager.hpp" #include "offmeshconnectionsmanager.hpp" -#include "sharednavmesh.hpp" #include "recastmeshtiles.hpp" - -#include +#include "waitconditiontype.hpp" +#include "heightfieldshape.hpp" +#include "agentbounds.hpp" #include @@ -21,58 +21,67 @@ namespace DetourNavigator class NavMeshManager { public: - NavMeshManager(const Settings& settings); + explicit NavMeshManager(const Settings& settings, std::unique_ptr&& db); + + void setWorldspace(std::string_view worldspace); + + void updateBounds(const osg::Vec3f& playerPosition); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool removeObject(const ObjectId id); - void addAgent(const osg::Vec3f& agentHalfExtents); + void addAgent(const AgentBounds& agentBounds); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); bool removeWater(const osg::Vec2i& cellPosition); - bool reset(const osg::Vec3f& agentHalfExtents); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); + + bool removeHeightfield(const osg::Vec2i& cellPosition); + + bool reset(const AgentBounds& agentBounds); void addOffMeshConnection(const ObjectId id, const osg::Vec3f& start, const osg::Vec3f& end, const AreaType areaType); void removeOffMeshConnections(const ObjectId id); - void update(osg::Vec3f playerPosition, const osg::Vec3f& agentHalfExtents); + void update(const osg::Vec3f& playerPosition, const AgentBounds& agentBounds); - void wait(); + void wait(Loading::Listener& listener, WaitConditionType waitConditionType); - SharedNavMeshCacheItem getNavMesh(const osg::Vec3f& agentHalfExtents) const; + SharedNavMeshCacheItem getNavMesh(const AgentBounds& agentBounds) const; - std::map getNavMeshes() const; + std::map getNavMeshes() const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; - RecastMeshTiles getRecastMeshTiles(); + RecastMeshTiles getRecastMeshTiles() const; private: const Settings& mSettings; + std::string mWorldspace; TileCachedRecastMeshManager mRecastMeshManager; OffMeshConnectionsManager mOffMeshConnectionsManager; AsyncNavMeshUpdater mAsyncNavMeshUpdater; - std::map mCache; - std::map> mChangedTiles; + std::map mCache; + std::map> mChangedTiles; std::size_t mGenerationCounter = 0; - std::map mPlayerTile; - std::map mLastRecastMeshManagerRevision; + std::map mPlayerTile; + std::map mLastRecastMeshManagerRevision; void addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType); - void addChangedTiles(const int cellSize, const btTransform& transform, const ChangeType changeType); + void addChangedTiles(const int cellSize, const btVector3& shift, const ChangeType changeType); void addChangedTile(const TilePosition& tilePosition, const ChangeType changeType); - SharedNavMeshCacheItem getCached(const osg::Vec3f& agentHalfExtents) const; + SharedNavMeshCacheItem getCached(const AgentBounds& agentBounds) const; }; } diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp index f554cd4143..a07aa873c4 100644 --- a/components/detournavigator/navmeshtilescache.cpp +++ b/components/detournavigator/navmeshtilescache.cpp @@ -1,5 +1,4 @@ #include "navmeshtilescache.hpp" -#include "exceptions.hpp" #include @@ -7,152 +6,101 @@ namespace DetourNavigator { - namespace - { - inline std::vector makeNavMeshKey(const RecastMesh& recastMesh, - const std::vector& offMeshConnections) - { - const std::size_t indicesSize = recastMesh.getIndices().size() * sizeof(int); - const std::size_t verticesSize = recastMesh.getVertices().size() * sizeof(float); - const std::size_t areaTypesSize = recastMesh.getAreaTypes().size() * sizeof(AreaType); - const std::size_t waterSize = recastMesh.getWater().size() * sizeof(RecastMesh::Water); - const std::size_t offMeshConnectionsSize = offMeshConnections.size() * sizeof(OffMeshConnection); - - std::vector result(indicesSize + verticesSize + areaTypesSize + waterSize + offMeshConnectionsSize); - unsigned char* dst = result.data(); - - std::memcpy(dst, recastMesh.getIndices().data(), indicesSize); - dst += indicesSize; - - std::memcpy(dst, recastMesh.getVertices().data(), verticesSize); - dst += verticesSize; - - std::memcpy(dst, recastMesh.getAreaTypes().data(), areaTypesSize); - dst += areaTypesSize; - - std::memcpy(dst, recastMesh.getWater().data(), waterSize); - dst += waterSize; - - std::memcpy(dst, offMeshConnections.data(), offMeshConnectionsSize); - - return result; - } - } - NavMeshTilesCache::NavMeshTilesCache(const std::size_t maxNavMeshDataSize) - : mMaxNavMeshDataSize(maxNavMeshDataSize), mUsedNavMeshDataSize(0), mFreeNavMeshDataSize(0) {} + : mMaxNavMeshDataSize(maxNavMeshDataSize), mUsedNavMeshDataSize(0), mFreeNavMeshDataSize(0), + mHitCount(0), mGetCount(0) {} - NavMeshTilesCache::Value NavMeshTilesCache::get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections) + NavMeshTilesCache::Value NavMeshTilesCache::get(const AgentBounds& agentBounds, const TilePosition& changedTile, + const RecastMesh& recastMesh) { const std::lock_guard lock(mMutex); - const auto agentValues = mValues.find(agentHalfExtents); - if (agentValues == mValues.end()) - return Value(); + ++mGetCount; - const auto tileValues = agentValues->second.find(changedTile); - if (tileValues == agentValues->second.end()) - return Value(); - - const auto tile = tileValues->second.mMap.find(RecastMeshKeyView(recastMesh, offMeshConnections)); - if (tile == tileValues->second.mMap.end()) + const auto tile = mValues.find(std::tie(agentBounds, changedTile, recastMesh)); + if (tile == mValues.end()) return Value(); acquireItemUnsafe(tile->second); + ++mHitCount; + return Value(*this, tile->second); } - NavMeshTilesCache::Value NavMeshTilesCache::set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections, - NavMeshData&& value) + NavMeshTilesCache::Value NavMeshTilesCache::set(const AgentBounds& agentBounds, const TilePosition& changedTile, + const RecastMesh& recastMesh, std::unique_ptr&& value) { - const auto navMeshSize = static_cast(value.mSize); + const auto itemSize = sizeof(RecastMesh) + getSize(recastMesh) + + (value == nullptr ? 0 : sizeof(PreparedNavMeshData) + getSize(*value)); const std::lock_guard lock(mMutex); - if (navMeshSize > mMaxNavMeshDataSize) - return Value(); - - if (navMeshSize > mFreeNavMeshDataSize + (mMaxNavMeshDataSize - mUsedNavMeshDataSize)) - return Value(); - - auto navMeshKey = makeNavMeshKey(recastMesh, offMeshConnections); - const auto itemSize = navMeshSize + 2 * navMeshKey.size(); - if (itemSize > mFreeNavMeshDataSize + (mMaxNavMeshDataSize - mUsedNavMeshDataSize)) return Value(); while (!mFreeItems.empty() && mUsedNavMeshDataSize + itemSize > mMaxNavMeshDataSize) removeLeastRecentlyUsed(); - const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, std::move(navMeshKey)); - const auto emplaced = mValues[agentHalfExtents][changedTile].mMap.emplace(iterator->mNavMeshKey, iterator); + RecastMeshData key {recastMesh.getMesh(), recastMesh.getWater(), + recastMesh.getHeightfields(), recastMesh.getFlatHeightfields()}; + + const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentBounds, changedTile, std::move(key), itemSize); + const auto emplaced = mValues.emplace(std::make_tuple(agentBounds, changedTile, std::cref(iterator->mRecastMeshData)), iterator); if (!emplaced.second) { mFreeItems.erase(iterator); - throw InvalidArgument("Set existing cache value"); + acquireItemUnsafe(emplaced.first->second); + ++mGetCount; + ++mHitCount; + return Value(*this, emplaced.first->second); } - iterator->mNavMeshData = std::move(value); + iterator->mPreparedNavMeshData = std::move(value); + ++iterator->mUseCount; mUsedNavMeshDataSize += itemSize; - mFreeNavMeshDataSize += itemSize; - - acquireItemUnsafe(iterator); + mBusyItems.splice(mBusyItems.end(), mFreeItems, iterator); return Value(*this, iterator); } - void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& stats) const + NavMeshTilesCache::Stats NavMeshTilesCache::getStats() const { - std::size_t navMeshCacheSize = 0; - std::size_t usedNavMeshTiles = 0; - std::size_t cachedNavMeshTiles = 0; - + Stats result; { const std::lock_guard lock(mMutex); - navMeshCacheSize = mUsedNavMeshDataSize; - usedNavMeshTiles = mBusyItems.size(); - cachedNavMeshTiles = mFreeItems.size(); + result.mNavMeshCacheSize = mUsedNavMeshDataSize; + result.mUsedNavMeshTiles = mBusyItems.size(); + result.mCachedNavMeshTiles = mFreeItems.size(); + result.mHitCount = mHitCount; + result.mGetCount = mGetCount; } + return result; + } - stats.setAttribute(frameNumber, "NavMesh CacheSize", navMeshCacheSize); - stats.setAttribute(frameNumber, "NavMesh UsedTiles", usedNavMeshTiles); - stats.setAttribute(frameNumber, "NavMesh CachedTiles", cachedNavMeshTiles); + void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out) + { + out.setAttribute(frameNumber, "NavMesh CacheSize", static_cast(stats.mNavMeshCacheSize)); + out.setAttribute(frameNumber, "NavMesh UsedTiles", static_cast(stats.mUsedNavMeshTiles)); + out.setAttribute(frameNumber, "NavMesh CachedTiles", static_cast(stats.mCachedNavMeshTiles)); + if (stats.mGetCount > 0) + out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); } void NavMeshTilesCache::removeLeastRecentlyUsed() { const auto& item = mFreeItems.back(); - const auto agentValues = mValues.find(item.mAgentHalfExtents); - if (agentValues == mValues.end()) + const auto value = mValues.find(std::tie(item.mAgentBounds, item.mChangedTile, item.mRecastMeshData)); + if (value == mValues.end()) return; - const auto tileValues = agentValues->second.find(item.mChangedTile); - if (tileValues == agentValues->second.end()) - return; + mUsedNavMeshDataSize -= item.mSize; + mFreeNavMeshDataSize -= item.mSize; - const auto value = tileValues->second.mMap.find(item.mNavMeshKey); - if (value == tileValues->second.mMap.end()) - return; - - mUsedNavMeshDataSize -= getSize(item); - mFreeNavMeshDataSize -= getSize(item); - - tileValues->second.mMap.erase(value); + mValues.erase(value); mFreeItems.pop_back(); - - if (!tileValues->second.mMap.empty()) - return; - - agentValues->second.erase(tileValues); - if (!agentValues->second.empty()) - return; - - mValues.erase(agentValues); } void NavMeshTilesCache::acquireItemUnsafe(ItemIterator iterator) @@ -161,7 +109,7 @@ namespace DetourNavigator return; mBusyItems.splice(mBusyItems.end(), mFreeItems, iterator); - mFreeNavMeshDataSize -= getSize(*iterator); + mFreeNavMeshDataSize -= iterator->mSize; } void NavMeshTilesCache::releaseItem(ItemIterator iterator) @@ -172,71 +120,6 @@ namespace DetourNavigator const std::lock_guard lock(mMutex); mFreeItems.splice(mFreeItems.begin(), mBusyItems, iterator); - mFreeNavMeshDataSize += getSize(*iterator); - } - - namespace - { - struct CompareBytes - { - const unsigned char* mRhsIt; - const unsigned char* const mRhsEnd; - - template - int operator ()(const std::vector& lhs) - { - const auto lhsBegin = reinterpret_cast(lhs.data()); - const auto lhsEnd = reinterpret_cast(lhs.data() + lhs.size()); - const auto lhsSize = static_cast(lhsEnd - lhsBegin); - const auto rhsSize = static_cast(mRhsEnd - mRhsIt); - - if (lhsBegin == nullptr || mRhsIt == nullptr) - { - if (lhsSize < rhsSize) - return -1; - else if (lhsSize > rhsSize) - return 1; - else - return 0; - } - - const auto size = std::min(lhsSize, rhsSize); - - if (const auto result = std::memcmp(lhsBegin, mRhsIt, size)) - return result; - - if (lhsSize > rhsSize) - return 1; - - mRhsIt += size; - - return 0; - } - }; - } - - int NavMeshTilesCache::RecastMeshKeyView::compare(const std::vector& other) const - { - CompareBytes compareBytes {other.data(), other.data() + other.size()}; - - if (const auto result = compareBytes(mRecastMesh.get().getIndices())) - return result; - - if (const auto result = compareBytes(mRecastMesh.get().getVertices())) - return result; - - if (const auto result = compareBytes(mRecastMesh.get().getAreaTypes())) - return result; - - if (const auto result = compareBytes(mRecastMesh.get().getWater())) - return result; - - if (const auto result = compareBytes(mOffMeshConnections.get())) - return result; - - if (compareBytes.mRhsIt < compareBytes.mRhsEnd) - return -1; - - return 0; + mFreeNavMeshDataSize += iterator->mSize; } } diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index 064d9e185b..572f7fdb4b 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -1,10 +1,10 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H -#include "offmeshconnection.hpp" -#include "navmeshdata.hpp" +#include "preparednavmeshdata.hpp" #include "recastmesh.hpp" #include "tileposition.hpp" +#include "agentbounds.hpp" #include #include @@ -21,28 +21,51 @@ namespace osg namespace DetourNavigator { - struct NavMeshDataRef + struct RecastMeshData { - unsigned char* mValue; - int mSize; + Mesh mMesh; + std::vector mWater; + std::vector mHeightfields; + std::vector mFlatHeightfields; }; + inline bool operator <(const RecastMeshData& lhs, const RecastMeshData& rhs) + { + return std::tie(lhs.mMesh, lhs.mWater, lhs.mHeightfields, lhs.mFlatHeightfields) + < std::tie(rhs.mMesh, rhs.mWater, rhs.mHeightfields, rhs.mFlatHeightfields); + } + + inline bool operator <(const RecastMeshData& lhs, const RecastMesh& rhs) + { + return std::tie(lhs.mMesh, lhs.mWater, lhs.mHeightfields, lhs.mFlatHeightfields) + < std::tie(rhs.getMesh(), rhs.getWater(), rhs.getHeightfields(), rhs.getFlatHeightfields()); + } + + inline bool operator <(const RecastMesh& lhs, const RecastMeshData& rhs) + { + return std::tie(lhs.getMesh(), lhs.getWater(), lhs.getHeightfields(), lhs.getFlatHeightfields()) + < std::tie(rhs.mMesh, rhs.mWater, rhs.mHeightfields, rhs.mFlatHeightfields); + } + class NavMeshTilesCache { public: struct Item { std::atomic mUseCount; - osg::Vec3f mAgentHalfExtents; + AgentBounds mAgentBounds; TilePosition mChangedTile; - std::vector mNavMeshKey; - NavMeshData mNavMeshData; + RecastMeshData mRecastMeshData; + std::unique_ptr mPreparedNavMeshData; + std::size_t mSize; - Item(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, std::vector&& navMeshKey) + Item(const AgentBounds& agentBounds, const TilePosition& changedTile, + RecastMeshData&& recastMeshData, std::size_t size) : mUseCount(0) - , mAgentHalfExtents(agentHalfExtents) + , mAgentBounds(agentBounds) , mChangedTile(changedTile) - , mNavMeshKey(std::move(navMeshKey)) + , mRecastMeshData(std::move(recastMeshData)) + , mSize(size) {} }; @@ -88,9 +111,9 @@ namespace DetourNavigator return *this; } - NavMeshDataRef get() const + const PreparedNavMeshData& get() const { - return NavMeshDataRef {mIterator->mNavMeshData.mValue.get(), mIterator->mNavMeshData.mSize}; + return *mIterator->mPreparedNavMeshData; } operator bool() const @@ -103,112 +126,44 @@ namespace DetourNavigator ItemIterator mIterator; }; - NavMeshTilesCache(const std::size_t maxNavMeshDataSize); - - Value get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections); - - Value set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections, - NavMeshData&& value); - - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; - - private: - class KeyView + struct Stats { - public: - KeyView() = default; - - virtual ~KeyView() = default; - - KeyView(const std::vector& value) - : mValue(&value) {} - - const std::vector& getValue() const - { - assert(mValue); - return *mValue; - } - - virtual int compare(const std::vector& other) const - { - assert(mValue); - - const auto valueSize = mValue->size(); - const auto otherSize = other.size(); - - if (const auto result = std::memcmp(mValue->data(), other.data(), std::min(valueSize, otherSize))) - return result; - - if (valueSize < otherSize) - return -1; - - if (valueSize > otherSize) - return 1; - - return 0; - } - - virtual bool isLess(const KeyView& other) const - { - assert(mValue); - return other.compare(*mValue) > 0; - } - - friend bool operator <(const KeyView& lhs, const KeyView& rhs) - { - return lhs.isLess(rhs); - } - - private: - const std::vector* mValue = nullptr; + std::size_t mNavMeshCacheSize; + std::size_t mUsedNavMeshTiles; + std::size_t mCachedNavMeshTiles; + std::size_t mHitCount; + std::size_t mGetCount; }; - class RecastMeshKeyView : public KeyView - { - public: - RecastMeshKeyView(const RecastMesh& recastMesh, const std::vector& offMeshConnections) - : mRecastMesh(recastMesh), mOffMeshConnections(offMeshConnections) {} - - int compare(const std::vector& other) const override; - - bool isLess(const KeyView& other) const override - { - return compare(other.getValue()) < 0; - } + NavMeshTilesCache(const std::size_t maxNavMeshDataSize); - virtual ~RecastMeshKeyView() = default; + Value get(const AgentBounds& agentBounds, const TilePosition& changedTile, + const RecastMesh& recastMesh); - private: - std::reference_wrapper mRecastMesh; - std::reference_wrapper> mOffMeshConnections; - }; + Value set(const AgentBounds& agentBounds, const TilePosition& changedTile, + const RecastMesh& recastMesh, std::unique_ptr&& value); - struct TileMap - { - std::map mMap; - }; + Stats getStats() const; + private: mutable std::mutex mMutex; std::size_t mMaxNavMeshDataSize; std::size_t mUsedNavMeshDataSize; std::size_t mFreeNavMeshDataSize; + std::size_t mHitCount; + std::size_t mGetCount; std::list mBusyItems; std::list mFreeItems; - std::map> mValues; + std::map>, ItemIterator, std::less<>> mValues; void removeLeastRecentlyUsed(); void acquireItemUnsafe(ItemIterator iterator); void releaseItem(ItemIterator iterator); - - static std::size_t getSize(const Item& item) - { - return static_cast(item.mNavMeshData.mSize) + 2 * item.mNavMeshKey.size(); - } }; + + void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out); } #endif diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp new file mode 100644 index 0000000000..d12bcecd7e --- /dev/null +++ b/components/detournavigator/navmeshtileview.cpp @@ -0,0 +1,157 @@ +#include "navmeshtileview.hpp" +#include "ref.hpp" + +#include +#include + +#include +#include +#include +#include + +inline bool operator==(const dtMeshHeader& lhs, const dtMeshHeader& rhs) noexcept +{ + const auto makeTuple = [] (const dtMeshHeader& v) + { + using DetourNavigator::ArrayRef; + return std::tuple( + v.x, + v.y, + v.layer, + v.userId, + v.polyCount, + v.vertCount, + v.maxLinkCount, + v.detailMeshCount, + v.detailVertCount, + v.detailTriCount, + v.bvNodeCount, + v.offMeshConCount, + v.offMeshBase, + v.walkableHeight, + v.walkableRadius, + v.walkableClimb, + v.detailVertCount, + ArrayRef(v.bmin), + ArrayRef(v.bmax), + v.bvQuantFactor + ); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +inline bool operator==(const dtPoly& lhs, const dtPoly& rhs) noexcept +{ + const auto makeTuple = [] (const dtPoly& v) + { + using DetourNavigator::ArrayRef; + return std::tuple(ArrayRef(v.verts), ArrayRef(v.neis), v.flags, v.vertCount, v.areaAndtype); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +inline bool operator==(const dtPolyDetail& lhs, const dtPolyDetail& rhs) noexcept +{ + const auto makeTuple = [] (const dtPolyDetail& v) + { + return std::tuple(v.vertBase, v.triBase, v.vertCount, v.triCount); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +inline bool operator==(const dtBVNode& lhs, const dtBVNode& rhs) noexcept +{ + const auto makeTuple = [] (const dtBVNode& v) + { + using DetourNavigator::ArrayRef; + return std::tuple(ArrayRef(v.bmin), ArrayRef(v.bmax), v.i); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +inline bool operator==(const dtOffMeshConnection& lhs, const dtOffMeshConnection& rhs) noexcept +{ + const auto makeTuple = [] (const dtOffMeshConnection& v) + { + using DetourNavigator::ArrayRef; + return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +namespace DetourNavigator +{ + NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data) + { + const dtMeshHeader* header = reinterpret_cast(data); + + if (header->magic != DT_NAVMESH_MAGIC) + throw std::logic_error("Invalid navmesh magic"); + + if (header->version != DT_NAVMESH_VERSION) + throw std::logic_error("Invalid navmesh version"); + + // Similar code to https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/Detour/Source/DetourNavMesh.cpp#L978-L996 + const int headerSize = dtAlign4(sizeof(dtMeshHeader)); + const int vertsSize = dtAlign4(sizeof(float) * 3 * header->vertCount); + const int polysSize = dtAlign4(sizeof(dtPoly) * header->polyCount); + const int linksSize = dtAlign4(sizeof(dtLink) * (header->maxLinkCount)); + const int detailMeshesSize = dtAlign4(sizeof(dtPolyDetail) * header->detailMeshCount); + const int detailVertsSize = dtAlign4(sizeof(float) * 3 * header->detailVertCount); + const int detailTrisSize = dtAlign4(sizeof(unsigned char) * 4 * header->detailTriCount); + const int bvtreeSize = dtAlign4(sizeof(dtBVNode) * header->bvNodeCount); + const int offMeshLinksSize = dtAlign4(sizeof(dtOffMeshConnection) * header->offMeshConCount); + + const unsigned char* ptr = data + headerSize; + + NavMeshTileConstView view; + + view.mHeader = header; + view.mVerts = dtGetThenAdvanceBufferPointer(ptr, vertsSize); + view.mPolys = dtGetThenAdvanceBufferPointer(ptr, polysSize); + ptr += linksSize; + view.mDetailMeshes = dtGetThenAdvanceBufferPointer(ptr, detailMeshesSize); + view.mDetailVerts = dtGetThenAdvanceBufferPointer(ptr, detailVertsSize); + view.mDetailTris = dtGetThenAdvanceBufferPointer(ptr, detailTrisSize); + view.mBvTree = dtGetThenAdvanceBufferPointer(ptr, bvtreeSize); + view.mOffMeshCons = dtGetThenAdvanceBufferPointer(ptr, offMeshLinksSize); + + return view; + } + + NavMeshTileConstView asNavMeshTileConstView(const dtMeshTile& tile) + { + NavMeshTileConstView view; + + view.mHeader = tile.header; + view.mPolys = tile.polys; + view.mVerts = tile.verts; + view.mDetailMeshes = tile.detailMeshes; + view.mDetailVerts = tile.detailVerts; + view.mDetailTris = tile.detailTris; + view.mBvTree = tile.bvTree; + view.mOffMeshCons = tile.offMeshCons; + + return view; + } + + bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept + { + using DetourNavigator::Ref; + using DetourNavigator::Span; + const auto makeTuple = [] (const DetourNavigator::NavMeshTileConstView& v) + { + return std::tuple( + Ref(*v.mHeader), + Span(v.mPolys, v.mHeader->polyCount), + Span(v.mVerts, v.mHeader->vertCount), + Span(v.mDetailMeshes, v.mHeader->detailMeshCount), + Span(v.mDetailVerts, v.mHeader->detailVertCount), + Span(v.mDetailTris, v.mHeader->detailTriCount), + Span(v.mBvTree, v.mHeader->bvNodeCount), + Span(v.mOffMeshCons, v.mHeader->offMeshConCount) + ); + }; + return makeTuple(lhs) == makeTuple(rhs); + } +} diff --git a/components/detournavigator/navmeshtileview.hpp b/components/detournavigator/navmeshtileview.hpp new file mode 100644 index 0000000000..b797545b8a --- /dev/null +++ b/components/detournavigator/navmeshtileview.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILEVIEW_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILEVIEW_H + +struct dtMeshHeader; +struct dtPoly; +struct dtPolyDetail; +struct dtBVNode; +struct dtOffMeshConnection; +struct dtMeshTile; + +namespace DetourNavigator +{ + struct NavMeshTileConstView + { + const dtMeshHeader* mHeader; + const dtPoly* mPolys; + const float* mVerts; + const dtPolyDetail* mDetailMeshes; + const float* mDetailVerts; + const unsigned char* mDetailTris; + const dtBVNode* mBvTree; + const dtOffMeshConnection* mOffMeshCons; + + friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept; + }; + + NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data); + NavMeshTileConstView asNavMeshTileConstView(const dtMeshTile& tile); +} + +#endif diff --git a/components/detournavigator/objectid.hpp b/components/detournavigator/objectid.hpp index 9c4b5b2710..22fc792c6f 100644 --- a/components/detournavigator/objectid.hpp +++ b/components/detournavigator/objectid.hpp @@ -15,6 +15,11 @@ namespace DetourNavigator { } + explicit ObjectId(std::size_t value) noexcept + : mValue(value) + { + } + std::size_t value() const noexcept { return mValue; diff --git a/components/detournavigator/objecttransform.hpp b/components/detournavigator/objecttransform.hpp new file mode 100644 index 0000000000..2da9a25348 --- /dev/null +++ b/components/detournavigator/objecttransform.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTTRANSFORM_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTTRANSFORM_H + +#include + +#include + +namespace DetourNavigator +{ + struct ObjectTransform + { + ESM::Position mPosition; + float mScale; + + friend inline auto tie(const ObjectTransform& v) + { + return std::tie(v.mPosition, v.mScale); + } + + friend inline bool operator<(const ObjectTransform& l, const ObjectTransform& r) + { + return tie(l) < tie(r); + } + }; +} + +#endif diff --git a/components/detournavigator/offmeshconnection.hpp b/components/detournavigator/offmeshconnection.hpp index ca999dbdb9..9312bc6c48 100644 --- a/components/detournavigator/offmeshconnection.hpp +++ b/components/detournavigator/offmeshconnection.hpp @@ -5,6 +5,9 @@ #include +#include +#include + namespace DetourNavigator { struct OffMeshConnection @@ -13,6 +16,11 @@ namespace DetourNavigator osg::Vec3f mEnd; AreaType mAreaType; }; + + inline bool operator<(const OffMeshConnection& lhs, const OffMeshConnection& rhs) + { + return std::tie(lhs.mStart, lhs.mEnd, lhs.mAreaType) < std::tie(rhs.mStart, rhs.mEnd, rhs.mAreaType); + } } #endif diff --git a/components/detournavigator/offmeshconnectionsmanager.cpp b/components/detournavigator/offmeshconnectionsmanager.cpp new file mode 100644 index 0000000000..a11da21218 --- /dev/null +++ b/components/detournavigator/offmeshconnectionsmanager.cpp @@ -0,0 +1,95 @@ +#include "offmeshconnectionsmanager.hpp" +#include "settings.hpp" +#include "settingsutils.hpp" +#include "tileposition.hpp" +#include "objectid.hpp" +#include "offmeshconnection.hpp" + +#include +#include +#include + +namespace DetourNavigator +{ + OffMeshConnectionsManager::OffMeshConnectionsManager(const RecastSettings& settings) + : mSettings(settings) + {} + + void OffMeshConnectionsManager::add(const ObjectId id, const OffMeshConnection& value) + { + const auto values = mValues.lock(); + + values->mById.insert(std::make_pair(id, value)); + + const auto startTilePosition = getTilePosition(mSettings, value.mStart); + const auto endTilePosition = getTilePosition(mSettings, value.mEnd); + + values->mByTilePosition[startTilePosition].insert(id); + + if (startTilePosition != endTilePosition) + values->mByTilePosition[endTilePosition].insert(id); + } + + std::set OffMeshConnectionsManager::remove(const ObjectId id) + { + const auto values = mValues.lock(); + + const auto byId = values->mById.equal_range(id); + + if (byId.first == byId.second) + return {}; + + std::set removed; + + std::for_each(byId.first, byId.second, [&] (const auto& v) { + const auto startTilePosition = getTilePosition(mSettings, v.second.mStart); + const auto endTilePosition = getTilePosition(mSettings, v.second.mEnd); + + removed.emplace(startTilePosition); + if (startTilePosition != endTilePosition) + removed.emplace(endTilePosition); + }); + + for (const TilePosition& tilePosition : removed) + { + const auto it = values->mByTilePosition.find(tilePosition); + if (it == values->mByTilePosition.end()) + continue; + it->second.erase(id); + if (it->second.empty()) + values->mByTilePosition.erase(it); + } + + values->mById.erase(byId.first, byId.second); + + return removed; + } + + std::vector OffMeshConnectionsManager::get(const TilePosition& tilePosition) const + { + std::vector result; + + const auto values = mValues.lockConst(); + + const auto itByTilePosition = values->mByTilePosition.find(tilePosition); + + if (itByTilePosition == values->mByTilePosition.end()) + return result; + + std::for_each(itByTilePosition->second.begin(), itByTilePosition->second.end(), + [&] (const ObjectId v) + { + const auto byId = values->mById.equal_range(v); + std::for_each(byId.first, byId.second, [&] (const auto& v) + { + if (getTilePosition(mSettings, v.second.mStart) == tilePosition + || getTilePosition(mSettings, v.second.mEnd) == tilePosition) + result.push_back(v.second); + }); + }); + + std::sort(result.begin(), result.end()); + + return result; + } +} diff --git a/components/detournavigator/offmeshconnectionsmanager.hpp b/components/detournavigator/offmeshconnectionsmanager.hpp index de707f3a86..455b03276a 100644 --- a/components/detournavigator/offmeshconnectionsmanager.hpp +++ b/components/detournavigator/offmeshconnectionsmanager.hpp @@ -2,16 +2,12 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_OFFMESHCONNECTIONSMANAGER_H #include "settings.hpp" -#include "settingsutils.hpp" #include "tileposition.hpp" #include "objectid.hpp" #include "offmeshconnection.hpp" #include -#include - -#include #include #include #include @@ -22,71 +18,13 @@ namespace DetourNavigator class OffMeshConnectionsManager { public: - OffMeshConnectionsManager(const Settings& settings) - : mSettings(settings) - {} - - void add(const ObjectId id, const OffMeshConnection& value) - { - const auto values = mValues.lock(); - - values->mById.insert(std::make_pair(id, value)); - - const auto startTilePosition = getTilePosition(mSettings, value.mStart); - const auto endTilePosition = getTilePosition(mSettings, value.mEnd); - - values->mByTilePosition[startTilePosition].insert(id); - - if (startTilePosition != endTilePosition) - values->mByTilePosition[endTilePosition].insert(id); - } - - std::set remove(const ObjectId id) - { - const auto values = mValues.lock(); - - const auto byId = values->mById.equal_range(id); - - if (byId.first == byId.second) { - return {}; - } + explicit OffMeshConnectionsManager(const RecastSettings& settings); - std::set removed; + void add(const ObjectId id, const OffMeshConnection& value); - std::for_each(byId.first, byId.second, [&] (const auto& v) { - const auto startTilePosition = getTilePosition(mSettings, v.second.mStart); - const auto endTilePosition = getTilePosition(mSettings, v.second.mEnd); + std::set remove(const ObjectId id); - removed.emplace(startTilePosition); - if (startTilePosition != endTilePosition) - removed.emplace(endTilePosition); - }); - - values->mById.erase(byId.first, byId.second); - - return removed; - } - - std::vector get(const TilePosition& tilePosition) - { - std::vector result; - - const auto values = mValues.lock(); - - const auto itByTilePosition = values->mByTilePosition.find(tilePosition); - - if (itByTilePosition == values->mByTilePosition.end()) - return result; - - std::for_each(itByTilePosition->second.begin(), itByTilePosition->second.end(), - [&] (const ObjectId v) - { - const auto byId = values->mById.equal_range(v); - std::for_each(byId.first, byId.second, [&] (const auto& v) { result.push_back(v.second); }); - }); - - return result; - } + std::vector get(const TilePosition& tilePosition) const; private: struct Values @@ -95,16 +33,8 @@ namespace DetourNavigator std::map> mByTilePosition; }; - const Settings& mSettings; + const RecastSettings& mSettings; Misc::ScopeGuarded mValues; - - void removeByTilePosition(std::map>& valuesByTilePosition, - const TilePosition& tilePosition, const ObjectId id) - { - const auto it = valuesByTilePosition.find(tilePosition); - if (it != valuesByTilePosition.end()) - it->second.erase(id); - } }; } diff --git a/components/detournavigator/oscillatingrecastmeshobject.cpp b/components/detournavigator/oscillatingrecastmeshobject.cpp new file mode 100644 index 0000000000..fbe4b77ffd --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.cpp @@ -0,0 +1,57 @@ +#include "oscillatingrecastmeshobject.hpp" +#include "tilebounds.hpp" + +#include + +#include + +namespace DetourNavigator +{ + namespace + { + void limitBy(btAABB& aabb, const TileBounds& bounds) + { + aabb.m_min.setX(std::max(aabb.m_min.x(), static_cast(bounds.mMin.x()))); + aabb.m_min.setY(std::max(aabb.m_min.y(), static_cast(bounds.mMin.y()))); + aabb.m_max.setX(std::min(aabb.m_max.x(), static_cast(bounds.mMax.x()))); + aabb.m_max.setY(std::min(aabb.m_max.y(), static_cast(bounds.mMax.y()))); + } + } + + OscillatingRecastMeshObject::OscillatingRecastMeshObject(RecastMeshObject&& impl, std::size_t lastChangeRevision) + : mImpl(std::move(impl)) + , mLastChangeRevision(lastChangeRevision) + , mAabb(BulletHelpers::getAabb(mImpl.getShape(), mImpl.getTransform())) + { + } + + OscillatingRecastMeshObject::OscillatingRecastMeshObject(const RecastMeshObject& impl, std::size_t lastChangeRevision) + : mImpl(impl) + , mLastChangeRevision(lastChangeRevision) + , mAabb(BulletHelpers::getAabb(mImpl.getShape(), mImpl.getTransform())) + { + } + + bool OscillatingRecastMeshObject::update(const btTransform& transform, const AreaType areaType, + std::size_t lastChangeRevision, const TileBounds& bounds) + { + const btTransform oldTransform = mImpl.getTransform(); + if (!mImpl.update(transform, areaType)) + return false; + if (transform == oldTransform) + return true; + if (mLastChangeRevision != lastChangeRevision) + { + mLastChangeRevision = lastChangeRevision; + // btAABB doesn't have copy-assignment operator + const btAABB aabb = BulletHelpers::getAabb(mImpl.getShape(), transform); + mAabb.m_min = aabb.m_min; + mAabb.m_max = aabb.m_max; + return true; + } + const btAABB currentAabb = mAabb; + mAabb.merge(BulletHelpers::getAabb(mImpl.getShape(), transform)); + limitBy(mAabb, bounds); + return currentAabb != mAabb; + } +} diff --git a/components/detournavigator/oscillatingrecastmeshobject.hpp b/components/detournavigator/oscillatingrecastmeshobject.hpp new file mode 100644 index 0000000000..f8aabce628 --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H + +#include "areatype.hpp" +#include "recastmeshobject.hpp" +#include "tilebounds.hpp" + +#include +#include + +namespace DetourNavigator +{ + class OscillatingRecastMeshObject + { + public: + explicit OscillatingRecastMeshObject(RecastMeshObject&& impl, std::size_t lastChangeRevision); + explicit OscillatingRecastMeshObject(const RecastMeshObject& impl, std::size_t lastChangeRevision); + + bool update(const btTransform& transform, const AreaType areaType, std::size_t lastChangeRevision, + const TileBounds& bounds); + + const RecastMeshObject& getImpl() const { return mImpl; } + + private: + RecastMeshObject mImpl; + std::size_t mLastChangeRevision; + btAABB mAabb; + }; +} + +#endif diff --git a/components/detournavigator/preparednavmeshdata.cpp b/components/detournavigator/preparednavmeshdata.cpp new file mode 100644 index 0000000000..a737ae19a5 --- /dev/null +++ b/components/detournavigator/preparednavmeshdata.cpp @@ -0,0 +1,47 @@ +#include "preparednavmeshdata.hpp" +#include "preparednavmeshdatatuple.hpp" +#include "recast.hpp" + +#include + +#include + +namespace +{ + void initPolyMeshDetail(rcPolyMeshDetail& value) noexcept + { + value.meshes = nullptr; + value.verts = nullptr; + value.tris = nullptr; + value.nmeshes = 0; + value.nverts = 0; + value.ntris = 0; + } +} + +namespace DetourNavigator +{ + PreparedNavMeshData::PreparedNavMeshData() noexcept + { + initPolyMeshDetail(mPolyMeshDetail); + } + + PreparedNavMeshData::PreparedNavMeshData(const PreparedNavMeshData& other) + : mUserId(other.mUserId) + , mCellSize(other.mCellSize) + , mCellHeight(other.mCellHeight) + { + copyPolyMesh(other.mPolyMesh, mPolyMesh); + copyPolyMeshDetail(other.mPolyMeshDetail, mPolyMeshDetail); + } + + PreparedNavMeshData::~PreparedNavMeshData() noexcept + { + freePolyMeshDetail(mPolyMeshDetail); + } + + bool operator==(const PreparedNavMeshData& lhs, const PreparedNavMeshData& rhs) noexcept + { + return makeTuple(lhs) == makeTuple(rhs); + } +} diff --git a/components/detournavigator/preparednavmeshdata.hpp b/components/detournavigator/preparednavmeshdata.hpp new file mode 100644 index 0000000000..b3de7a447f --- /dev/null +++ b/components/detournavigator/preparednavmeshdata.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATA_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATA_H + +#include "recast.hpp" + +#include + +#include + +namespace DetourNavigator +{ + struct PreparedNavMeshData + { + unsigned int mUserId = 0; + float mCellSize = 0; + float mCellHeight = 0; + rcPolyMesh mPolyMesh; + rcPolyMeshDetail mPolyMeshDetail; + + PreparedNavMeshData() noexcept; + PreparedNavMeshData(const PreparedNavMeshData& other); + + ~PreparedNavMeshData() noexcept; + + friend bool operator==(const PreparedNavMeshData& lhs, const PreparedNavMeshData& rhs) noexcept; + }; + + inline constexpr std::size_t getSize(const rcPolyMesh& value) noexcept + { + return getVertsLength(value) * sizeof(*value.verts) + + getPolysLength(value) * sizeof(*value.polys) + + getRegsLength(value) * sizeof(*value.regs) + + getFlagsLength(value) * sizeof(*value.flags) + + getAreasLength(value) * sizeof(*value.areas); + } + + inline constexpr std::size_t getSize(const rcPolyMeshDetail& value) noexcept + { + return getMeshesLength(value) * sizeof(*value.meshes) + + getVertsLength(value) * sizeof(*value.verts) + + getTrisLength(value) * sizeof(*value.tris); + } + + inline constexpr std::size_t getSize(const PreparedNavMeshData& value) noexcept + { + return getSize(value.mPolyMesh) + getSize(value.mPolyMeshDetail); + } +} + +#endif diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp new file mode 100644 index 0000000000..03b192ad38 --- /dev/null +++ b/components/detournavigator/preparednavmeshdatatuple.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATATUPLE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATATUPLE_H + +#include "preparednavmeshdata.hpp" +#include "ref.hpp" +#include "recast.hpp" + +#include + +#include + +inline bool operator==(const rcPolyMesh& lhs, const rcPolyMesh& rhs) noexcept +{ + const auto makeTuple = [] (const rcPolyMesh& v) + { + using namespace DetourNavigator; + return std::tuple( + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.polys, static_cast(getPolysLength(v))), + Span(v.regs, static_cast(getRegsLength(v))), + Span(v.flags, static_cast(getFlagsLength(v))), + Span(v.areas, static_cast(getAreasLength(v))), + ArrayRef(v.bmin), + ArrayRef(v.bmax), + v.cs, + v.ch, + v.borderSize, + v.maxEdgeError + ); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +inline bool operator==(const rcPolyMeshDetail& lhs, const rcPolyMeshDetail& rhs) noexcept +{ + const auto makeTuple = [] (const rcPolyMeshDetail& v) + { + using namespace DetourNavigator; + return std::tuple( + Span(v.meshes, static_cast(getMeshesLength(v))), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.tris, static_cast(getTrisLength(v))) + ); + }; + return makeTuple(lhs) == makeTuple(rhs); +} + +namespace DetourNavigator +{ + inline auto makeTuple(const PreparedNavMeshData& v) noexcept + { + return std::tuple( + v.mUserId, + v.mCellHeight, + v.mCellSize, + Ref(v.mPolyMesh), + Ref(v.mPolyMeshDetail) + ); + } +} + +#endif diff --git a/components/detournavigator/raycast.cpp b/components/detournavigator/raycast.cpp new file mode 100644 index 0000000000..be3217ba40 --- /dev/null +++ b/components/detournavigator/raycast.cpp @@ -0,0 +1,43 @@ +#include "raycast.hpp" +#include "settings.hpp" +#include "findsmoothpath.hpp" + +#include +#include + +#include + +namespace DetourNavigator +{ + std::optional raycast(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, + const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const DetourSettings& settings) + { + dtNavMeshQuery navMeshQuery; + if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes)) + return {}; + + dtQueryFilter queryFilter; + queryFilter.setIncludeFlags(includeFlags); + + dtPolyRef ref = 0; + if (dtStatus status = navMeshQuery.findNearestPoly(start.ptr(), halfExtents.ptr(), &queryFilter, &ref, nullptr); + dtStatusFailed(status) || ref == 0) + return {}; + + const unsigned options = 0; + std::array path; + dtRaycastHit hit; + hit.path = path.data(); + hit.maxPath = path.size(); + if (dtStatus status = navMeshQuery.raycast(ref, start.ptr(), end.ptr(), &queryFilter, options, &hit); + dtStatusFailed(status) || hit.pathCount == 0) + return {}; + + osg::Vec3f hitPosition; + if (dtStatus status = navMeshQuery.closestPointOnPoly(path[hit.pathCount - 1], end.ptr(), hitPosition.ptr(), nullptr); + dtStatusFailed(status)) + return {}; + + return hitPosition; + } +} diff --git a/components/detournavigator/raycast.hpp b/components/detournavigator/raycast.hpp new file mode 100644 index 0000000000..60cdf0a157 --- /dev/null +++ b/components/detournavigator/raycast.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RAYCAST_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RAYCAST_H + +#include "flags.hpp" + +#include +#include + +class dtNavMesh; + +namespace DetourNavigator +{ + struct DetourSettings; + + std::optional raycast(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, + const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const DetourSettings& settings); +} + +#endif diff --git a/components/detournavigator/recast.cpp b/components/detournavigator/recast.cpp new file mode 100644 index 0000000000..c1d14c0aa8 --- /dev/null +++ b/components/detournavigator/recast.cpp @@ -0,0 +1,80 @@ +#include "recast.hpp" + +#include +#include + +#include +#include + +namespace DetourNavigator +{ + void* permRecastAlloc(std::size_t size) + { + void* const result = rcAlloc(size, RC_ALLOC_PERM); + if (result == nullptr) + throw std::bad_alloc(); + return result; + } + + void permRecastAlloc(rcPolyMesh& value) + { + permRecastAlloc(value.verts, getVertsLength(value)); + permRecastAlloc(value.polys, getPolysLength(value)); + permRecastAlloc(value.regs, getRegsLength(value)); + permRecastAlloc(value.flags, getFlagsLength(value)); + permRecastAlloc(value.areas, getAreasLength(value)); + } + + void permRecastAlloc(rcPolyMeshDetail& value) + { + try + { + permRecastAlloc(value.meshes, getMeshesLength(value)); + permRecastAlloc(value.verts, getVertsLength(value)); + permRecastAlloc(value.tris, getTrisLength(value)); + } + catch (...) + { + freePolyMeshDetail(value); + throw; + } + } + + void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept + { + rcFree(value.meshes); + rcFree(value.verts); + rcFree(value.tris); + } + + void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst) + { + dst.nverts = src.nverts; + dst.npolys = src.npolys; + dst.maxpolys = src.maxpolys; + dst.nvp = src.nvp; + rcVcopy(dst.bmin, src.bmin); + rcVcopy(dst.bmax, src.bmax); + dst.cs = src.cs; + dst.ch = src.ch; + dst.borderSize = src.borderSize; + dst.maxEdgeError = src.maxEdgeError; + permRecastAlloc(dst); + std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts)); + std::memcpy(dst.polys, src.polys, getPolysLength(src) * sizeof(*dst.polys)); + std::memcpy(dst.regs, src.regs, getRegsLength(src) * sizeof(*dst.regs)); + std::memcpy(dst.flags, src.flags, getFlagsLength(src) * sizeof(*dst.flags)); + std::memcpy(dst.areas, src.areas, getAreasLength(src) * sizeof(*dst.areas)); + } + + void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst) + { + dst.nmeshes = src.nmeshes; + dst.nverts = src.nverts; + dst.ntris = src.ntris; + permRecastAlloc(dst); + std::memcpy(dst.meshes, src.meshes, getMeshesLength(src) * sizeof(*dst.meshes)); + std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts)); + std::memcpy(dst.tris, src.tris, getTrisLength(src) * sizeof(*dst.tris)); + } +} diff --git a/components/detournavigator/recast.hpp b/components/detournavigator/recast.hpp new file mode 100644 index 0000000000..1811d35772 --- /dev/null +++ b/components/detournavigator/recast.hpp @@ -0,0 +1,72 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H + +#include +#include + +#include +#include + +namespace DetourNavigator +{ + constexpr std::size_t getVertsLength(const rcPolyMesh& value) noexcept + { + return 3 * static_cast(value.nverts); + } + + constexpr std::size_t getPolysLength(const rcPolyMesh& value) noexcept + { + return 2 * static_cast(value.maxpolys * value.nvp); + } + + constexpr std::size_t getRegsLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.maxpolys); + } + + constexpr std::size_t getFlagsLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.npolys); + } + + constexpr std::size_t getAreasLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.maxpolys); + } + + constexpr std::size_t getMeshesLength(const rcPolyMeshDetail& value) noexcept + { + return 4 * static_cast(value.nmeshes); + } + + constexpr std::size_t getVertsLength(const rcPolyMeshDetail& value) noexcept + { + return 3 * static_cast(value.nverts); + } + + constexpr std::size_t getTrisLength(const rcPolyMeshDetail& value) noexcept + { + return 4 * static_cast(value.ntris); + } + + void* permRecastAlloc(std::size_t size); + + template + inline void permRecastAlloc(T*& values, std::size_t size) + { + static_assert(std::is_arithmetic_v); + values = new (permRecastAlloc(size * sizeof(T))) T[size]; + } + + void permRecastAlloc(rcPolyMesh& value); + + void permRecastAlloc(rcPolyMeshDetail& value); + + void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept; + + void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst); + + void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst); +} + +#endif diff --git a/components/detournavigator/recastglobalallocator.hpp b/components/detournavigator/recastglobalallocator.hpp index 7c4b2c5343..313d311009 100644 --- a/components/detournavigator/recastglobalallocator.hpp +++ b/components/detournavigator/recastglobalallocator.hpp @@ -3,6 +3,8 @@ #include "recasttempallocator.hpp" +#include + namespace DetourNavigator { class RecastGlobalAllocator @@ -32,7 +34,7 @@ namespace DetourNavigator else { assert(BufferType_perm == getDataPtrBufferType(ptr)); - ::free(getPermDataPtrHeapPtr(ptr)); + std::free(getPermDataPtrHeapPtr(ptr)); } } @@ -56,7 +58,7 @@ namespace DetourNavigator static void* allocPerm(size_t size) { - const auto ptr = ::malloc(size + sizeof(std::size_t)); + const auto ptr = std::malloc(size + sizeof(std::size_t)); if (rcUnlikely(!ptr)) return ptr; setPermPtrBufferType(ptr, BufferType_perm); diff --git a/components/detournavigator/recastmesh.cpp b/components/detournavigator/recastmesh.cpp index dc56f7b931..16220d74f1 100644 --- a/components/detournavigator/recastmesh.cpp +++ b/components/detournavigator/recastmesh.cpp @@ -5,20 +5,33 @@ namespace DetourNavigator { - RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, std::vector indices, std::vector vertices, - std::vector areaTypes, std::vector water, const std::size_t trianglesPerChunk) + Mesh::Mesh(std::vector&& indices, std::vector&& vertices, std::vector&& areaTypes) + { + if (indices.size() / 3 != areaTypes.size()) + throw InvalidArgument("Number of flags doesn't match number of triangles: triangles=" + + std::to_string(indices.size() / 3) + ", areaTypes=" + std::to_string(areaTypes.size())); + indices.shrink_to_fit(); + vertices.shrink_to_fit(); + areaTypes.shrink_to_fit(); + mIndices = std::move(indices); + mVertices = std::move(vertices); + mAreaTypes = std::move(areaTypes); + } + + RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, + std::vector heightfields, std::vector flatHeightfields, + std::vector meshSources) : mGeneration(generation) , mRevision(revision) - , mIndices(std::move(indices)) - , mVertices(std::move(vertices)) - , mAreaTypes(std::move(areaTypes)) + , mMesh(std::move(mesh)) , mWater(std::move(water)) - , mChunkyTriMesh(mVertices, mIndices, mAreaTypes, trianglesPerChunk) + , mHeightfields(std::move(heightfields)) + , mFlatHeightfields(std::move(flatHeightfields)) + , mMeshSources(std::move(meshSources)) { - if (getTrianglesCount() != mAreaTypes.size()) - throw InvalidArgument("Number of flags doesn't match number of triangles: triangles=" - + std::to_string(getTrianglesCount()) + ", areaTypes=" + std::to_string(mAreaTypes.size())); - if (getVerticesCount()) - rcCalcBounds(mVertices.data(), static_cast(getVerticesCount()), mBounds.mMin.ptr(), mBounds.mMax.ptr()); + mWater.shrink_to_fit(); + mHeightfields.shrink_to_fit(); + for (Heightfield& v : mHeightfields) + v.mHeights.shrink_to_fit(); } } diff --git a/components/detournavigator/recastmesh.hpp b/components/detournavigator/recastmesh.hpp index f3259903f3..df9d6414d5 100644 --- a/components/detournavigator/recastmesh.hpp +++ b/components/detournavigator/recastmesh.hpp @@ -2,90 +2,185 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESH_H #include "areatype.hpp" -#include "chunkytrimesh.hpp" #include "bounds.hpp" +#include "tilebounds.hpp" +#include "objecttransform.hpp" -#include -#include -#include +#include +#include #include +#include -#include +#include +#include +#include +#include +#include namespace DetourNavigator { - class RecastMesh + class Mesh { public: - struct Water - { - int mCellSize; - btTransform mTransform; - }; + Mesh(std::vector&& indices, std::vector&& vertices, std::vector&& areaTypes); - RecastMesh(std::size_t generation, std::size_t revision, std::vector indices, std::vector vertices, - std::vector areaTypes, std::vector water, const std::size_t trianglesPerChunk); + const std::vector& getIndices() const noexcept { return mIndices; } + const std::vector& getVertices() const noexcept { return mVertices; } + const std::vector& getAreaTypes() const noexcept { return mAreaTypes; } + std::size_t getVerticesCount() const noexcept { return mVertices.size() / 3; } + std::size_t getTrianglesCount() const noexcept { return mAreaTypes.size(); } - std::size_t getGeneration() const - { - return mGeneration; - } + private: + std::vector mIndices; + std::vector mVertices; + std::vector mAreaTypes; - std::size_t getRevision() const + friend inline bool operator<(const Mesh& lhs, const Mesh& rhs) noexcept { - return mRevision; + return std::tie(lhs.mIndices, lhs.mVertices, lhs.mAreaTypes) + < std::tie(rhs.mIndices, rhs.mVertices, rhs.mAreaTypes); } - const std::vector& getIndices() const + friend inline std::size_t getSize(const Mesh& value) noexcept { - return mIndices; + return value.mIndices.size() * sizeof(int) + + value.mVertices.size() * sizeof(float) + + value.mAreaTypes.size() * sizeof(AreaType); } + }; - const std::vector& getVertices() const - { - return mVertices; - } + struct Water + { + int mCellSize; + float mLevel; + }; - const std::vector& getAreaTypes() const - { - return mAreaTypes; - } + inline bool operator<(const Water& lhs, const Water& rhs) noexcept + { + const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); }; + return tie(lhs) < tie(rhs); + } + + struct CellWater + { + osg::Vec2i mCellPosition; + Water mWater; + }; + + inline bool operator<(const CellWater& lhs, const CellWater& rhs) noexcept + { + const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); }; + return tie(lhs) < tie(rhs); + } + + inline osg::Vec2f getWaterShift2d(const osg::Vec2i& cellPosition, int cellSize) + { + return osg::Vec2f((cellPosition.x() + 0.5f) * cellSize, (cellPosition.y() + 0.5f) * cellSize); + } + + inline osg::Vec3f getWaterShift3d(const osg::Vec2i& cellPosition, int cellSize, float level) + { + return osg::Vec3f(getWaterShift2d(cellPosition, cellSize), level); + } + + struct Heightfield + { + osg::Vec2i mCellPosition; + int mCellSize; + std::uint8_t mLength; + float mMinHeight; + float mMaxHeight; + std::vector mHeights; + std::size_t mOriginalSize; + std::uint8_t mMinX; + std::uint8_t mMinY; + }; + + inline auto makeTuple(const Heightfield& v) noexcept + { + return std::tie(v.mCellPosition, v.mCellSize, v.mLength, v.mMinHeight, v.mMaxHeight, + v.mHeights, v.mOriginalSize, v.mMinX, v.mMinY); + } + + inline bool operator<(const Heightfield& lhs, const Heightfield& rhs) noexcept + { + return makeTuple(lhs) < makeTuple(rhs); + } + + struct FlatHeightfield + { + osg::Vec2i mCellPosition; + int mCellSize; + float mHeight; + }; + + inline bool operator<(const FlatHeightfield& lhs, const FlatHeightfield& rhs) noexcept + { + const auto tie = [] (const FlatHeightfield& v) { return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); }; + return tie(lhs) < tie(rhs); + } + + struct MeshSource + { + osg::ref_ptr mShape; + ObjectTransform mObjectTransform; + AreaType mAreaType; + }; + + class RecastMesh + { + public: + RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, + std::vector heightfields, std::vector flatHeightfields, + std::vector sources); - const std::vector& getWater() const + std::size_t getGeneration() const { - return mWater; + return mGeneration; } - std::size_t getVerticesCount() const + std::size_t getRevision() const { - return mVertices.size() / 3; + return mRevision; } - std::size_t getTrianglesCount() const + const Mesh& getMesh() const noexcept { return mMesh; } + + const std::vector& getWater() const { - return mIndices.size() / 3; + return mWater; } - const ChunkyTriMesh& getChunkyTriMesh() const + const std::vector& getHeightfields() const noexcept { - return mChunkyTriMesh; + return mHeightfields; } - const Bounds& getBounds() const + const std::vector& getFlatHeightfields() const noexcept { - return mBounds; + return mFlatHeightfields; } + const std::vector& getMeshSources() const noexcept { return mMeshSources; } + private: std::size_t mGeneration; std::size_t mRevision; - std::vector mIndices; - std::vector mVertices; - std::vector mAreaTypes; - std::vector mWater; - ChunkyTriMesh mChunkyTriMesh; - Bounds mBounds; + Mesh mMesh; + std::vector mWater; + std::vector mHeightfields; + std::vector mFlatHeightfields; + std::vector mMeshSources; + + friend inline std::size_t getSize(const RecastMesh& value) noexcept + { + return getSize(value.mMesh) + value.mWater.size() * sizeof(CellWater) + + value.mHeightfields.size() * sizeof(Heightfield) + + std::accumulate(value.mHeightfields.begin(), value.mHeightfields.end(), std::size_t {0}, + [] (std::size_t r, const Heightfield& v) { return r + v.mHeights.size() * sizeof(float); }) + + value.mFlatHeightfields.size() * sizeof(FlatHeightfield); + } }; } diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index ee014b9328..08e7002cf8 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -1,13 +1,12 @@ #include "recastmeshbuilder.hpp" -#include "chunkytrimesh.hpp" #include "debug.hpp" -#include "settings.hpp" -#include "settingsutils.hpp" #include "exceptions.hpp" #include #include #include +#include +#include #include #include @@ -18,7 +17,10 @@ #include #include -#include +#include +#include +#include +#include namespace DetourNavigator { @@ -26,48 +28,127 @@ namespace DetourNavigator namespace { - void optimizeRecastMesh(std::vector& indices, std::vector& vertices) + RecastMeshTriangle makeRecastMeshTriangle(const btVector3* vertices, const AreaType areaType) { - std::vector> uniqueVertices; - uniqueVertices.reserve(vertices.size() / 3); + RecastMeshTriangle result; + result.mAreaType = areaType; + for (std::size_t i = 0; i < 3; ++i) + result.mVertices[i] = Misc::Convert::toOsg(vertices[i]); + return result; + } - for (std::size_t i = 0, n = vertices.size() / 3; i < n; ++i) - uniqueVertices.emplace_back(vertices[i * 3], vertices[i * 3 + 1], vertices[i * 3 + 2]); + float getHeightfieldScale(int cellSize, std::size_t dataSize) + { + return static_cast(cellSize) / (dataSize - 1); + } - std::sort(uniqueVertices.begin(), uniqueVertices.end()); - const auto end = std::unique(uniqueVertices.begin(), uniqueVertices.end()); - uniqueVertices.erase(end, uniqueVertices.end()); + bool isNan(const RecastMeshTriangle& triangle) + { + for (std::size_t i = 0; i < 3; ++i) + if (std::isnan(triangle.mVertices[i].x()) + || std::isnan(triangle.mVertices[i].y()) + || std::isnan(triangle.mVertices[i].z())) + return true; + return false; + } + } + + Mesh makeMesh(std::vector&& triangles, const osg::Vec3f& shift) + { + std::vector uniqueVertices; + uniqueVertices.reserve(3 * triangles.size()); - if (uniqueVertices.size() == vertices.size() / 3) - return; + for (const RecastMeshTriangle& triangle : triangles) + for (const osg::Vec3f& vertex : triangle.mVertices) + uniqueVertices.push_back(vertex); - for (std::size_t i = 0, n = indices.size(); i < n; ++i) + std::sort(uniqueVertices.begin(), uniqueVertices.end()); + uniqueVertices.erase(std::unique(uniqueVertices.begin(), uniqueVertices.end()), uniqueVertices.end()); + + std::vector indices; + indices.reserve(3 * triangles.size()); + std::vector areaTypes; + areaTypes.reserve(triangles.size()); + + for (const RecastMeshTriangle& triangle : triangles) + { + areaTypes.push_back(triangle.mAreaType); + + for (const osg::Vec3f& vertex : triangle.mVertices) { - const auto index = indices[i]; - const auto vertex = std::make_tuple(vertices[index * 3], vertices[index * 3 + 1], vertices[index * 3 + 2]); const auto it = std::lower_bound(uniqueVertices.begin(), uniqueVertices.end(), vertex); assert(it != uniqueVertices.end()); assert(*it == vertex); - indices[i] = std::distance(uniqueVertices.begin(), it); + indices.push_back(static_cast(it - uniqueVertices.begin())); } + } - vertices.resize(uniqueVertices.size() * 3); + triangles.clear(); - for (std::size_t i = 0, n = uniqueVertices.size(); i < n; ++i) - { - vertices[i * 3] = std::get<0>(uniqueVertices[i]); - vertices[i * 3 + 1] = std::get<1>(uniqueVertices[i]); - vertices[i * 3 + 2] = std::get<2>(uniqueVertices[i]); - } + std::vector vertices; + vertices.reserve(3 * uniqueVertices.size()); + + for (const osg::Vec3f& vertex : uniqueVertices) + { + vertices.push_back(vertex.x() + shift.x()); + vertices.push_back(vertex.y() + shift.y()); + vertices.push_back(vertex.z() + shift.z()); } + + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); } - RecastMeshBuilder::RecastMeshBuilder(const Settings& settings, const TileBounds& bounds) - : mSettings(settings) - , mBounds(bounds) + Mesh makeMesh(const Heightfield& heightfield) { - mBounds.mMin /= mSettings.get().mRecastScaleFactor; - mBounds.mMax /= mSettings.get().mRecastScaleFactor; + using BulletHelpers::makeProcessTriangleCallback; + using Misc::Convert::toOsg; + + constexpr int upAxis = 2; + constexpr bool flipQuadEdges = false; +#if BT_BULLET_VERSION < 310 + std::vector heights(heightfield.mHeights.begin(), heightfield.mHeights.end()); + btHeightfieldTerrainShape shape(static_cast(heightfield.mHeights.size() / heightfield.mLength), + static_cast(heightfield.mLength), heights.data(), 1, + heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, PHY_FLOAT, flipQuadEdges + ); +#else + btHeightfieldTerrainShape shape(static_cast(heightfield.mHeights.size() / heightfield.mLength), + static_cast(heightfield.mLength), heightfield.mHeights.data(), + heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, flipQuadEdges); +#endif + const float scale = getHeightfieldScale(heightfield.mCellSize, heightfield.mOriginalSize); + shape.setLocalScaling(btVector3(scale, scale, 1)); + btVector3 aabbMin; + btVector3 aabbMax; + shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + std::vector triangles; + auto callback = makeProcessTriangleCallback([&] (btVector3* vertices, int, int) + { + triangles.emplace_back(makeRecastMeshTriangle(vertices, AreaType_ground)); + }); + shape.processAllTriangles(&callback, aabbMin, aabbMax); + const osg::Vec2f aabbShift = (osg::Vec2f(aabbMax.x(), aabbMax.y()) - osg::Vec2f(aabbMin.x(), aabbMin.y())) * 0.5; + const osg::Vec2f tileShift = osg::Vec2f(heightfield.mMinX, heightfield.mMinY) * scale; + const osg::Vec2f localShift = aabbShift + tileShift; + const float cellSize = static_cast(heightfield.mCellSize); + const osg::Vec3f cellShift( + heightfield.mCellPosition.x() * cellSize, + heightfield.mCellPosition.y() * cellSize, + (heightfield.mMinHeight + heightfield.mMaxHeight) * 0.5f + ); + return makeMesh(std::move(triangles), cellShift + osg::Vec3f(localShift.x(), localShift.y(), 0)); + } + + RecastMeshBuilder::RecastMeshBuilder(const TileBounds& bounds) noexcept + : mBounds(bounds) + { + } + + void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform, + const AreaType areaType, osg::ref_ptr source, const ObjectTransform& objectTransform) + { + addObject(shape, transform, areaType); + mSources.push_back(MeshSource {std::move(source), objectTransform, areaType}); } void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform, @@ -96,37 +177,26 @@ namespace DetourNavigator void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform, const AreaType areaType) { - return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int) + return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* vertices, int, int) { - for (std::size_t i = 3; i > 0; --i) - addTriangleVertex(triangle[i - 1]); - mAreaTypes.push_back(areaType); + RecastMeshTriangle triangle = makeRecastMeshTriangle(vertices, areaType); + std::reverse(triangle.mVertices.begin(), triangle.mVertices.end()); + mTriangles.emplace_back(triangle); })); } void RecastMeshBuilder::addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform, const AreaType areaType) { - return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int) + addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* vertices, int, int) { - for (std::size_t i = 0; i < 3; ++i) - addTriangleVertex(triangle[i]); - mAreaTypes.push_back(areaType); + mTriangles.emplace_back(makeRecastMeshTriangle(vertices, areaType)); })); } void RecastMeshBuilder::addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType) { - const auto indexOffset = static_cast(mVertices.size() / 3); - - for (int vertex = 0, count = shape.getNumVertices(); vertex < count; ++vertex) - { - btVector3 position; - shape.getVertex(vertex, position); - addVertex(transform(position)); - } - - const std::array indices {{ + constexpr std::array indices {{ 0, 2, 3, 3, 1, 0, 0, 4, 6, @@ -141,30 +211,77 @@ namespace DetourNavigator 4, 5, 7, }}; - std::transform(indices.begin(), indices.end(), std::back_inserter(mIndices), - [&] (int index) { return index + indexOffset; }); + for (std::size_t i = 0; i < indices.size(); i += 3) + { + std::array vertices; + for (std::size_t j = 0; j < 3; ++j) + { + btVector3 position; + shape.getVertex(indices[i + j], position); + vertices[j] = transform(position); + } + mTriangles.emplace_back(makeRecastMeshTriangle(vertices.data(), areaType)); + } + } - std::generate_n(std::back_inserter(mAreaTypes), 12, [=] { return areaType; }); + void RecastMeshBuilder::addWater(const osg::Vec2i& cellPosition, const Water& water) + { + mWater.push_back(CellWater {cellPosition, water}); } - void RecastMeshBuilder::addWater(const int cellSize, const btTransform& transform) + void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height) { - mWater.push_back(RecastMesh::Water {cellSize, transform}); + if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize))) + mFlatHeightfields.emplace_back(FlatHeightfield {cellPosition, cellSize, height}); } - std::shared_ptr RecastMeshBuilder::create(std::size_t generation, std::size_t revision) + void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights, + std::size_t size, float minHeight, float maxHeight) { - optimizeRecastMesh(mIndices, mVertices); - return std::make_shared(generation, revision, mIndices, mVertices, mAreaTypes, - mWater, mSettings.get().mTrianglesPerChunk); + const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize)); + if (!intersection.has_value()) + return; + const osg::Vec3f shift = Misc::Convert::toOsg(BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, minHeight, maxHeight)); + const float stepSize = getHeightfieldScale(cellSize, size); + const int halfCellSize = cellSize / 2; + const auto local = [&] (float v, float shift) { return (v - shift + halfCellSize) / stepSize; }; + const auto index = [&] (float v, int add) { return std::clamp(static_cast(v) + add, 0, size); }; + const std::size_t minX = index(std::round(local(intersection->mMin.x(), shift.x())), -1); + const std::size_t minY = index(std::round(local(intersection->mMin.y(), shift.y())), -1); + const std::size_t maxX = index(std::round(local(intersection->mMax.x(), shift.x())), 1); + const std::size_t maxY = index(std::round(local(intersection->mMax.y(), shift.y())), 1); + const std::size_t endX = std::min(maxX + 1, size); + const std::size_t endY = std::min(maxY + 1, size); + const std::size_t sliceSize = (endX - minX) * (endY - minY); + if (sliceSize == 0) + return; + std::vector tileHeights; + tileHeights.reserve(sliceSize); + for (std::size_t y = minY; y < endY; ++y) + for (std::size_t x = minX; x < endX; ++x) + tileHeights.push_back(heights[x + y * size]); + Heightfield heightfield; + heightfield.mCellPosition = cellPosition; + heightfield.mCellSize = cellSize; + heightfield.mLength = static_cast(endY - minY); + heightfield.mMinHeight = minHeight; + heightfield.mMaxHeight = maxHeight; + heightfield.mHeights = std::move(tileHeights); + heightfield.mOriginalSize = size; + heightfield.mMinX = static_cast(minX); + heightfield.mMinY = static_cast(minY); + mHeightfields.push_back(std::move(heightfield)); } - void RecastMeshBuilder::reset() + std::shared_ptr RecastMeshBuilder::create(std::size_t generation, std::size_t revision) && { - mIndices.clear(); - mVertices.clear(); - mAreaTypes.clear(); - mWater.clear(); + mTriangles.erase(std::remove_if(mTriangles.begin(), mTriangles.end(), isNan), mTriangles.end()); + std::sort(mTriangles.begin(), mTriangles.end()); + std::sort(mWater.begin(), mWater.end()); + Mesh mesh = makeMesh(std::move(mTriangles)); + return std::make_shared(generation, revision, std::move(mesh), std::move(mWater), + std::move(mHeightfields), std::move(mFlatHeightfields), + std::move(mSources)); } void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform, @@ -226,18 +343,4 @@ namespace DetourNavigator shape.processAllTriangles(&wrapper, aabbMin, aabbMax); } - - void RecastMeshBuilder::addTriangleVertex(const btVector3& worldPosition) - { - mIndices.push_back(static_cast(mVertices.size() / 3)); - addVertex(worldPosition); - } - - void RecastMeshBuilder::addVertex(const btVector3& worldPosition) - { - const auto navMeshPosition = toNavMeshCoordinates(mSettings, Misc::Convert::makeOsgVec3f(worldPosition)); - mVertices.push_back(navMeshPosition.x()); - mVertices.push_back(navMeshPosition.y()); - mVertices.push_back(navMeshPosition.z()); - } } diff --git a/components/detournavigator/recastmeshbuilder.hpp b/components/detournavigator/recastmeshbuilder.hpp index fc2bbbc02a..d0848c2a45 100644 --- a/components/detournavigator/recastmeshbuilder.hpp +++ b/components/detournavigator/recastmeshbuilder.hpp @@ -4,8 +4,17 @@ #include "recastmesh.hpp" #include "tilebounds.hpp" +#include + +#include + #include +#include +#include +#include +#include + class btBoxShape; class btCollisionShape; class btCompoundShape; @@ -15,14 +24,24 @@ class btTriangleCallback; namespace DetourNavigator { - struct Settings; + struct RecastMeshTriangle + { + AreaType mAreaType; + std::array mVertices; + + friend inline bool operator<(const RecastMeshTriangle& lhs, const RecastMeshTriangle& rhs) + { + return std::tie(lhs.mAreaType, lhs.mVertices) < std::tie(rhs.mAreaType, rhs.mVertices); + } + }; class RecastMeshBuilder { public: - RecastMeshBuilder(const Settings& settings, const TileBounds& bounds); + explicit RecastMeshBuilder(const TileBounds& bounds) noexcept; - void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); + void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType, + osg::ref_ptr source, const ObjectTransform& objectTransform); void addObject(const btCompoundShape& shape, const btTransform& transform, const AreaType areaType); @@ -32,28 +51,33 @@ namespace DetourNavigator void addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType); - void addWater(const int mCellSize, const btTransform& transform); + void addWater(const osg::Vec2i& cellPosition, const Water& water); + + void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height); - std::shared_ptr create(std::size_t generation, std::size_t revision); + void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights, std::size_t size, + float minHeight, float maxHeight); - void reset(); + std::shared_ptr create(std::size_t generation, std::size_t revision) &&; private: - std::reference_wrapper mSettings; - TileBounds mBounds; - std::vector mIndices; - std::vector mVertices; - std::vector mAreaTypes; - std::vector mWater; + const TileBounds mBounds; + std::vector mTriangles; + std::vector mWater; + std::vector mHeightfields; + std::vector mFlatHeightfields; + std::vector mSources; + + inline void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); void addObject(const btConcaveShape& shape, const btTransform& transform, btTriangleCallback&& callback); void addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform, btTriangleCallback&& callback); + }; - void addTriangleVertex(const btVector3& worldPosition); + Mesh makeMesh(std::vector&& triangles, const osg::Vec3f& shift = osg::Vec3f()); - void addVertex(const btVector3& worldPosition); - }; + Mesh makeMesh(const Heightfield& heightfield); } #endif diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp index 3796c9816c..a7b24766fc 100644 --- a/components/detournavigator/recastmeshmanager.cpp +++ b/components/detournavigator/recastmeshmanager.cpp @@ -1,34 +1,63 @@ #include "recastmeshmanager.hpp" +#include "recastmeshbuilder.hpp" +#include "settings.hpp" +#include "heightfieldshape.hpp" -#include +#include +#include + +#include + +namespace +{ + struct AddHeightfield + { + osg::Vec2i mCellPosition; + int mCellSize; + DetourNavigator::RecastMeshBuilder& mBuilder; + + void operator()(const DetourNavigator::HeightfieldSurface& v) + { + mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight); + } + + void operator()(DetourNavigator::HeightfieldPlane v) + { + mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeight); + } + }; +} namespace DetourNavigator { - RecastMeshManager::RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation) + RecastMeshManager::RecastMeshManager(const TileBounds& bounds, std::size_t generation) : mGeneration(generation) - , mMeshBuilder(settings, bounds) + , mTileBounds(bounds) { } - bool RecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool RecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { - const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), RecastMeshObject(shape, transform, areaType)); - if (!mObjects.emplace(id, iterator).second) - { - mObjectsOrder.erase(iterator); + const std::lock_guard lock(mMutex); + const auto object = mObjects.lower_bound(id); + if (object != mObjects.end() && object->first == id) return false; - } + mObjects.emplace_hint(object, id, + OscillatingRecastMeshObject(RecastMeshObject(shape, transform, areaType), mRevision + 1)); ++mRevision; return true; } bool RecastMeshManager::updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType) { + const std::lock_guard lock(mMutex); const auto object = mObjects.find(id); if (object == mObjects.end()) return false; - if (!object->second->update(transform, areaType)) + const std::size_t lastChangeRevision = mLastNavMeshReportedChange.has_value() + ? mLastNavMeshReportedChange->mRevision : mRevision; + if (!object->second.update(transform, areaType, lastChangeRevision, mTileBounds)) return false; ++mRevision; return true; @@ -36,61 +65,113 @@ namespace DetourNavigator std::optional RecastMeshManager::removeObject(const ObjectId id) { + const std::lock_guard lock(mMutex); const auto object = mObjects.find(id); if (object == mObjects.end()) return std::nullopt; - const RemovedRecastMeshObject result {object->second->getShape(), object->second->getTransform()}; - mObjectsOrder.erase(object->second); + const RemovedRecastMeshObject result {object->second.getImpl().getShape(), object->second.getImpl().getTransform()}; mObjects.erase(object); ++mRevision; return result; } - bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - const auto iterator = mWaterOrder.emplace(mWaterOrder.end(), Water {cellSize, transform}); - if (!mWater.emplace(cellPosition, iterator).second) - { - mWaterOrder.erase(iterator); + const std::lock_guard lock(mMutex); + if (!mWater.emplace(cellPosition, Water {cellSize, level}).second) return false; - } ++mRevision; return true; } - std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { + const std::lock_guard lock(mMutex); const auto water = mWater.find(cellPosition); if (water == mWater.end()) return std::nullopt; ++mRevision; - const auto result = *water->second; - mWaterOrder.erase(water->second); + Water result = water->second; mWater.erase(water); return result; } - std::shared_ptr RecastMeshManager::getMesh() + bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, + const HeightfieldShape& shape) { - rebuild(); - return mMeshBuilder.create(mGeneration, mLastBuildRevision); + const std::lock_guard lock(mMutex); + if (!mHeightfields.emplace(cellPosition, SizedHeightfieldShape {cellSize, shape}).second) + return false; + ++mRevision; + return true; + } + + std::optional RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const std::lock_guard lock(mMutex); + const auto it = mHeightfields.find(cellPosition); + if (it == mHeightfields.end()) + return std::nullopt; + ++mRevision; + auto result = std::make_optional(it->second); + mHeightfields.erase(it); + return result; + } + + std::shared_ptr RecastMeshManager::getMesh() const + { + RecastMeshBuilder builder(mTileBounds); + using Object = std::tuple< + osg::ref_ptr, + ObjectTransform, + std::reference_wrapper, + btTransform, + AreaType + >; + std::vector objects; + std::size_t revision; + { + const std::lock_guard lock(mMutex); + for (const auto& [k, v] : mWater) + builder.addWater(k, v); + for (const auto& [cellPosition, v] : mHeightfields) + std::visit(AddHeightfield {cellPosition, v.mCellSize, builder}, v.mShape); + objects.reserve(mObjects.size()); + for (const auto& [k, object] : mObjects) + { + const RecastMeshObject& impl = object.getImpl(); + objects.emplace_back(impl.getInstance(), impl.getObjectTransform(), impl.getShape(), + impl.getTransform(), impl.getAreaType()); + } + revision = mRevision; + } + for (const auto& [instance, objectTransform, shape, transform, areaType] : objects) + builder.addObject(shape, transform, areaType, instance->getSource(), objectTransform); + return std::move(builder).create(mGeneration, revision); } bool RecastMeshManager::isEmpty() const { - return mObjects.empty(); + const std::lock_guard lock(mMutex); + return mObjects.empty() && mWater.empty() && mHeightfields.empty(); } - void RecastMeshManager::rebuild() + void RecastMeshManager::reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion) { - if (mLastBuildRevision == mRevision) + if (recastMeshVersion.mGeneration != mGeneration) return; - mMeshBuilder.reset(); - for (const auto& v : mWaterOrder) - mMeshBuilder.addWater(v.mCellSize, v.mTransform); - for (const auto& v : mObjectsOrder) - mMeshBuilder.addObject(v.getShape(), v.getTransform(), v.getAreaType()); - mLastBuildRevision = mRevision; + const std::lock_guard lock(mMutex); + if (mLastNavMeshReport.has_value() && navMeshVersion < mLastNavMeshReport->mNavMeshVersion) + return; + mLastNavMeshReport = {recastMeshVersion.mRevision, navMeshVersion}; + if (!mLastNavMeshReportedChange.has_value() + || mLastNavMeshReportedChange->mNavMeshVersion < mLastNavMeshReport->mNavMeshVersion) + mLastNavMeshReportedChange = mLastNavMeshReport; + } + + Version RecastMeshManager::getVersion() const + { + const std::lock_guard lock(mMutex); + return Version {mGeneration, mRevision}; } } diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp index 5b568e004c..e897c797fc 100644 --- a/components/detournavigator/recastmeshmanager.hpp +++ b/components/detournavigator/recastmeshmanager.hpp @@ -1,66 +1,84 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H -#include "recastmeshbuilder.hpp" -#include "recastmeshobject.hpp" +#include "oscillatingrecastmeshobject.hpp" #include "objectid.hpp" +#include "version.hpp" +#include "recastmesh.hpp" +#include "heightfieldshape.hpp" #include #include -#include #include #include -#include +#include +#include class btCollisionShape; namespace DetourNavigator { + struct Settings; + class RecastMesh; + struct RemovedRecastMeshObject { std::reference_wrapper mShape; btTransform mTransform; }; + struct SizedHeightfieldShape + { + int mCellSize; + HeightfieldShape mShape; + }; + class RecastMeshManager { public: - struct Water - { - int mCellSize = 0; - btTransform mTransform; - }; + explicit RecastMeshManager(const TileBounds& bounds, std::size_t generation); - RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); - - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + std::optional removeObject(const ObjectId id); + + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); std::optional removeWater(const osg::Vec2i& cellPosition); - std::optional removeObject(const ObjectId id); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); - std::shared_ptr getMesh(); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); + + std::shared_ptr getMesh() const; bool isEmpty() const; + void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion); + + Version getVersion() const; + private: + struct Report + { + std::size_t mRevision; + Version mNavMeshVersion; + }; + + const std::size_t mGeneration; + const TileBounds mTileBounds; + mutable std::mutex mMutex; std::size_t mRevision = 0; - std::size_t mLastBuildRevision = 0; - std::size_t mGeneration; - RecastMeshBuilder mMeshBuilder; - std::list mObjectsOrder; - std::unordered_map::iterator> mObjects; - std::list mWaterOrder; - std::map::iterator> mWater; - - void rebuild(); + std::map mObjects; + std::map mWater; + std::map mHeightfields; + std::optional mLastNavMeshReportedChange; + std::optional mLastNavMeshReport; }; } diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index aac0b4c3c8..343aeeb39e 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -8,7 +8,38 @@ namespace DetourNavigator { - RecastMeshObject::RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, + namespace + { + bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, + std::vector& children) + { + assert(static_cast(shape.getNumChildShapes()) == children.size()); + bool result = false; + for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) + { + assert(shape.getChildShape(i) == std::addressof(children[static_cast(i)].getShape())); + result = children[static_cast(i)].update(shape.getChildTransform(i), areaType) || result; + } + return result; + } + + std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) + { + std::vector result; + for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) + result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); + return result; + } + + std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) + { + if (shape.isCompound()) + return makeChildrenObjects(static_cast(shape), areaType); + return {}; + } + } + + ChildRecastMeshObject::ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) : mShape(shape) , mTransform(transform) @@ -18,7 +49,7 @@ namespace DetourNavigator { } - bool RecastMeshObject::update(const btTransform& transform, const AreaType areaType) + bool ChildRecastMeshObject::update(const btTransform& transform, const AreaType areaType) { bool result = false; if (!(mTransform == transform)) @@ -42,32 +73,11 @@ namespace DetourNavigator return result; } - bool RecastMeshObject::updateCompoundObject(const btCompoundShape& shape, - const AreaType areaType, std::vector& children) - { - assert(static_cast(shape.getNumChildShapes()) == children.size()); - bool result = false; - for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - { - assert(shape.getChildShape(i) == std::addressof(children[static_cast(i)].mShape.get())); - result = children[static_cast(i)].update(shape.getChildTransform(i), areaType) || result; - } - return result; - } - - std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) - { - if (shape.isCompound()) - return makeChildrenObjects(static_cast(shape), areaType); - else - return std::vector(); - } - - std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) + RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + const AreaType areaType) + : mInstance(shape.getInstance()) + , mObjectTransform(shape.getObjectTransform()) + , mImpl(shape.getShape(), transform, areaType) { - std::vector result; - for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); - return result; } } diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index f25647ae50..760774353c 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -2,9 +2,15 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHOBJECT_H #include "areatype.hpp" +#include "objecttransform.hpp" + +#include #include +#include +#include + #include #include @@ -13,42 +19,69 @@ class btCompoundShape; namespace DetourNavigator { - class RecastMeshObject + class CollisionShape + { + public: + CollisionShape(osg::ref_ptr instance, const btCollisionShape& shape, + const ObjectTransform& transform) + : mInstance(std::move(instance)) + , mShape(shape) + , mObjectTransform(transform) + {} + + const osg::ref_ptr& getInstance() const { return mInstance; } + const btCollisionShape& getShape() const { return mShape; } + const ObjectTransform& getObjectTransform() const { return mObjectTransform; } + + private: + osg::ref_ptr mInstance; + std::reference_wrapper mShape; + ObjectTransform mObjectTransform; + }; + + class ChildRecastMeshObject { public: - RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); + ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); bool update(const btTransform& transform, const AreaType areaType); - const btCollisionShape& getShape() const - { - return mShape; - } + const btCollisionShape& getShape() const { return mShape; } - const btTransform& getTransform() const - { - return mTransform; - } + const btTransform& getTransform() const { return mTransform; } - AreaType getAreaType() const - { - return mAreaType; - } + AreaType getAreaType() const { return mAreaType; } private: std::reference_wrapper mShape; btTransform mTransform; AreaType mAreaType; btVector3 mLocalScaling; - std::vector mChildren; - - static bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children); + std::vector mChildren; }; - std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType); + class RecastMeshObject + { + public: + RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType); + + bool update(const btTransform& transform, const AreaType areaType) { return mImpl.update(transform, areaType); } + + const osg::ref_ptr& getInstance() const { return mInstance; } + + const btCollisionShape& getShape() const { return mImpl.getShape(); } - std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType); + const btTransform& getTransform() const { return mImpl.getTransform(); } + + AreaType getAreaType() const { return mImpl.getAreaType(); } + + const ObjectTransform& getObjectTransform() const { return mObjectTransform; } + + private: + osg::ref_ptr mInstance; + ObjectTransform mObjectTransform; + ChildRecastMeshObject mImpl; + }; } #endif diff --git a/components/detournavigator/recastmeshprovider.hpp b/components/detournavigator/recastmeshprovider.hpp new file mode 100644 index 0000000000..b01b7c4ea1 --- /dev/null +++ b/components/detournavigator/recastmeshprovider.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H + +#include "tileposition.hpp" +#include "recastmesh.hpp" +#include "tilecachedrecastmeshmanager.hpp" +#include "version.hpp" + +#include +#include + +namespace DetourNavigator +{ + class RecastMesh; + + class RecastMeshProvider + { + public: + RecastMeshProvider(TileCachedRecastMeshManager& impl) + : mImpl(impl) + {} + + std::shared_ptr getMesh(std::string_view worldspace, const TilePosition& tilePosition) const + { + return mImpl.get().getNewMesh(worldspace, tilePosition); + } + + private: + std::reference_wrapper mImpl; + }; +} + +#endif diff --git a/components/detournavigator/recastmeshtiles.hpp b/components/detournavigator/recastmeshtiles.hpp index 68e30ba630..03059c7449 100644 --- a/components/detournavigator/recastmeshtiles.hpp +++ b/components/detournavigator/recastmeshtiles.hpp @@ -2,13 +2,14 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHTILE_H #include "tileposition.hpp" -#include "recastmesh.hpp" #include #include namespace DetourNavigator { + class RecastMesh; + using RecastMeshTiles = std::map>; } diff --git a/components/detournavigator/recastparams.hpp b/components/detournavigator/recastparams.hpp new file mode 100644 index 0000000000..e765623431 --- /dev/null +++ b/components/detournavigator/recastparams.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTPARAMS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTPARAMS_H + +#include "agentbounds.hpp" + +#include + +#include +#include +#include + +namespace DetourNavigator +{ + inline float getAgentHeight(const AgentBounds& agentBounds) + { + return 2.0f * agentBounds.mHalfExtents.z(); + } + + inline float getAgentRadius(const AgentBounds& agentBounds) + { + switch (agentBounds.mShapeType) + { + case CollisionShapeType::Aabb: + return std::max(agentBounds.mHalfExtents.x(), agentBounds.mHalfExtents.y()) * std::sqrt(2); + case CollisionShapeType::RotatingBox: + return agentBounds.mHalfExtents.x(); + } + assert(false && "Unsupported agent shape type"); + return 0; + } +} + +#endif diff --git a/components/detournavigator/ref.hpp b/components/detournavigator/ref.hpp new file mode 100644 index 0000000000..5a9e49e22a --- /dev/null +++ b/components/detournavigator/ref.hpp @@ -0,0 +1,56 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_REF_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_REF_H + +#include +#include +#include + +namespace DetourNavigator +{ + template + struct Ref + { + T& mRef; + + constexpr explicit Ref(T& ref) noexcept : mRef(ref) {} + + friend bool operator==(const Ref& lhs, const Ref& rhs) + { + return lhs.mRef == rhs.mRef; + } + }; + + template + struct ArrayRef + { + T (&mRef)[size]; + + constexpr explicit ArrayRef(T (&ref)[size]) noexcept : mRef(ref) {} + + friend bool operator==(const ArrayRef& lhs, const ArrayRef& rhs) + { + return std::equal(std::begin(lhs.mRef), std::end(lhs.mRef), std::begin(rhs.mRef)); + } + }; + + template + struct Span + { + T* mBegin; + T* mEnd; + + constexpr explicit Span(T* data, int size) noexcept + : mBegin(data) + , mEnd(data + static_cast(size)) + {} + + friend bool operator==(const Span& lhs, const Span& rhs) + { + // size is already equal if headers are equal + assert((lhs.mEnd - lhs.mBegin) == (rhs.mEnd - rhs.mBegin)); + return std::equal(lhs.mBegin, lhs.mEnd, rhs.mBegin); + } + }; +} + +#endif diff --git a/components/detournavigator/serialization.cpp b/components/detournavigator/serialization.cpp new file mode 100644 index 0000000000..a0a097ab0b --- /dev/null +++ b/components/detournavigator/serialization.cpp @@ -0,0 +1,281 @@ +#include "serialization.hpp" + +#include "dbrefgeometryobject.hpp" +#include "preparednavmeshdata.hpp" +#include "recast.hpp" +#include "recastmesh.hpp" +#include "settings.hpp" +#include "agentbounds.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace DetourNavigator +{ +namespace +{ + template + struct Format : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + void operator()(Visitor&& visitor, const osg::Vec2i& value) const + { + visitor(*this, value.ptr(), 2); + } + + template + void operator()(Visitor&& visitor, const osg::Vec2f& value) const + { + visitor(*this, value.ptr(), 2); + } + + template + void operator()(Visitor&& visitor, const osg::Vec3f& value) const + { + visitor(*this, value.ptr(), 3); + } + + template + void operator()(Visitor&& visitor, const Water& value) const + { + visitor(*this, value.mCellSize); + visitor(*this, value.mLevel); + } + + template + void operator()(Visitor&& visitor, const CellWater& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mWater); + } + + template + void operator()(Visitor&& visitor, const RecastSettings& value) const + { + visitor(*this, value.mCellHeight); + visitor(*this, value.mCellSize); + visitor(*this, value.mDetailSampleDist); + visitor(*this, value.mDetailSampleMaxError); + visitor(*this, value.mMaxClimb); + visitor(*this, value.mMaxSimplificationError); + visitor(*this, value.mMaxSlope); + visitor(*this, value.mRecastScaleFactor); + visitor(*this, value.mSwimHeightScale); + visitor(*this, value.mBorderSize); + visitor(*this, value.mMaxEdgeLen); + visitor(*this, value.mMaxVertsPerPoly); + visitor(*this, value.mRegionMergeArea); + visitor(*this, value.mRegionMinArea); + visitor(*this, value.mTileSize); + } + + template + void operator()(Visitor&& visitor, const TileBounds& value) const + { + visitor(*this, value.mMin); + visitor(*this, value.mMax); + } + + template + void operator()(Visitor&& visitor, const Heightfield& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mCellSize); + visitor(*this, value.mLength); + visitor(*this, value.mMinHeight); + visitor(*this, value.mMaxHeight); + visitor(*this, value.mHeights); + visitor(*this, value.mOriginalSize); + visitor(*this, value.mMinX); + visitor(*this, value.mMinY); + } + + template + void operator()(Visitor&& visitor, const FlatHeightfield& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mCellSize); + visitor(*this, value.mHeight); + } + + template + void operator()(Visitor&& visitor, const RecastMesh& value) const + { + visitor(*this, value.getWater()); + visitor(*this, value.getHeightfields()); + visitor(*this, value.getFlatHeightfields()); + } + + template + void operator()(Visitor&& visitor, const ESM::Position& value) const + { + visitor(*this, value.pos); + visitor(*this, value.rot); + } + + template + void operator()(Visitor&& visitor, const ObjectTransform& value) const + { + visitor(*this, value.mPosition); + visitor(*this, value.mScale); + } + + template + void operator()(Visitor&& visitor, const DbRefGeometryObject& value) const + { + visitor(*this, value.mShapeId); + visitor(*this, value.mObjectTransform); + } + + template + void operator()(Visitor&& visitor, const RecastSettings& settings, const AgentBounds& agentBounds, + const RecastMesh& recastMesh, const std::vector& dbRefGeometryObjects) const + { + visitor(*this, DetourNavigator::recastMeshMagic); + visitor(*this, DetourNavigator::recastMeshVersion); + visitor(*this, settings); + visitor(*this, agentBounds); + visitor(*this, recastMesh); + visitor(*this, dbRefGeometryObjects); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, rcPolyMesh>> + { + visitor(*this, value.nverts); + visitor(*this, value.npolys); + visitor(*this, value.maxpolys); + visitor(*this, value.nvp); + visitor(*this, value.bmin); + visitor(*this, value.bmax); + visitor(*this, value.cs); + visitor(*this, value.ch); + visitor(*this, value.borderSize); + visitor(*this, value.maxEdgeError); + if constexpr (mode == Serialization::Mode::Read) + { + if (value.verts == nullptr) + permRecastAlloc(value.verts, getVertsLength(value)); + if (value.polys == nullptr) + permRecastAlloc(value.polys, getPolysLength(value)); + if (value.regs == nullptr) + permRecastAlloc(value.regs, getRegsLength(value)); + if (value.flags == nullptr) + permRecastAlloc(value.flags, getFlagsLength(value)); + if (value.areas == nullptr) + permRecastAlloc(value.areas, getAreasLength(value)); + } + visitor(*this, value.verts, getVertsLength(value)); + visitor(*this, value.polys, getPolysLength(value)); + visitor(*this, value.regs, getRegsLength(value)); + visitor(*this, value.flags, getFlagsLength(value)); + visitor(*this, value.areas, getAreasLength(value)); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, rcPolyMeshDetail>> + { + visitor(*this, value.nmeshes); + if constexpr (mode == Serialization::Mode::Read) + if (value.meshes == nullptr) + permRecastAlloc(value.meshes, getMeshesLength(value)); + visitor(*this, value.meshes, getMeshesLength(value)); + visitor(*this, value.nverts); + if constexpr (mode == Serialization::Mode::Read) + if (value.verts == nullptr) + permRecastAlloc(value.verts, getVertsLength(value)); + visitor(*this, value.verts, getVertsLength(value)); + visitor(*this, value.ntris); + if constexpr (mode == Serialization::Mode::Read) + if (value.tris == nullptr) + permRecastAlloc(value.tris, getTrisLength(value)); + visitor(*this, value.tris, getTrisLength(value)); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, PreparedNavMeshData>> + { + if constexpr (mode == Serialization::Mode::Write) + { + visitor(*this, DetourNavigator::preparedNavMeshDataMagic); + visitor(*this, DetourNavigator::preparedNavMeshDataVersion); + } + else + { + static_assert(mode == Serialization::Mode::Read); + char magic[std::size(DetourNavigator::preparedNavMeshDataMagic)]; + visitor(*this, magic); + if (std::memcmp(magic, DetourNavigator::preparedNavMeshDataMagic, sizeof(magic)) != 0) + throw std::runtime_error("Bad PreparedNavMeshData magic"); + std::uint32_t version = 0; + visitor(*this, version); + if (version != DetourNavigator::preparedNavMeshDataVersion) + throw std::runtime_error("Bad PreparedNavMeshData version"); + } + visitor(*this, value.mUserId); + visitor(*this, value.mCellSize); + visitor(*this, value.mCellHeight); + visitor(*this, value.mPolyMesh); + visitor(*this, value.mPolyMeshDetail); + } + + template + void operator()(Visitor&& visitor, const AgentBounds& value) const + { + visitor(*this, value.mShapeType); + visitor(*this, value.mHalfExtents); + } + }; +} +} // namespace DetourNavigator + +namespace DetourNavigator +{ + std::vector serialize(const RecastSettings& settings, const AgentBounds& agentBounds, + const RecastMesh& recastMesh, const std::vector& dbRefGeometryObjects) + { + constexpr Format format; + Serialization::SizeAccumulator sizeAccumulator; + format(sizeAccumulator, settings, agentBounds, recastMesh, dbRefGeometryObjects); + std::vector result(sizeAccumulator.value()); + format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), + settings, agentBounds, recastMesh, dbRefGeometryObjects); + return result; + } + + std::vector serialize(const PreparedNavMeshData& value) + { + constexpr Format format; + Serialization::SizeAccumulator sizeAccumulator; + format(sizeAccumulator, value); + std::vector result(sizeAccumulator.value()); + format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), value); + return result; + } + + bool deserialize(const std::vector& data, PreparedNavMeshData& value) + { + try + { + constexpr Format format; + format(Serialization::BinaryReader(data.data(), data.data() + data.size()), value); + return true; + } + catch (const std::exception&) + { + return false; + } + } +} diff --git a/components/detournavigator/serialization.hpp b/components/detournavigator/serialization.hpp new file mode 100644 index 0000000000..93022df97e --- /dev/null +++ b/components/detournavigator/serialization.hpp @@ -0,0 +1,30 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H + +#include +#include +#include + +namespace DetourNavigator +{ + class RecastMesh; + struct DbRefGeometryObject; + struct PreparedNavMeshData; + struct RecastSettings; + struct AgentBounds; + + constexpr char recastMeshMagic[] = {'r', 'c', 's', 't'}; + constexpr std::uint32_t recastMeshVersion = 2; + + constexpr char preparedNavMeshDataMagic[] = {'p', 'n', 'a', 'v'}; + constexpr std::uint32_t preparedNavMeshDataVersion = 1; + + std::vector serialize(const RecastSettings& settings, const AgentBounds& agentBounds, + const RecastMesh& recastMesh, const std::vector& dbRefGeometryObjects); + + std::vector serialize(const PreparedNavMeshData& value); + + bool deserialize(const std::vector& data, PreparedNavMeshData& value); +} + +#endif diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index 977b80cf5f..178a1215a5 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -1,47 +1,70 @@ #include "settings.hpp" #include +#include + +#include namespace DetourNavigator { - std::optional makeSettingsFromSettingsManager() + RecastSettings makeRecastSettingsFromSettingsManager() + { + constexpr float epsilon = std::numeric_limits::epsilon(); + + RecastSettings result; + + result.mBorderSize = std::max(0, ::Settings::Manager::getInt("border size", "Navigator")); + result.mCellHeight = std::max(epsilon, ::Settings::Manager::getFloat("cell height", "Navigator")); + result.mCellSize = std::max(epsilon, ::Settings::Manager::getFloat("cell size", "Navigator")); + result.mDetailSampleDist = std::max(0.0f, ::Settings::Manager::getFloat("detail sample dist", "Navigator")); + result.mDetailSampleMaxError = std::max(0.0f, ::Settings::Manager::getFloat("detail sample max error", "Navigator")); + result.mMaxClimb = Constants::sStepSizeUp; + result.mMaxSimplificationError = std::max(0.0f, ::Settings::Manager::getFloat("max simplification error", "Navigator")); + result.mMaxSlope = Constants::sMaxSlope; + result.mRecastScaleFactor = std::max(epsilon, ::Settings::Manager::getFloat("recast scale factor", "Navigator")); + result.mSwimHeightScale = 0; + result.mMaxEdgeLen = std::max(0, ::Settings::Manager::getInt("max edge len", "Navigator")); + result.mMaxVertsPerPoly = std::max(3, ::Settings::Manager::getInt("max verts per poly", "Navigator")); + result.mRegionMergeArea = std::max(0, ::Settings::Manager::getInt("region merge area", "Navigator")); + result.mRegionMinArea = std::max(0, ::Settings::Manager::getInt("region min area", "Navigator")); + result.mTileSize = std::max(1, ::Settings::Manager::getInt("tile size", "Navigator")); + + return result; + } + + DetourSettings makeDetourSettingsFromSettingsManager() { - if (!::Settings::Manager::getBool("enable", "Navigator")) - return std::optional(); - - Settings navigatorSettings; - - navigatorSettings.mBorderSize = ::Settings::Manager::getInt("border size", "Navigator"); - navigatorSettings.mCellHeight = ::Settings::Manager::getFloat("cell height", "Navigator"); - navigatorSettings.mCellSize = ::Settings::Manager::getFloat("cell size", "Navigator"); - navigatorSettings.mDetailSampleDist = ::Settings::Manager::getFloat("detail sample dist", "Navigator"); - navigatorSettings.mDetailSampleMaxError = ::Settings::Manager::getFloat("detail sample max error", "Navigator"); - navigatorSettings.mMaxClimb = 0; - navigatorSettings.mMaxSimplificationError = ::Settings::Manager::getFloat("max simplification error", "Navigator"); - navigatorSettings.mMaxSlope = 0; - navigatorSettings.mRecastScaleFactor = ::Settings::Manager::getFloat("recast scale factor", "Navigator"); - navigatorSettings.mSwimHeightScale = 0; - navigatorSettings.mMaxEdgeLen = ::Settings::Manager::getInt("max edge len", "Navigator"); - navigatorSettings.mMaxNavMeshQueryNodes = ::Settings::Manager::getInt("max nav mesh query nodes", "Navigator"); - navigatorSettings.mMaxPolys = ::Settings::Manager::getInt("max polygons per tile", "Navigator"); - navigatorSettings.mMaxTilesNumber = ::Settings::Manager::getInt("max tiles number", "Navigator"); - navigatorSettings.mMaxVertsPerPoly = ::Settings::Manager::getInt("max verts per poly", "Navigator"); - navigatorSettings.mRegionMergeSize = ::Settings::Manager::getInt("region merge size", "Navigator"); - navigatorSettings.mRegionMinSize = ::Settings::Manager::getInt("region min size", "Navigator"); - navigatorSettings.mTileSize = ::Settings::Manager::getInt("tile size", "Navigator"); - navigatorSettings.mAsyncNavMeshUpdaterThreads = static_cast(::Settings::Manager::getInt("async nav mesh updater threads", "Navigator")); - navigatorSettings.mMaxNavMeshTilesCacheSize = static_cast(::Settings::Manager::getInt("max nav mesh tiles cache size", "Navigator")); - navigatorSettings.mMaxPolygonPathSize = static_cast(::Settings::Manager::getInt("max polygon path size", "Navigator")); - navigatorSettings.mMaxSmoothPathSize = static_cast(::Settings::Manager::getInt("max smooth path size", "Navigator")); - navigatorSettings.mTrianglesPerChunk = static_cast(::Settings::Manager::getInt("triangles per chunk", "Navigator")); - navigatorSettings.mEnableWriteRecastMeshToFile = ::Settings::Manager::getBool("enable write recast mesh to file", "Navigator"); - navigatorSettings.mEnableWriteNavMeshToFile = ::Settings::Manager::getBool("enable write nav mesh to file", "Navigator"); - navigatorSettings.mRecastMeshPathPrefix = ::Settings::Manager::getString("recast mesh path prefix", "Navigator"); - navigatorSettings.mNavMeshPathPrefix = ::Settings::Manager::getString("nav mesh path prefix", "Navigator"); - navigatorSettings.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator"); - navigatorSettings.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator"); - navigatorSettings.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator")); - - return navigatorSettings; + DetourSettings result; + + result.mMaxNavMeshQueryNodes = std::clamp(::Settings::Manager::getInt("max nav mesh query nodes", "Navigator"), 1, 65535); + result.mMaxPolys = std::clamp(::Settings::Manager::getInt("max polygons per tile", "Navigator"), 1, (1 << 22) - 1); + result.mMaxPolygonPathSize = static_cast(std::max(0, ::Settings::Manager::getInt("max polygon path size", "Navigator"))); + result.mMaxSmoothPathSize = static_cast(std::max(0, ::Settings::Manager::getInt("max smooth path size", "Navigator"))); + + return result; + } + + Settings makeSettingsFromSettingsManager() + { + Settings result; + + result.mRecast = makeRecastSettingsFromSettingsManager(); + result.mDetour = makeDetourSettingsFromSettingsManager(); + result.mMaxTilesNumber = std::max(0, ::Settings::Manager::getInt("max tiles number", "Navigator")); + result.mWaitUntilMinDistanceToPlayer = ::Settings::Manager::getInt("wait until min distance to player", "Navigator"); + result.mAsyncNavMeshUpdaterThreads = static_cast(std::max(0, ::Settings::Manager::getInt("async nav mesh updater threads", "Navigator"))); + result.mMaxNavMeshTilesCacheSize = static_cast(std::max(std::int64_t {0}, ::Settings::Manager::getInt64("max nav mesh tiles cache size", "Navigator"))); + result.mEnableWriteRecastMeshToFile = ::Settings::Manager::getBool("enable write recast mesh to file", "Navigator"); + result.mEnableWriteNavMeshToFile = ::Settings::Manager::getBool("enable write nav mesh to file", "Navigator"); + result.mRecastMeshPathPrefix = ::Settings::Manager::getString("recast mesh path prefix", "Navigator"); + result.mNavMeshPathPrefix = ::Settings::Manager::getString("nav mesh path prefix", "Navigator"); + result.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator"); + result.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator"); + result.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator")); + result.mEnableNavMeshDiskCache = ::Settings::Manager::getBool("enable nav mesh disk cache", "Navigator"); + result.mWriteToNavMeshDb = ::Settings::Manager::getBool("write to navmeshdb", "Navigator"); + result.mMaxDbFileSize = static_cast(::Settings::Manager::getInt64("max navmeshdb file size", "Navigator")); + + return result; } } diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index d73087b217..45bcf15dbf 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -2,17 +2,12 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H #include -#include #include namespace DetourNavigator { - struct Settings + struct RecastSettings { - bool mEnableWriteRecastMeshToFile = false; - bool mEnableWriteNavMeshToFile = false; - bool mEnableRecastMeshFileNameRevision = false; - bool mEnableNavMeshFileNameRevision = false; float mCellHeight = 0; float mCellSize = 0; float mDetailSampleDist = 0; @@ -24,24 +19,47 @@ namespace DetourNavigator float mSwimHeightScale = 0; int mBorderSize = 0; int mMaxEdgeLen = 0; - int mMaxNavMeshQueryNodes = 0; - int mMaxPolys = 0; - int mMaxTilesNumber = 0; int mMaxVertsPerPoly = 0; - int mRegionMergeSize = 0; - int mRegionMinSize = 0; + int mRegionMergeArea = 0; + int mRegionMinArea = 0; int mTileSize = 0; - std::size_t mAsyncNavMeshUpdaterThreads = 0; - std::size_t mMaxNavMeshTilesCacheSize = 0; + }; + + struct DetourSettings + { + int mMaxPolys = 0; + int mMaxNavMeshQueryNodes = 0; std::size_t mMaxPolygonPathSize = 0; std::size_t mMaxSmoothPathSize = 0; - std::size_t mTrianglesPerChunk = 0; + }; + + struct Settings + { + bool mEnableWriteRecastMeshToFile = false; + bool mEnableWriteNavMeshToFile = false; + bool mEnableRecastMeshFileNameRevision = false; + bool mEnableNavMeshFileNameRevision = false; + bool mEnableNavMeshDiskCache = false; + bool mWriteToNavMeshDb = false; + RecastSettings mRecast; + DetourSettings mDetour; + int mWaitUntilMinDistanceToPlayer = 0; + int mMaxTilesNumber = 0; + std::size_t mAsyncNavMeshUpdaterThreads = 0; + std::size_t mMaxNavMeshTilesCacheSize = 0; std::string mRecastMeshPathPrefix; std::string mNavMeshPathPrefix; std::chrono::milliseconds mMinUpdateInterval; + std::uint64_t mMaxDbFileSize = 0; }; - std::optional makeSettingsFromSettingsManager(); + inline constexpr std::int64_t navMeshFormatVersion = 2; + + RecastSettings makeRecastSettingsFromSettingsManager(); + + DetourSettings makeDetourSettingsFromSettingsManager(); + + Settings makeSettingsFromSettingsManager(); } #endif diff --git a/components/detournavigator/settingsutils.hpp b/components/detournavigator/settingsutils.hpp index a22205b2aa..c1e611aaf8 100644 --- a/components/detournavigator/settingsutils.hpp +++ b/components/detournavigator/settingsutils.hpp @@ -1,48 +1,48 @@ -#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGSUTILS_H +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGSUTILS_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGSUTILS_H #include "settings.hpp" #include "tilebounds.hpp" #include "tileposition.hpp" -#include "tilebounds.hpp" - -#include #include -#include #include -#include +#include +#include namespace DetourNavigator { - inline float getHeight(const Settings& settings,const osg::Vec3f& agentHalfExtents) + inline float toNavMeshCoordinates(const RecastSettings& settings, float value) { - return 2.0f * agentHalfExtents.z() * settings.mRecastScaleFactor; + return value * settings.mRecastScaleFactor; } - inline float getMaxClimb(const Settings& settings) + inline osg::Vec2f toNavMeshCoordinates(const RecastSettings& settings, osg::Vec2f position) { - return settings.mMaxClimb * settings.mRecastScaleFactor; + return position * settings.mRecastScaleFactor; } - inline float getRadius(const Settings& settings, const osg::Vec3f& agentHalfExtents) + inline osg::Vec3f toNavMeshCoordinates(const RecastSettings& settings, osg::Vec3f position) { - return agentHalfExtents.x() * settings.mRecastScaleFactor; + std::swap(position.y(), position.z()); + return position * settings.mRecastScaleFactor; } - inline float toNavMeshCoordinates(const Settings& settings, float value) + inline TileBounds toNavMeshCoordinates(const RecastSettings& settings, const TileBounds& value) { - return value * settings.mRecastScaleFactor; + return TileBounds { + toNavMeshCoordinates(settings, value.mMin), + toNavMeshCoordinates(settings, value.mMax) + }; } - inline osg::Vec3f toNavMeshCoordinates(const Settings& settings, osg::Vec3f position) + inline float fromNavMeshCoordinates(const RecastSettings& settings, float value) { - std::swap(position.y(), position.z()); - return position * settings.mRecastScaleFactor; + return value / settings.mRecastScaleFactor; } - inline osg::Vec3f fromNavMeshCoordinates(const Settings& settings, osg::Vec3f position) + inline osg::Vec3f fromNavMeshCoordinates(const RecastSettings& settings, osg::Vec3f position) { const auto factor = 1.0f / settings.mRecastScaleFactor; position *= factor; @@ -50,20 +50,32 @@ namespace DetourNavigator return position; } - inline float getTileSize(const Settings& settings) + inline float getTileSize(const RecastSettings& settings) + { + return static_cast(settings.mTileSize) * settings.mCellSize; + } + + inline int getTilePosition(const RecastSettings& settings, float position) + { + const float v = std::floor(position / getTileSize(settings)); + if (v < static_cast(std::numeric_limits::min())) + return std::numeric_limits::min(); + if (v > static_cast(std::numeric_limits::max() - 1)) + return std::numeric_limits::max() - 1; + return static_cast(v); + } + + inline TilePosition getTilePosition(const RecastSettings& settings, const osg::Vec2f& position) { - return settings.mTileSize * settings.mCellSize; + return TilePosition(getTilePosition(settings, position.x()), getTilePosition(settings, position.y())); } - inline TilePosition getTilePosition(const Settings& settings, const osg::Vec3f& position) + inline TilePosition getTilePosition(const RecastSettings& settings, const osg::Vec3f& position) { - return TilePosition( - static_cast(std::floor(position.x() / getTileSize(settings))), - static_cast(std::floor(position.z() / getTileSize(settings))) - ); + return getTilePosition(settings, osg::Vec2f(position.x(), position.z())); } - inline TileBounds makeTileBounds(const Settings& settings, const TilePosition& tilePosition) + inline TileBounds makeTileBounds(const RecastSettings& settings, const TilePosition& tilePosition) { return TileBounds { osg::Vec2f(tilePosition.x(), tilePosition.y()) * getTileSize(settings), @@ -71,23 +83,30 @@ namespace DetourNavigator }; } - inline float getBorderSize(const Settings& settings) + inline float getBorderSize(const RecastSettings& settings) + { + return static_cast(settings.mBorderSize) * settings.mCellSize; + } + + inline float getRealTileSize(const RecastSettings& settings) { - return settings.mBorderSize * settings.mCellSize; + return settings.mTileSize * settings.mCellSize / settings.mRecastScaleFactor; } - inline float getSwimLevel(const Settings& settings, const float agentHalfExtentsZ) + inline float getMaxNavmeshAreaRadius(const Settings& settings) { - return - settings.mSwimHeightScale * agentHalfExtentsZ; + return std::floor(std::sqrt(settings.mMaxTilesNumber / osg::PI)) - 1; } - inline btTransform getSwimLevelTransform(const Settings& settings, const btTransform& transform, - const float agentHalfExtentsZ) + inline TileBounds makeRealTileBoundsWithBorder(const RecastSettings& settings, const TilePosition& tilePosition) { - return btTransform( - transform.getBasis(), - transform.getOrigin() + btVector3(0, 0, getSwimLevel(settings, agentHalfExtentsZ) - agentHalfExtentsZ) - ); + TileBounds result = makeTileBounds(settings, tilePosition); + const float border = getBorderSize(settings); + result.mMin -= osg::Vec2f(border, border); + result.mMax += osg::Vec2f(border, border); + result.mMin /= settings.mRecastScaleFactor; + result.mMax /= settings.mRecastScaleFactor; + return result; } } diff --git a/components/detournavigator/status.hpp b/components/detournavigator/status.hpp index 3715489acc..5f01f04758 100644 --- a/components/detournavigator/status.hpp +++ b/components/detournavigator/status.hpp @@ -6,6 +6,7 @@ namespace DetourNavigator enum class Status { Success, + PartialPath, NavMeshNotFound, StartPolygonNotFound, EndPolygonNotFound, @@ -21,6 +22,8 @@ namespace DetourNavigator { case Status::Success: return "success"; + case Status::PartialPath: + return "partial path is found"; case Status::NavMeshNotFound: return "navmesh is not found"; case Status::StartPolygonNotFound: diff --git a/components/detournavigator/tilebounds.hpp b/components/detournavigator/tilebounds.hpp index 83fe2b6296..2dc32292e8 100644 --- a/components/detournavigator/tilebounds.hpp +++ b/components/detournavigator/tilebounds.hpp @@ -1,7 +1,17 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_TILEBOUNDS_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_TILEBOUNDS_H +#include + #include +#include + +#include +#include + +#include +#include +#include namespace DetourNavigator { @@ -10,6 +20,55 @@ namespace DetourNavigator osg::Vec2f mMin; osg::Vec2f mMax; }; + + inline auto tie(const TileBounds& value) noexcept + { + return std::tie(value.mMin, value.mMax); + } + + inline bool operator<(const TileBounds& lhs, const TileBounds& rhs) noexcept + { + return tie(lhs) < tie(rhs); + } + + inline bool operator ==(const TileBounds& lhs, const TileBounds& rhs) noexcept + { + return tie(lhs) == tie(rhs); + } + + inline bool operator !=(const TileBounds& lhs, const TileBounds& rhs) noexcept + { + return !(lhs == rhs); + } + + inline std::optional getIntersection(const TileBounds& a, const TileBounds& b) noexcept + { + const float minX = std::max(a.mMin.x(), b.mMin.x()); + const float maxX = std::min(a.mMax.x(), b.mMax.x()); + if (minX > maxX) + return std::nullopt; + const float minY = std::max(a.mMin.y(), b.mMin.y()); + const float maxY = std::min(a.mMax.y(), b.mMax.y()); + if (minY > maxY) + return std::nullopt; + return TileBounds {osg::Vec2f(minX, minY), osg::Vec2f(maxX, maxY)}; + } + + inline TileBounds maxCellTileBounds(const osg::Vec2i& position, int size) + { + return TileBounds { + osg::Vec2f(position.x(), position.y()) * size, + osg::Vec2f(position.x() + 1, position.y() + 1) * size + }; + } + + inline TileBounds makeObjectTileBounds(const btCollisionShape& shape, const btTransform& transform) + { + btVector3 aabbMin; + btVector3 aabbMax; + shape.getAabb(transform, aabbMin, aabbMax); + return TileBounds {Misc::Convert::toOsgXY(aabbMin), Misc::Convert::toOsgXY(aabbMax)}; + } } #endif diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index fddaa88f1d..e7e46e96df 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -2,70 +2,145 @@ #include "makenavmesh.hpp" #include "gettilespositions.hpp" #include "settingsutils.hpp" +#include "changetype.hpp" + +#include +#include + +#include +#include +#include namespace DetourNavigator { - TileCachedRecastMeshManager::TileCachedRecastMeshManager(const Settings& settings) + namespace + { + const TileBounds infiniteTileBounds { + osg::Vec2f(-std::numeric_limits::max(), -std::numeric_limits::max()), + osg::Vec2f(std::numeric_limits::max(), std::numeric_limits::max()) + }; + } + + TileCachedRecastMeshManager::TileCachedRecastMeshManager(const RecastSettings& settings) : mSettings(settings) + , mBounds(infiniteTileBounds) + , mRange(makeTilesPositionsRange(mBounds.mMin, mBounds.mMax, mSettings)) {} - bool TileCachedRecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, - const btTransform& transform, const AreaType areaType) + TileBounds TileCachedRecastMeshManager::getBounds() const { - bool result = false; - auto& tilesPositions = mObjectsTilesPositions[id]; - const auto border = getBorderSize(mSettings); + return mBounds; + } + + std::vector> TileCachedRecastMeshManager::setBounds(const TileBounds& bounds) + { + std::vector> changedTiles; + + if (mBounds == bounds) + return changedTiles; + + const auto newRange = makeTilesPositionsRange(bounds.mMin, bounds.mMax, mSettings); + + if (mBounds != infiniteTileBounds) { - auto tiles = mTiles.lock(); - getTilesPositions(shape, transform, mSettings, [&] (const TilePosition& tilePosition) + const auto locked = mWorldspaceTiles.lock(); + for (auto& object : mObjects) + { + const ObjectId id = object.first; + ObjectData& data = object.second; + const TilesPositionsRange objectRange = makeTilesPositionsRange(data.mShape.getShape(), data.mTransform, mSettings); + + const auto onOldTilePosition = [&] (const TilePosition& position) + { + if (isInTilesPositionsRange(newRange, position)) + return; + const auto it = data.mTiles.find(position); + if (it == data.mTiles.end()) + return; + data.mTiles.erase(it); + if (removeTile(id, position, locked->mTiles)) + changedTiles.emplace_back(position, ChangeType::remove); + }; + getTilesPositions(getIntersection(mRange, objectRange), onOldTilePosition); + + const auto onNewTilePosition = [&] (const TilePosition& position) { - if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get())) + if (data.mTiles.find(position) != data.mTiles.end()) + return; + if (addTile(id, data.mShape, data.mTransform, data.mAreaType, position, locked->mTiles)) { - tilesPositions.insert(tilePosition); - result = true; + data.mTiles.insert(position); + changedTiles.emplace_back(position, ChangeType::add); } - }); + }; + getTilesPositions(getIntersection(newRange, objectRange), onNewTilePosition); + } + + std::sort(changedTiles.begin(), changedTiles.end()); + changedTiles.erase(std::unique(changedTiles.begin(), changedTiles.end()), changedTiles.end()); } - if (result) + + if (!changedTiles.empty()) ++mRevision; - return result; + + mBounds = bounds; + mRange = newRange; + + return changedTiles; + } + + std::string TileCachedRecastMeshManager::getWorldspace() const + { + return mWorldspaceTiles.lockConst()->mWorldspace; + } + + void TileCachedRecastMeshManager::setWorldspace(std::string_view worldspace) + { + const auto locked = mWorldspaceTiles.lock(); + if (locked->mWorldspace == worldspace) + return; + locked->mTiles.clear(); + locked->mWorldspace = worldspace; } std::optional TileCachedRecastMeshManager::removeObject(const ObjectId id) { - const auto object = mObjectsTilesPositions.find(id); - if (object == mObjectsTilesPositions.end()) + const auto object = mObjects.find(id); + if (object == mObjects.end()) return std::nullopt; std::optional result; { - auto tiles = mTiles.lock(); - for (const auto& tilePosition : object->second) + const auto locked = mWorldspaceTiles.lock(); + for (const auto& tilePosition : object->second.mTiles) { - const auto removed = removeTile(id, tilePosition, tiles.get()); + const auto removed = removeTile(id, tilePosition, locked->mTiles); if (removed && !result) result = removed; } } + mObjects.erase(object); if (result) ++mRevision; return result; } - bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - const auto border = getBorderSize(mSettings); + const auto it = mWaterTilesPositions.find(cellPosition); + if (it != mWaterTilesPositions.end()) + return false; - auto& tilesPositions = mWaterTilesPositions[cellPosition]; + std::vector& tilesPositions = mWaterTilesPositions.emplace_hint( + it, cellPosition, std::vector())->second; bool result = false; if (cellSize == std::numeric_limits::max()) { - const auto tiles = mTiles.lock(); - for (auto& tile : *tiles) + const auto locked = mWorldspaceTiles.lock(); + for (auto& tile : locked->mTiles) { - if (tile.second.addWater(cellPosition, cellSize, transform)) + if (tile.second->addWater(cellPosition, cellSize, level)) { tilesPositions.push_back(tile.first); result = true; @@ -74,19 +149,19 @@ namespace DetourNavigator } else { - getTilesPositions(cellSize, transform, mSettings, [&] (const TilePosition& tilePosition) + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level)); + getTilesPositions(makeTilesPositionsRange(cellSize, shift, mSettings), + [&] (const TilePosition& tilePosition) { - const auto tiles = mTiles.lock(); - auto tile = tiles->find(tilePosition); - if (tile == tiles->end()) + const auto locked = mWorldspaceTiles.lock(); + auto tile = locked->mTiles.find(tilePosition); + if (tile == locked->mTiles.end()) { - auto tileBounds = makeTileBounds(mSettings, tilePosition); - tileBounds.mMin -= osg::Vec2f(border, border); - tileBounds.mMax += osg::Vec2f(border, border); - tile = tiles->insert(std::make_pair(tilePosition, - CachedRecastMeshManager(mSettings, tileBounds, mTilesGeneration))).first; + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = locked->mTiles.emplace_hint(tile, tilePosition, + std::make_shared(tileBounds, mTilesGeneration)); } - if (tile->second.addWater(cellPosition, cellSize, transform)) + if (tile->second->addWater(cellPosition, cellSize, level)) { tilesPositions.push_back(tilePosition); result = true; @@ -100,44 +175,116 @@ namespace DetourNavigator return result; } - std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto object = mWaterTilesPositions.find(cellPosition); if (object == mWaterTilesPositions.end()) return std::nullopt; - std::optional result; + std::optional result; for (const auto& tilePosition : object->second) { - const auto tiles = mTiles.lock(); - const auto tile = tiles->find(tilePosition); - if (tile == tiles->end()) + const auto locked = mWorldspaceTiles.lock(); + const auto tile = locked->mTiles.find(tilePosition); + if (tile == locked->mTiles.end()) continue; - const auto tileResult = tile->second.removeWater(cellPosition); - if (tile->second.isEmpty()) + const auto tileResult = tile->second->removeWater(cellPosition); + if (tile->second->isEmpty()) { - tiles->erase(tile); + locked->mTiles.erase(tile); ++mTilesGeneration; } if (tileResult && !result) result = tileResult; } + mWaterTilesPositions.erase(object); if (result) ++mRevision; return result; } - std::shared_ptr TileCachedRecastMeshManager::getMesh(const TilePosition& tilePosition) + bool TileCachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, + const HeightfieldShape& shape) { - const auto tiles = mTiles.lock(); - const auto it = tiles->find(tilePosition); - if (it == tiles->end()) - return nullptr; - return it->second.getMesh(); + const auto it = mHeightfieldTilesPositions.find(cellPosition); + if (it != mHeightfieldTilesPositions.end()) + return false; + + std::vector& tilesPositions = mHeightfieldTilesPositions.emplace_hint( + it, cellPosition, std::vector())->second; + const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize); + + bool result = false; + + getTilesPositions(makeTilesPositionsRange(cellSize, shift, mSettings), + [&] (const TilePosition& tilePosition) + { + const auto locked = mWorldspaceTiles.lock(); + auto tile = locked->mTiles.find(tilePosition); + if (tile == locked->mTiles.end()) + { + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = locked->mTiles.emplace_hint(tile, tilePosition, + std::make_shared(tileBounds, mTilesGeneration)); + } + if (tile->second->addHeightfield(cellPosition, cellSize, shape)) + { + tilesPositions.push_back(tilePosition); + result = true; + } + }); + + if (result) + ++mRevision; + + return result; + } + + std::optional TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const auto object = mHeightfieldTilesPositions.find(cellPosition); + if (object == mHeightfieldTilesPositions.end()) + return std::nullopt; + std::optional result; + for (const auto& tilePosition : object->second) + { + const auto locked = mWorldspaceTiles.lock(); + const auto tile = locked->mTiles.find(tilePosition); + if (tile == locked->mTiles.end()) + continue; + const auto tileResult = tile->second->removeHeightfield(cellPosition); + if (tile->second->isEmpty()) + { + locked->mTiles.erase(tile); + ++mTilesGeneration; + } + if (tileResult && !result) + result = tileResult; + } + mHeightfieldTilesPositions.erase(object); + if (result) + ++mRevision; + return result; } - bool TileCachedRecastMeshManager::hasTile(const TilePosition& tilePosition) + std::shared_ptr TileCachedRecastMeshManager::getMesh(std::string_view worldspace, const TilePosition& tilePosition) const { - return mTiles.lockConst()->count(tilePosition); + if (const auto manager = getManager(worldspace, tilePosition)) + return manager->getMesh(); + return nullptr; + } + + std::shared_ptr TileCachedRecastMeshManager::getCachedMesh(std::string_view worldspace, const TilePosition& tilePosition) const + { + if (const auto manager = getManager(worldspace, tilePosition)) + return manager->getCachedMesh(); + return nullptr; + } + + std::shared_ptr TileCachedRecastMeshManager::getNewMesh(std::string_view worldspace, const TilePosition& tilePosition) const + { + if (const auto manager = getManager(worldspace, tilePosition)) + return manager->getNewMesh(); + return nullptr; } std::size_t TileCachedRecastMeshManager::getRevision() const @@ -145,41 +292,60 @@ namespace DetourNavigator return mRevision; } - bool TileCachedRecastMeshManager::addTile(const ObjectId id, const btCollisionShape& shape, - const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border, - std::map& tiles) + void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) const + { + const auto locked = mWorldspaceTiles.lockConst(); + const auto it = locked->mTiles.find(tilePosition); + if (it == locked->mTiles.end()) + return; + it->second->reportNavMeshChange(recastMeshVersion, navMeshVersion); + } + + bool TileCachedRecastMeshManager::addTile(const ObjectId id, const CollisionShape& shape, + const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, + TilesMap& tiles) { auto tile = tiles.find(tilePosition); if (tile == tiles.end()) { - auto tileBounds = makeTileBounds(mSettings, tilePosition); - tileBounds.mMin -= osg::Vec2f(border, border); - tileBounds.mMax += osg::Vec2f(border, border); - tile = tiles.insert(std::make_pair( - tilePosition, CachedRecastMeshManager(mSettings, tileBounds, mTilesGeneration))).first; + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = tiles.emplace_hint(tile, tilePosition, + std::make_shared(tileBounds, mTilesGeneration)); } - return tile->second.addObject(id, shape, transform, areaType); + return tile->second->addObject(id, shape, transform, areaType); } bool TileCachedRecastMeshManager::updateTile(const ObjectId id, const btTransform& transform, - const AreaType areaType, const TilePosition& tilePosition, std::map& tiles) + const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles) { const auto tile = tiles.find(tilePosition); - return tile != tiles.end() && tile->second.updateObject(id, transform, areaType); + return tile != tiles.end() && tile->second->updateObject(id, transform, areaType); } std::optional TileCachedRecastMeshManager::removeTile(const ObjectId id, - const TilePosition& tilePosition, std::map& tiles) + const TilePosition& tilePosition, TilesMap& tiles) { const auto tile = tiles.find(tilePosition); if (tile == tiles.end()) return std::optional(); - const auto tileResult = tile->second.removeObject(id); - if (tile->second.isEmpty()) + auto tileResult = tile->second->removeObject(id); + if (tile->second->isEmpty()) { tiles.erase(tile); ++mTilesGeneration; } return tileResult; } + + std::shared_ptr TileCachedRecastMeshManager::getManager(std::string_view worldspace, + const TilePosition& tilePosition) const + { + const auto locked = mWorldspaceTiles.lockConst(); + if (locked->mWorldspace != worldspace) + return nullptr; + const auto it = locked->mTiles.find(tilePosition); + if (it == locked->mTiles.end()) + return nullptr; + return it->second; + } } diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index fa192bd732..5e168efc16 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -5,11 +5,16 @@ #include "tileposition.hpp" #include "settingsutils.hpp" #include "gettilespositions.hpp" +#include "version.hpp" +#include "heightfieldshape.hpp" +#include "changetype.hpp" #include +#include #include #include +#include #include namespace DetourNavigator @@ -17,94 +22,158 @@ namespace DetourNavigator class TileCachedRecastMeshManager { public: - TileCachedRecastMeshManager(const Settings& settings); + explicit TileCachedRecastMeshManager(const RecastSettings& settings); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, - const AreaType areaType); + TileBounds getBounds() const; + + std::vector> setBounds(const TileBounds& bounds); + + std::string getWorldspace() const; + + void setWorldspace(std::string_view worldspace); + + template + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, + const AreaType areaType, OnChangedTile&& onChangedTile) + { + auto it = mObjects.find(id); + if (it != mObjects.end()) + return false; + const TilesPositionsRange objectRange = makeTilesPositionsRange(shape.getShape(), transform, mSettings); + const TilesPositionsRange range = getIntersection(mRange, objectRange); + std::set tilesPositions; + if (range.mBegin != range.mEnd) + { + const auto locked = mWorldspaceTiles.lock(); + getTilesPositions(range, + [&] (const TilePosition& tilePosition) + { + if (addTile(id, shape, transform, areaType, tilePosition, locked->mTiles)) + tilesPositions.insert(tilePosition); + }); + } + it = mObjects.emplace_hint(it, id, ObjectData {shape, transform, areaType, std::move(tilesPositions)}); + std::for_each(it->second.mTiles.begin(), it->second.mTiles.end(), std::forward(onChangedTile)); + ++mRevision; + return true; + } template - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType, OnChangedTile&& onChangedTile) { - const auto object = mObjectsTilesPositions.find(id); - if (object == mObjectsTilesPositions.end()) + const auto object = mObjects.find(id); + if (object == mObjects.end()) return false; - auto& currentTiles = object->second; - const auto border = getBorderSize(mSettings); + auto& data = object->second; bool changed = false; std::set newTiles; { - auto tiles = mTiles.lock(); + const TilesPositionsRange objectRange = makeTilesPositionsRange(shape.getShape(), transform, mSettings); + const TilesPositionsRange range = getIntersection(mRange, objectRange); + const auto locked = mWorldspaceTiles.lock(); const auto onTilePosition = [&] (const TilePosition& tilePosition) { - if (currentTiles.count(tilePosition)) + if (data.mTiles.find(tilePosition) != data.mTiles.end()) { newTiles.insert(tilePosition); - if (updateTile(id, transform, areaType, tilePosition, tiles.get())) + if (updateTile(id, transform, areaType, tilePosition, locked->mTiles)) { - onChangedTile(tilePosition); + onChangedTile(tilePosition, ChangeType::update); changed = true; } } - else if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get())) + else if (addTile(id, shape, transform, areaType, tilePosition, locked->mTiles)) { newTiles.insert(tilePosition); - onChangedTile(tilePosition); + onChangedTile(tilePosition, ChangeType::add); changed = true; } }; - getTilesPositions(shape, transform, mSettings, onTilePosition); - for (const auto& tile : currentTiles) + getTilesPositions(range, onTilePosition); + for (const auto& tile : data.mTiles) { - if (!newTiles.count(tile) && removeTile(id, tile, tiles.get())) + if (newTiles.find(tile) == newTiles.end() && removeTile(id, tile, locked->mTiles)) { - onChangedTile(tile); + onChangedTile(tile, ChangeType::remove); changed = true; } } } - std::swap(currentTiles, newTiles); if (changed) + { + data.mTiles = std::move(newTiles); ++mRevision; + } return changed; } std::optional removeObject(const ObjectId id); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); + + std::optional removeWater(const osg::Vec2i& cellPosition); - std::optional removeWater(const osg::Vec2i& cellPosition); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); - std::shared_ptr getMesh(const TilePosition& tilePosition); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); - bool hasTile(const TilePosition& tilePosition); + std::shared_ptr getMesh(std::string_view worldspace, const TilePosition& tilePosition) const; + + std::shared_ptr getCachedMesh(std::string_view worldspace, const TilePosition& tilePosition) const; + + std::shared_ptr getNewMesh(std::string_view worldspace, const TilePosition& tilePosition) const; template - void forEachTilePosition(Function&& function) + void forEachTile(Function&& function) const { - for (const auto& tile : *mTiles.lock()) - function(tile.first); + const auto& locked = mWorldspaceTiles.lockConst(); + for (const auto& [tilePosition, recastMeshManager] : locked->mTiles) + function(tilePosition, *recastMeshManager); } std::size_t getRevision() const; + void reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) const; + private: - const Settings& mSettings; - Misc::ScopeGuarded> mTiles; - std::unordered_map> mObjectsTilesPositions; + using TilesMap = std::map>; + + struct ObjectData + { + const CollisionShape mShape; + const btTransform mTransform; + const AreaType mAreaType; + std::set mTiles; + }; + + struct WorldspaceTiles + { + std::string mWorldspace; + TilesMap mTiles; + }; + + const RecastSettings& mSettings; + TileBounds mBounds; + TilesPositionsRange mRange; + Misc::ScopeGuarded mWorldspaceTiles; + std::unordered_map mObjects; std::map> mWaterTilesPositions; + std::map> mHeightfieldTilesPositions; std::size_t mRevision = 0; std::size_t mTilesGeneration = 0; - bool addTile(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, - const AreaType areaType, const TilePosition& tilePosition, float border, - std::map& tiles); + bool addTile(const ObjectId id, const CollisionShape& shape, const btTransform& transform, + const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles); bool updateTile(const ObjectId id, const btTransform& transform, const AreaType areaType, - const TilePosition& tilePosition, std::map& tiles); + const TilePosition& tilePosition, TilesMap& tiles); std::optional removeTile(const ObjectId id, const TilePosition& tilePosition, - std::map& tiles); + TilesMap& tiles); + + inline std::shared_ptr getManager(std::string_view worldspace, + const TilePosition& tilePosition) const; }; } diff --git a/components/detournavigator/tilespositionsrange.hpp b/components/detournavigator/tilespositionsrange.hpp new file mode 100644 index 0000000000..d5f2622ba1 --- /dev/null +++ b/components/detournavigator/tilespositionsrange.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_TILESPOSITIONSRANGE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_TILESPOSITIONSRANGE_H + +#include "tileposition.hpp" + +namespace DetourNavigator +{ + struct TilesPositionsRange + { + TilePosition mBegin; + TilePosition mEnd; + }; +} + +#endif diff --git a/components/detournavigator/version.hpp b/components/detournavigator/version.hpp new file mode 100644 index 0000000000..792680a7d5 --- /dev/null +++ b/components/detournavigator/version.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H + +#include +#include + +namespace DetourNavigator +{ + struct Version + { + std::size_t mGeneration = 0; + std::size_t mRevision = 0; + + friend inline auto tie(const Version& value) + { + return std::tie(value.mGeneration, value.mRevision); + } + + friend inline bool operator<(const Version& lhs, const Version& rhs) + { + return tie(lhs) < tie(rhs); + } + + friend inline bool operator<=(const Version& lhs, const Version& rhs) + { + return tie(lhs) <= tie(rhs); + } + + friend inline bool operator==(const Version& lhs, const Version& rhs) + { + return tie(lhs) == tie(rhs); + } + + friend inline bool operator!=(const Version& lhs, const Version& rhs) + { + return !(lhs == rhs); + } + }; +} + +#endif diff --git a/components/detournavigator/waitconditiontype.hpp b/components/detournavigator/waitconditiontype.hpp new file mode 100644 index 0000000000..06a5901287 --- /dev/null +++ b/components/detournavigator/waitconditiontype.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_WAITCONDITIONTYPE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_WAITCONDITIONTYPE_H + +namespace DetourNavigator +{ + enum class WaitConditionType + { + requiredTilesPresent, + allJobsDone, + }; +} + +#endif diff --git a/components/esm/activespells.cpp b/components/esm/activespells.cpp deleted file mode 100644 index 4017a4933e..0000000000 --- a/components/esm/activespells.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "activespells.hpp" - -#include "esmreader.hpp" -#include "esmwriter.hpp" - -namespace ESM -{ - - void ActiveSpells::save(ESMWriter &esm) const - { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - esm.writeHNString ("ID__", it->first); - - const ActiveSpellParams& params = it->second; - - esm.writeHNT ("CAST", params.mCasterActorId); - esm.writeHNString ("DISP", params.mDisplayName); - - for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) - { - esm.writeHNT ("MGEF", effectIt->mEffectId); - if (effectIt->mArg != -1) - esm.writeHNT ("ARG_", effectIt->mArg); - esm.writeHNT ("MAGN", effectIt->mMagnitude); - esm.writeHNT ("DURA", effectIt->mDuration); - esm.writeHNT ("EIND", effectIt->mEffectIndex); - esm.writeHNT ("LEFT", effectIt->mTimeLeft); - } - } - } - - void ActiveSpells::load(ESMReader &esm) - { - int format = esm.getFormat(); - - while (esm.isNextSub("ID__")) - { - std::string spellId = esm.getHString(); - - ActiveSpellParams params; - esm.getHNT (params.mCasterActorId, "CAST"); - params.mDisplayName = esm.getHNString ("DISP"); - - // spell casting timestamp, no longer used - if (esm.isNextSub("TIME")) - esm.skipHSub(); - - while (esm.isNextSub("MGEF")) - { - ActiveEffect effect; - esm.getHT(effect.mEffectId); - effect.mArg = -1; - esm.getHNOT(effect.mArg, "ARG_"); - esm.getHNT (effect.mMagnitude, "MAGN"); - esm.getHNT (effect.mDuration, "DURA"); - effect.mEffectIndex = -1; - esm.getHNOT (effect.mEffectIndex, "EIND"); - if (format < 9) - effect.mTimeLeft = effect.mDuration; - else - esm.getHNT (effect.mTimeLeft, "LEFT"); - - params.mEffects.push_back(effect); - } - mSpells.insert(std::make_pair(spellId, params)); - } - } -} diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp deleted file mode 100644 index 4b9852d656..0000000000 --- a/components/esm/cellref.cpp +++ /dev/null @@ -1,234 +0,0 @@ -#include "cellref.hpp" - -#include - -#include "esmreader.hpp" -#include "esmwriter.hpp" - -void ESM::RefNum::load (ESMReader& esm, bool wide, const std::string& tag) -{ - if (wide) - esm.getHNT (*this, tag.c_str(), 8); - else - esm.getHNT (mIndex, tag.c_str()); -} - -void ESM::RefNum::save (ESMWriter &esm, bool wide, const std::string& tag) const -{ - if (wide) - esm.writeHNT (tag, *this, 8); - else - { - int refNum = (mIndex & 0xffffff) | ((hasContentFile() ? mContentFile : 0xff)<<24); - - esm.writeHNT (tag, refNum, 4); - } -} - - -void ESM::CellRef::load (ESMReader& esm, bool &isDeleted, bool wideRefNum) -{ - loadId(esm, wideRefNum); - loadData(esm, isDeleted); -} - -void ESM::CellRef::loadId (ESMReader& esm, bool wideRefNum) -{ - // According to Hrnchamd, this does not belong to the actual ref. Instead, it is a marker indicating that - // the following refs are part of a "temp refs" section. A temp ref is not being tracked by the moved references system. - // Its only purpose is a performance optimization for "immovable" things. We don't need this, and it's problematic anyway, - // because any item can theoretically be moved by a script. - if (esm.isNextSub ("NAM0")) - esm.skipHSub(); - - blank(); - - mRefNum.load (esm, wideRefNum); - - mRefID = esm.getHNOString ("NAME"); - if (mRefID.empty()) - { - Log(Debug::Warning) << "Warning: got CellRef with empty RefId in " << esm.getName() << " 0x" << std::hex << esm.getFileOffset(); - } -} - -void ESM::CellRef::loadData(ESMReader &esm, bool &isDeleted) -{ - isDeleted = false; - - bool isLoaded = false; - while (!isLoaded && esm.hasMoreSubs()) - { - esm.getSubName(); - switch (esm.retSubName().intval) - { - case ESM::FourCC<'U','N','A','M'>::value: - esm.getHT(mReferenceBlocked); - break; - case ESM::FourCC<'X','S','C','L'>::value: - esm.getHT(mScale); - if (mScale < 0.5) - mScale = 0.5; - else if (mScale > 2) - mScale = 2; - break; - case ESM::FourCC<'A','N','A','M'>::value: - mOwner = esm.getHString(); - break; - case ESM::FourCC<'B','N','A','M'>::value: - mGlobalVariable = esm.getHString(); - break; - case ESM::FourCC<'X','S','O','L'>::value: - mSoul = esm.getHString(); - break; - case ESM::FourCC<'C','N','A','M'>::value: - mFaction = esm.getHString(); - break; - case ESM::FourCC<'I','N','D','X'>::value: - esm.getHT(mFactionRank); - break; - case ESM::FourCC<'X','C','H','G'>::value: - esm.getHT(mEnchantmentCharge); - break; - case ESM::FourCC<'I','N','T','V'>::value: - esm.getHT(mChargeInt); - break; - case ESM::FourCC<'N','A','M','9'>::value: - esm.getHT(mGoldValue); - break; - case ESM::FourCC<'D','O','D','T'>::value: - esm.getHT(mDoorDest); - mTeleport = true; - break; - case ESM::FourCC<'D','N','A','M'>::value: - mDestCell = esm.getHString(); - break; - case ESM::FourCC<'F','L','T','V'>::value: - esm.getHT(mLockLevel); - break; - case ESM::FourCC<'K','N','A','M'>::value: - mKey = esm.getHString(); - break; - case ESM::FourCC<'T','N','A','M'>::value: - mTrap = esm.getHString(); - break; - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mPos, 24); - break; - case ESM::FourCC<'N','A','M','0'>::value: - esm.skipHSub(); - break; - case ESM::SREC_DELE: - esm.skipHSub(); - isDeleted = true; - break; - default: - esm.cacheSubName(); - isLoaded = true; - break; - } - } - - if (mLockLevel == 0 && !mKey.empty()) - { - mLockLevel = UnbreakableLock; - mTrap.clear(); - } -} - -void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory, bool isDeleted) const -{ - mRefNum.save (esm, wideRefNum); - - esm.writeHNCString("NAME", mRefID); - - if (isDeleted) { - esm.writeHNCString("DELE", ""); - return; - } - - if (mScale != 1.0) { - float scale = mScale; - if (scale < 0.5) - scale = 0.5; - else if (scale > 2) - scale = 2; - esm.writeHNT("XSCL", scale); - } - - if (!inInventory) - esm.writeHNOCString("ANAM", mOwner); - - esm.writeHNOCString("BNAM", mGlobalVariable); - esm.writeHNOCString("XSOL", mSoul); - - if (!inInventory) - { - esm.writeHNOCString("CNAM", mFaction); - if (mFactionRank != -2) - { - esm.writeHNT("INDX", mFactionRank); - } - } - - if (mEnchantmentCharge != -1) - esm.writeHNT("XCHG", mEnchantmentCharge); - - if (mChargeInt != -1) - esm.writeHNT("INTV", mChargeInt); - - if (mGoldValue > 1) - esm.writeHNT("NAM9", mGoldValue); - - if (!inInventory && mTeleport) - { - esm.writeHNT("DODT", mDoorDest); - esm.writeHNOCString("DNAM", mDestCell); - } - - if (!inInventory && mLockLevel != 0) { - esm.writeHNT("FLTV", mLockLevel); - } - - if (!inInventory) - { - esm.writeHNOCString ("KNAM", mKey); - esm.writeHNOCString ("TNAM", mTrap); - } - - if (mReferenceBlocked != -1) - esm.writeHNT("UNAM", mReferenceBlocked); - - if (!inInventory) - esm.writeHNT("DATA", mPos, 24); -} - -void ESM::CellRef::blank() -{ - mRefNum.unset(); - mRefID.clear(); - mScale = 1; - mOwner.clear(); - mGlobalVariable.clear(); - mSoul.clear(); - mFaction.clear(); - mFactionRank = -2; - mChargeInt = -1; - mChargeIntRemainder = 0.0f; - mEnchantmentCharge = -1; - mGoldValue = 1; - mDestCell.clear(); - mLockLevel = 0; - mKey.clear(); - mTrap.clear(); - mReferenceBlocked = -1; - mTeleport = false; - - for (int i=0; i<3; ++i) - { - mDoorDest.pos[i] = 0; - mDoorDest.rot[i] = 0; - mPos.pos[i] = 0; - mPos.rot[i] = 0; - } -} diff --git a/components/esm/common.cpp b/components/esm/common.cpp new file mode 100644 index 0000000000..d04033edef --- /dev/null +++ b/components/esm/common.cpp @@ -0,0 +1,15 @@ +#include "sstream" + +namespace ESM +{ + std::string printName(const std::uint32_t typeId) + { + unsigned char typeName[4]; + typeName[0] = typeId & 0xff; + typeName[1] = (typeId >> 8) & 0xff; + typeName[2] = (typeId >> 16) & 0xff; + typeName[3] = (typeId >> 24) & 0xff; + + return std::string((char*)typeName, 4); + } +} diff --git a/components/esm/common.hpp b/components/esm/common.hpp new file mode 100644 index 0000000000..af19572c03 --- /dev/null +++ b/components/esm/common.hpp @@ -0,0 +1,46 @@ +#ifndef COMPONENT_ESM_COMMON_H +#define COMPONENT_ESM_COMMON_H + +#include +#include + +namespace ESM +{ +#pragma pack(push, 1) + union ESMVersion + { + float f; + std::uint32_t ui; + }; + + union TypeId + { + std::uint32_t value; + char name[4]; // record type in ascii + }; +#pragma pack(pop) + + enum ESMVersions + { + VER_120 = 0x3f99999a, // TES3 + VER_130 = 0x3fa66666, // TES3 + VER_080 = 0x3f4ccccd, // TES4 + VER_100 = 0x3f800000, // TES4 + VER_132 = 0x3fa8f5c3, // FONV Courier's Stash, DeadMoney + VER_133 = 0x3faa3d71, // FONV HonestHearts + VER_134 = 0x3fab851f, // FONV, GunRunnersArsenal, LonesomeRoad, OldWorldBlues + VER_094 = 0x3f70a3d7, // TES5/FO3 + VER_170 = 0x3fd9999a // TES5 + }; + + // Defines another files (esm or esp) that this file depends upon. + struct MasterData + { + std::string name; + std::uint64_t size; + }; + + std::string printName(const std::uint32_t typeId); +} + +#endif // COMPONENT_ESM_COMMON_H diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 0f9cefab12..f3e7e9506c 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -3,6 +3,8 @@ #include +#include + #include namespace ESM @@ -39,9 +41,6 @@ enum RangeType RT_Target = 2 }; -#pragma pack(push) -#pragma pack(1) - // Position and rotation struct Position { @@ -54,8 +53,18 @@ struct Position { return osg::Vec3f(pos[0], pos[1], pos[2]); } + + osg::Vec3f asRotationVec3() const + { + return osg::Vec3f(rot[0], rot[1], rot[2]); + } + + friend inline bool operator<(const Position& l, const Position& r) + { + const auto tuple = [](const Position& v) { return std::tuple(v.asVec3(), v.asRotationVec3()); }; + return tuple(l) < tuple(r); + } }; -#pragma pack(pop) bool inline operator== (const Position& left, const Position& right) noexcept { @@ -77,96 +86,108 @@ bool inline operator!= (const Position& left, const Position& right) noexcept left.rot[2] != right.rot[2]; } -template -struct FourCC -{ - static const unsigned int value = (((((d << 8) | c) << 8) | b) << 8) | a; -}; +template +constexpr unsigned int fourCC(const char(&name)[len]) { + static_assert(len == 5, "Constant must be 4 characters long. (Plus null terminator)"); + return static_cast(name[0]) | (static_cast(name[1]) << 8) | (static_cast(name[2]) << 16) | (static_cast(name[3]) << 24); +} -enum RecNameInts +enum RecNameInts : unsigned int { + // Special values. Can not be used in any ESM. + // Added to this enum to guarantee that the values don't collide with any records. + REC_INTERNAL_PLAYER = 0, + REC_INTERNAL_MARKER = 1, + // format 0 / legacy - REC_ACTI = FourCC<'A','C','T','I'>::value, - REC_ALCH = FourCC<'A','L','C','H'>::value, - REC_APPA = FourCC<'A','P','P','A'>::value, - REC_ARMO = FourCC<'A','R','M','O'>::value, - REC_BODY = FourCC<'B','O','D','Y'>::value, - REC_BOOK = FourCC<'B','O','O','K'>::value, - REC_BSGN = FourCC<'B','S','G','N'>::value, - REC_CELL = FourCC<'C','E','L','L'>::value, - REC_CLAS = FourCC<'C','L','A','S'>::value, - REC_CLOT = FourCC<'C','L','O','T'>::value, - REC_CNTC = FourCC<'C','N','T','C'>::value, - REC_CONT = FourCC<'C','O','N','T'>::value, - REC_CREA = FourCC<'C','R','E','A'>::value, - REC_CREC = FourCC<'C','R','E','C'>::value, - REC_DIAL = FourCC<'D','I','A','L'>::value, - REC_DOOR = FourCC<'D','O','O','R'>::value, - REC_ENCH = FourCC<'E','N','C','H'>::value, - REC_FACT = FourCC<'F','A','C','T'>::value, - REC_GLOB = FourCC<'G','L','O','B'>::value, - REC_GMST = FourCC<'G','M','S','T'>::value, - REC_INFO = FourCC<'I','N','F','O'>::value, - REC_INGR = FourCC<'I','N','G','R'>::value, - REC_LAND = FourCC<'L','A','N','D'>::value, - REC_LEVC = FourCC<'L','E','V','C'>::value, - REC_LEVI = FourCC<'L','E','V','I'>::value, - REC_LIGH = FourCC<'L','I','G','H'>::value, - REC_LOCK = FourCC<'L','O','C','K'>::value, - REC_LTEX = FourCC<'L','T','E','X'>::value, - REC_MGEF = FourCC<'M','G','E','F'>::value, - REC_MISC = FourCC<'M','I','S','C'>::value, - REC_NPC_ = FourCC<'N','P','C','_'>::value, - REC_NPCC = FourCC<'N','P','C','C'>::value, - REC_PGRD = FourCC<'P','G','R','D'>::value, - REC_PROB = FourCC<'P','R','O','B'>::value, - REC_RACE = FourCC<'R','A','C','E'>::value, - REC_REGN = FourCC<'R','E','G','N'>::value, - REC_REPA = FourCC<'R','E','P','A'>::value, - REC_SCPT = FourCC<'S','C','P','T'>::value, - REC_SKIL = FourCC<'S','K','I','L'>::value, - REC_SNDG = FourCC<'S','N','D','G'>::value, - REC_SOUN = FourCC<'S','O','U','N'>::value, - REC_SPEL = FourCC<'S','P','E','L'>::value, - REC_SSCR = FourCC<'S','S','C','R'>::value, - REC_STAT = FourCC<'S','T','A','T'>::value, - REC_WEAP = FourCC<'W','E','A','P'>::value, + REC_ACTI = fourCC("ACTI"), + REC_ALCH = fourCC("ALCH"), + REC_APPA = fourCC("APPA"), + REC_ARMO = fourCC("ARMO"), + REC_BODY = fourCC("BODY"), + REC_BOOK = fourCC("BOOK"), + REC_BSGN = fourCC("BSGN"), + REC_CELL = fourCC("CELL"), + REC_CLAS = fourCC("CLAS"), + REC_CLOT = fourCC("CLOT"), + REC_CNTC = fourCC("CNTC"), + REC_CONT = fourCC("CONT"), + REC_CREA = fourCC("CREA"), + REC_CREC = fourCC("CREC"), + REC_DIAL = fourCC("DIAL"), + REC_DOOR = fourCC("DOOR"), + REC_ENCH = fourCC("ENCH"), + REC_FACT = fourCC("FACT"), + REC_GLOB = fourCC("GLOB"), + REC_GMST = fourCC("GMST"), + REC_INFO = fourCC("INFO"), + REC_INGR = fourCC("INGR"), + REC_LAND = fourCC("LAND"), + REC_LEVC = fourCC("LEVC"), + REC_LEVI = fourCC("LEVI"), + REC_LIGH = fourCC("LIGH"), + REC_LOCK = fourCC("LOCK"), + REC_LTEX = fourCC("LTEX"), + REC_MGEF = fourCC("MGEF"), + REC_MISC = fourCC("MISC"), + REC_NPC_ = fourCC("NPC_"), + REC_NPCC = fourCC("NPCC"), + REC_PGRD = fourCC("PGRD"), + REC_PROB = fourCC("PROB"), + REC_RACE = fourCC("RACE"), + REC_REGN = fourCC("REGN"), + REC_REPA = fourCC("REPA"), + REC_SCPT = fourCC("SCPT"), + REC_SKIL = fourCC("SKIL"), + REC_SNDG = fourCC("SNDG"), + REC_SOUN = fourCC("SOUN"), + REC_SPEL = fourCC("SPEL"), + REC_SSCR = fourCC("SSCR"), + REC_STAT = fourCC("STAT"), + REC_WEAP = fourCC("WEAP"), // format 0 - saved games - REC_SAVE = FourCC<'S','A','V','E'>::value, - REC_JOUR_LEGACY = FourCC<0xa4,'U','O','R'>::value, // "\xa4UOR", rather than "JOUR", little oversight when magic numbers were - // calculated by hand, needs to be supported for older files now - REC_JOUR = FourCC<'J','O','U','R'>::value, - REC_QUES = FourCC<'Q','U','E','S'>::value, - REC_GSCR = FourCC<'G','S','C','R'>::value, - REC_PLAY = FourCC<'P','L','A','Y'>::value, - REC_CSTA = FourCC<'C','S','T','A'>::value, - REC_GMAP = FourCC<'G','M','A','P'>::value, - REC_DIAS = FourCC<'D','I','A','S'>::value, - REC_WTHR = FourCC<'W','T','H','R'>::value, - REC_KEYS = FourCC<'K','E','Y','S'>::value, - REC_DYNA = FourCC<'D','Y','N','A'>::value, - REC_ASPL = FourCC<'A','S','P','L'>::value, - REC_ACTC = FourCC<'A','C','T','C'>::value, - REC_MPRJ = FourCC<'M','P','R','J'>::value, - REC_PROJ = FourCC<'P','R','O','J'>::value, - REC_DCOU = FourCC<'D','C','O','U'>::value, - REC_MARK = FourCC<'M','A','R','K'>::value, - REC_ENAB = FourCC<'E','N','A','B'>::value, - REC_CAM_ = FourCC<'C','A','M','_'>::value, - REC_STLN = FourCC<'S','T','L','N'>::value, - REC_INPU = FourCC<'I','N','P','U'>::value, + REC_SAVE = fourCC("SAVE"), + REC_JOUR_LEGACY = fourCC("\xa4UOR"), // "\xa4UOR", rather than "JOUR", little oversight when magic numbers were + // calculated by hand, needs to be supported for older files now + REC_JOUR = fourCC("JOUR"), + REC_QUES = fourCC("QUES"), + REC_GSCR = fourCC("GSCR"), + REC_PLAY = fourCC("PLAY"), + REC_CSTA = fourCC("CSTA"), + REC_GMAP = fourCC("GMAP"), + REC_DIAS = fourCC("DIAS"), + REC_WTHR = fourCC("WTHR"), + REC_KEYS = fourCC("KEYS"), + REC_DYNA = fourCC("DYNA"), + REC_ASPL = fourCC("ASPL"), + REC_ACTC = fourCC("ACTC"), + REC_MPRJ = fourCC("MPRJ"), + REC_PROJ = fourCC("PROJ"), + REC_DCOU = fourCC("DCOU"), + REC_MARK = fourCC("MARK"), + REC_ENAB = fourCC("ENAB"), + REC_CAM_ = fourCC("CAM_"), + REC_STLN = fourCC("STLN"), + REC_INPU = fourCC("INPU"), // format 1 - REC_FILT = FourCC<'F','I','L','T'>::value, - REC_DBGP = FourCC<'D','B','G','P'>::value ///< only used in project files + REC_FILT = fourCC("FILT"), + REC_DBGP = fourCC("DBGP"), ///< only used in project files + REC_LUAL = fourCC("LUAL"), // LuaScriptsCfg (only in omwgame or omwaddon) + + // format 16 - Lua scripts in saved games + REC_LUAM = fourCC("LUAM"), // LuaManager data + + // format 21 - Random state in saved games. + REC_RAND = fourCC("RAND"), // Random state. }; /// Common subrecords enum SubRecNameInts { - SREC_DELE = ESM::FourCC<'D','E','L','E'>::value, - SREC_NAME = ESM::FourCC<'N','A','M','E'>::value + SREC_DELE = ESM::fourCC("DELE"), + SREC_NAME = ESM::fourCC("NAME") }; } diff --git a/components/esm/esmcommon.hpp b/components/esm/esmcommon.hpp index 232a24fcf3..2685371ec0 100644 --- a/components/esm/esmcommon.hpp +++ b/components/esm/esmcommon.hpp @@ -4,8 +4,10 @@ #include #include #include - -#include +#include +#include +#include +#include namespace ESM { @@ -15,119 +17,159 @@ enum Version VER_13 = 0x3fa66666 }; +enum RecordFlag + { + // This flag exists, but is not used to determine if a record has been deleted while loading + FLAG_Deleted = 0x00000020, + FLAG_Persistent = 0x00000400, + FLAG_Ignored = 0x00001000, + FLAG_Blocked = 0x00002000 + }; -// CRTP for FIXED_STRING class, a structure used for holding fixed-length strings -template< template class DERIVED, size_t SIZE> -class FIXED_STRING_BASE +template +struct FixedString { - /* The following methods must be implemented in derived classes: - * char const* ro_data() const; // return pointer to ro buffer - * char* rw_data(); // return pointer to rw buffer - */ -public: - enum { size = SIZE }; - - template - bool operator==(char const (&str)[OTHER_SIZE]) const - { - size_t other_len = strnlen(str, OTHER_SIZE); - if (other_len != this->length()) - return false; - return std::strncmp(self()->ro_data(), str, size) == 0; - } + static_assert(capacity > 0); - //this operator will not be used for char[N], only for char* - template::value>::type> - bool operator==(const T* const& str) const + static constexpr std::size_t sCapacity = capacity; + + char mData[capacity]; + + FixedString() = default; + + template + constexpr FixedString(const char (&value)[size]) noexcept + : mData() { - char const* const data = self()->ro_data(); - for(size_t i = 0; i < size; ++i) + if constexpr (capacity == sizeof(std::uint32_t)) { - if(data[i] != str[i]) return false; - else if(data[i] == '\0') return true; + static_assert(capacity == size || capacity + 1 == size); + if constexpr (capacity + 1 == size) + assert(value[capacity] == '\0'); + for (std::size_t i = 0; i < capacity; ++i) + mData[i] = value[i]; + } + else + { + const std::size_t length = std::min(capacity, size); + for (std::size_t i = 0; i < length; ++i) + mData[i] = value[i]; + mData[std::min(capacity - 1, length)] = '\0'; } - return str[size] == '\0'; } - bool operator!=(const char* const str) const { return !( (*this) == str ); } - bool operator==(const std::string& str) const + constexpr explicit FixedString(std::uint32_t value) noexcept + : mData() { - return (*this) == str.c_str(); + static_assert(capacity == sizeof(std::uint32_t)); + for (std::size_t i = 0; i < capacity; ++i) + mData[i] = static_cast((value >> (i * std::numeric_limits::digits)) & std::numeric_limits::max()); } - bool operator!=(const std::string& str) const { return !( (*this) == str ); } - static size_t data_size() { return size; } - size_t length() const { return strnlen(self()->ro_data(), size); } - std::string toString() const { return std::string(self()->ro_data(), this->length()); } + template + constexpr explicit FixedString(T value) noexcept + : FixedString(static_cast(value)) {} - void assign(const std::string& value) + std::string_view toStringView() const noexcept { - std::strncpy(self()->rw_data(), value.c_str(), size-1); - self()->rw_data()[size-1] = '\0'; + return std::string_view(mData, strnlen(mData, capacity)); } - void clear() { this->assign(""); } -private: - DERIVED const* self() const + std::string toString() const { - return static_cast const*>(this); + return std::string(toStringView()); } - // write the non-const version in terms of the const version - // Effective C++ 3rd ed., Item 3 (p. 24-25) - DERIVED* self() + std::uint32_t toInt() const noexcept { - return const_cast*>(static_cast(this)->self()); + static_assert(capacity == sizeof(std::uint32_t)); + std::uint32_t value; + std::memcpy(&value, mData, capacity); + return value; } -}; -// Generic implementation -template -struct FIXED_STRING : public FIXED_STRING_BASE -{ - char data[SIZE]; + void clear() noexcept + { + std::memset(mData, 0, capacity); + } - char const* ro_data() const { return data; } - char* rw_data() { return data; } -}; + void assign(std::string_view value) noexcept + { + if (value.empty()) + { + clear(); + return; + } -// In the case of SIZE=4, it can be more efficient to match the string -// as a 32 bit number, therefore the struct is implemented as a union with an int. -template <> -struct FIXED_STRING<4> : public FIXED_STRING_BASE -{ - union { - char data[4]; - uint32_t intval; - }; + if (value.size() < capacity) + { + if constexpr (capacity == sizeof(std::uint32_t)) + std::memset(mData, 0, capacity); + std::memcpy(mData, value.data(), value.size()); + if constexpr (capacity != sizeof(std::uint32_t)) + mData[value.size()] = '\0'; + return; + } - using FIXED_STRING_BASE::operator==; - using FIXED_STRING_BASE::operator!=; + std::memcpy(mData, value.data(), capacity); - bool operator==(uint32_t v) const { return v == intval; } - bool operator!=(uint32_t v) const { return v != intval; } + if constexpr (capacity != sizeof(std::uint32_t)) + mData[capacity - 1] = '\0'; + } - void assign(const std::string& value) + FixedString& operator=(std::uint32_t value) noexcept { - intval = 0; - size_t length = value.size(); - if (length == 0) return; - data[0] = value[0]; - if (length == 1) return; - data[1] = value[1]; - if (length == 2) return; - data[2] = value[2]; - if (length == 3) return; - data[3] = value[3]; + static_assert(capacity == sizeof(value)); + std::memcpy(&mData, &value, capacity); + return *this; } - - char const* ro_data() const { return data; } - char* rw_data() { return data; } }; -typedef FIXED_STRING<4> NAME; -typedef FIXED_STRING<32> NAME32; -typedef FIXED_STRING<64> NAME64; +template >> +inline bool operator==(const FixedString& lhs, const T* const& rhs) noexcept +{ + for (std::size_t i = 0; i < capacity; ++i) + { + if (lhs.mData[i] != rhs[i]) + return false; + if (lhs.mData[i] == '\0') + return true; + } + return rhs[capacity] == '\0'; +} + +template +inline bool operator==(const FixedString& lhs, const std::string& rhs) noexcept +{ + return lhs == rhs.c_str(); +} + +template +inline bool operator==(const FixedString& lhs, const char (&rhs)[rhsSize]) noexcept +{ + return strnlen(rhs, rhsSize) == strnlen(lhs.mData, capacity) + && std::strncmp(lhs.mData, rhs, capacity) == 0; +} + +inline bool operator==(const FixedString<4>& lhs, std::uint32_t rhs) noexcept +{ + return lhs.toInt() == rhs; +} + +inline bool operator==(const FixedString<4>& lhs, const FixedString<4>& rhs) noexcept +{ + return lhs.toInt() == rhs.toInt(); +} + +template +inline bool operator!=(const FixedString& lhs, const Rhs& rhs) noexcept +{ + return !(lhs == rhs); +} + +using NAME = FixedString<4>; +using NAME32 = FixedString<32>; +using NAME64 = FixedString<64>; /* This struct defines a file 'context' which can be saved and later restored by an ESMReader instance. It will save the position within diff --git a/components/esm/format.cpp b/components/esm/format.cpp new file mode 100644 index 0000000000..7ed109a580 --- /dev/null +++ b/components/esm/format.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include +#include + +namespace ESM +{ + namespace + { + bool isValidFormat(std::uint32_t value) + { + return value == static_cast(Format::Tes3) + || value == static_cast(Format::Tes4); + } + + Format toFormat(std::uint32_t value) + { + if (!isValidFormat(value)) + throw std::runtime_error("Invalid format: " + std::to_string(value)); + return static_cast(value); + } + } + + Format readFormat(std::istream& stream) + { + std::uint32_t format = 0; + stream.read(reinterpret_cast(&format), sizeof(format)); + if (stream.gcount() != sizeof(format)) + throw std::runtime_error("Not enough bytes to read file header"); + return toFormat(format); + } + + Format parseFormat(std::string_view value) + { + if (value.size() != sizeof(std::uint32_t)) + throw std::logic_error("Invalid format value: " + std::string(value)); + std::uint32_t format; + std::memcpy(&format, value.data(), sizeof(std::uint32_t)); + return toFormat(format); + } +} diff --git a/components/esm/format.hpp b/components/esm/format.hpp new file mode 100644 index 0000000000..6bb5e270c1 --- /dev/null +++ b/components/esm/format.hpp @@ -0,0 +1,23 @@ +#ifndef COMPONENT_ESM_FORMAT_H +#define COMPONENT_ESM_FORMAT_H + +#include "defs.hpp" + +#include +#include +#include + +namespace ESM +{ + enum class Format : std::uint32_t + { + Tes3 = fourCC("TES3"), + Tes4 = fourCC("TES4"), + }; + + Format readFormat(std::istream& stream); + + Format parseFormat(std::string_view value); +} + +#endif diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp new file mode 100644 index 0000000000..d541482ad6 --- /dev/null +++ b/components/esm/luascripts.cpp @@ -0,0 +1,202 @@ +#include "luascripts.hpp" + +#include "components/esm3/esmreader.hpp" +#include "components/esm3/esmwriter.hpp" + +#include + +// List of all records, that are related to Lua. +// +// Records: +// LUAL - LuaScriptsCfg - list of all scripts (in content files) +// LUAM - MWLua::LuaManager (in saves) +// +// Subrecords: +// LUAF - LuaScriptCfg::mFlags and ESM::RecNameInts list +// LUAW - Start of MWLua::WorldView data +// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) +// LUAS - VFS path to a Lua script +// LUAD - Serialized Lua variable +// LUAT - MWLua::ScriptsContainer::Timer +// LUAC - Name of a timer callback (string) +// LUAR - Attach script to a specific record (LuaScriptCfg::PerRecordCfg) +// LUAI - Attach script to a specific instance (LuaScriptCfg::PerRefCfg) + +void ESM::saveLuaBinaryData(ESMWriter& esm, const std::string& data) +{ + if (data.empty()) + return; + esm.startSubRecord("LUAD"); + esm.write(data.data(), data.size()); + esm.endRecord("LUAD"); +} + +std::string ESM::loadLuaBinaryData(ESMReader& esm) +{ + std::string data; + if (esm.isNextSub("LUAD")) + { + esm.getSubHeader(); + data.resize(esm.getSubSize()); + esm.getExact(data.data(), static_cast(data.size())); + } + return data; +} + +static bool readBool(ESM::ESMReader& esm) +{ + char c; + esm.getT(c); + return c != 0; +} + +void ESM::LuaScriptsCfg::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + mScripts.emplace_back(); + ESM::LuaScriptCfg& script = mScripts.back(); + script.mScriptPath = esm.getHString(); + + esm.getSubNameIs("LUAF"); + esm.getSubHeader(); + if (esm.getSubSize() < 4 || (esm.getSubSize() % 4 != 0)) + esm.fail("Incorrect LUAF size"); + esm.getT(script.mFlags); + script.mTypes.resize((esm.getSubSize() - 4) / 4); + for (uint32_t& type : script.mTypes) + esm.getT(type); + + script.mInitializationData = loadLuaBinaryData(esm); + + while (esm.isNextSub("LUAR")) + { + esm.getSubHeader(); + script.mRecords.emplace_back(); + ESM::LuaScriptCfg::PerRecordCfg& recordCfg = script.mRecords.back(); + recordCfg.mRecordId.resize(esm.getSubSize() - 1); + recordCfg.mAttach = readBool(esm); + esm.getExact(recordCfg.mRecordId.data(), static_cast(recordCfg.mRecordId.size())); + recordCfg.mInitializationData = loadLuaBinaryData(esm); + } + while (esm.isNextSub("LUAI")) + { + esm.getSubHeader(); + script.mRefs.emplace_back(); + ESM::LuaScriptCfg::PerRefCfg& refCfg = script.mRefs.back(); + refCfg.mAttach = readBool(esm); + esm.getT(refCfg.mRefnumIndex); + esm.getT(refCfg.mRefnumContentFile); + refCfg.mInitializationData = loadLuaBinaryData(esm); + } + } +} + +void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm) +{ + auto adjustRefNumFn = [&esm](int contentFile) -> int + { + if (contentFile == 0) + return esm.getIndex(); + else if (contentFile > 0 && contentFile <= static_cast(esm.getParentFileIndices().size())) + return esm.getParentFileIndices()[contentFile - 1]; + else + throw std::runtime_error("Incorrect contentFile index"); + }; + + lua_State* L = lua_open(); + LuaUtil::BasicSerializer serializer(adjustRefNumFn); + + auto adjustLuaData = [&](std::string& data) + { + if (data.empty()) + return; + sol::object luaData = LuaUtil::deserialize(L, data, &serializer); + data = LuaUtil::serialize(luaData, &serializer); + }; + + for (LuaScriptCfg& script : mScripts) + { + adjustLuaData(script.mInitializationData); + for (LuaScriptCfg::PerRecordCfg& recordCfg : script.mRecords) + adjustLuaData(recordCfg.mInitializationData); + for (LuaScriptCfg::PerRefCfg& refCfg : script.mRefs) + { + adjustLuaData(refCfg.mInitializationData); + refCfg.mRefnumContentFile = adjustRefNumFn(refCfg.mRefnumContentFile); + } + } + lua_close(L); +} + +void ESM::LuaScriptsCfg::save(ESMWriter& esm) const +{ + for (const LuaScriptCfg& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + esm.startSubRecord("LUAF"); + esm.writeT(script.mFlags); + for (uint32_t type : script.mTypes) + esm.writeT(type); + esm.endRecord("LUAF"); + saveLuaBinaryData(esm, script.mInitializationData); + for (const LuaScriptCfg::PerRecordCfg& recordCfg : script.mRecords) + { + esm.startSubRecord("LUAR"); + esm.writeT(recordCfg.mAttach ? 1 : 0); + esm.write(recordCfg.mRecordId.data(), recordCfg.mRecordId.size()); + esm.endRecord("LUAR"); + saveLuaBinaryData(esm, recordCfg.mInitializationData); + } + for (const LuaScriptCfg::PerRefCfg& refCfg : script.mRefs) + { + esm.startSubRecord("LUAI"); + esm.writeT(refCfg.mAttach ? 1 : 0); + esm.writeT(refCfg.mRefnumIndex); + esm.writeT(refCfg.mRefnumContentFile); + esm.endRecord("LUAI"); + saveLuaBinaryData(esm, refCfg.mInitializationData); + } + } +} + +void ESM::LuaScripts::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + std::string data = loadLuaBinaryData(esm); + std::vector timers; + while (esm.isNextSub("LUAT")) + { + esm.getSubHeader(); + LuaTimer timer; + esm.getT(timer.mType); + esm.getT(timer.mTime); + timer.mCallbackName = esm.getHNString("LUAC"); + timer.mCallbackArgument = loadLuaBinaryData(esm); + timers.push_back(std::move(timer)); + } + mScripts.push_back({std::move(name), std::move(data), std::move(timers)}); + } +} + +void ESM::LuaScripts::save(ESMWriter& esm) const +{ + for (const LuaScript& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + saveLuaBinaryData(esm, script.mData); + for (const LuaTimer& timer : script.mTimers) + { + esm.startSubRecord("LUAT"); + esm.writeT(timer.mType); + esm.writeT(timer.mTime); + esm.endRecord("LUAT"); + esm.writeHNString("LUAC", timer.mCallbackName); + if (!timer.mCallbackArgument.empty()) + saveLuaBinaryData(esm, timer.mCallbackArgument); + + } + } +} diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp new file mode 100644 index 0000000000..c3bb94ac70 --- /dev/null +++ b/components/esm/luascripts.hpp @@ -0,0 +1,105 @@ +#ifndef OPENMW_ESM_LUASCRIPTS_H +#define OPENMW_ESM_LUASCRIPTS_H + +#include +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // LuaScriptCfg, LuaScriptsCfg are used in content files. + + struct LuaScriptCfg + { + using Flags = uint32_t; + static constexpr Flags sGlobal = 1ull << 0; // start as a global script + static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script + static constexpr Flags sPlayer = 1ull << 2; // auto attach to players + + static constexpr Flags sMerge = 1ull << 3; // merge with configuration for this script from previous content files. + + std::string mScriptPath; // VFS path to the script. + std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'. + Flags mFlags; // bitwise OR of Flags. + + // Auto attach as a local script to objects of specific types (i.e. Container, Door, Activator, etc.) + std::vector mTypes; // values are ESM::RecNameInts + + // Auto attach as a local script to objects with specific recordIds (i.e. specific door type, or an unique NPC) + struct PerRecordCfg + { + bool mAttach; // true - attach, false - don't attach (overrides previous attach) + std::string mRecordId; + // Initialization data for this specific record. If empty than LuaScriptCfg::mInitializationData is used. + std::string mInitializationData; + }; + std::vector mRecords; + + // Auto attach as a local script to specific objects by their Refnums. The reference must be defined in the same + // content file as this LuaScriptCfg or in one of its deps. + struct PerRefCfg + { + bool mAttach; // true - attach, false - don't attach (overrides previous attach) + uint32_t mRefnumIndex; + int32_t mRefnumContentFile; + // Initialization data for this specific refnum. If empty than LuaScriptCfg::mInitializationData is used. + std::string mInitializationData; + }; + std::vector mRefs; + }; + + struct LuaScriptsCfg + { + std::vector mScripts; + + void load(ESMReader &esm); + void adjustRefNums(const ESMReader &esm); + + void save(ESMWriter &esm) const; + }; + + // LuaTimer, LuaScript, LuaScripts are used in saved game files. + // Storage structure for LuaUtil::ScriptsContainer. These are not top-level records. + // Used either for global scripts or for local scripts on a specific object. + + struct LuaTimer + { + enum class Type : bool + { + SIMULATION_TIME = 0, + GAME_TIME = 1, + }; + + Type mType; + double mTime; + std::string mCallbackName; + std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'. + }; + + struct LuaScript + { + std::string mScriptPath; + std::string mData; // Serialized Lua table. It is a binary data. Can contain '\0'. + std::vector mTimers; + }; + + struct LuaScripts + { + std::vector mScripts; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + // Saves binary string `data` (can contain '\0') as LUAD record. + void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); + + // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. + std::string loadLuaBinaryData(ESM::ESMReader& esm); + +} + +#endif + diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp deleted file mode 100644 index 898e7e4b18..0000000000 --- a/components/esm/magiceffects.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "magiceffects.hpp" - -#include "esmwriter.hpp" -#include "esmreader.hpp" - -namespace ESM -{ - -void MagicEffects::save(ESMWriter &esm) const -{ - for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) - { - esm.writeHNT("EFID", it->first); - esm.writeHNT("BASE", it->second); - } -} - -void MagicEffects::load(ESMReader &esm) -{ - while (esm.isNextSub("EFID")) - { - int id, base; - esm.getHT(id); - esm.getHNT(base, "BASE"); - mEffects.insert(std::make_pair(id, base)); - } -} - -} diff --git a/components/esm/mappings.cpp b/components/esm/mappings.cpp deleted file mode 100644 index 440e735739..0000000000 --- a/components/esm/mappings.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "mappings.hpp" - -#include - -namespace ESM -{ - ESM::BodyPart::MeshPart getMeshPart(ESM::PartReferenceType type) - { - switch(type) - { - case ESM::PRT_Head: - return ESM::BodyPart::MP_Head; - case ESM::PRT_Hair: - return ESM::BodyPart::MP_Hair; - case ESM::PRT_Neck: - return ESM::BodyPart::MP_Neck; - case ESM::PRT_Cuirass: - return ESM::BodyPart::MP_Chest; - case ESM::PRT_Groin: - return ESM::BodyPart::MP_Groin; - case ESM::PRT_RHand: - return ESM::BodyPart::MP_Hand; - case ESM::PRT_LHand: - return ESM::BodyPart::MP_Hand; - case ESM::PRT_RWrist: - return ESM::BodyPart::MP_Wrist; - case ESM::PRT_LWrist: - return ESM::BodyPart::MP_Wrist; - case ESM::PRT_RForearm: - return ESM::BodyPart::MP_Forearm; - case ESM::PRT_LForearm: - return ESM::BodyPart::MP_Forearm; - case ESM::PRT_RUpperarm: - return ESM::BodyPart::MP_Upperarm; - case ESM::PRT_LUpperarm: - return ESM::BodyPart::MP_Upperarm; - case ESM::PRT_RFoot: - return ESM::BodyPart::MP_Foot; - case ESM::PRT_LFoot: - return ESM::BodyPart::MP_Foot; - case ESM::PRT_RAnkle: - return ESM::BodyPart::MP_Ankle; - case ESM::PRT_LAnkle: - return ESM::BodyPart::MP_Ankle; - case ESM::PRT_RKnee: - return ESM::BodyPart::MP_Knee; - case ESM::PRT_LKnee: - return ESM::BodyPart::MP_Knee; - case ESM::PRT_RLeg: - return ESM::BodyPart::MP_Upperleg; - case ESM::PRT_LLeg: - return ESM::BodyPart::MP_Upperleg; - case ESM::PRT_Tail: - return ESM::BodyPart::MP_Tail; - default: - throw std::runtime_error("PartReferenceType " + - std::to_string(type) + " not associated with a mesh part"); - } - } - - std::string getBoneName(ESM::PartReferenceType type) - { - switch(type) - { - case ESM::PRT_Head: - return "head"; - case ESM::PRT_Hair: - return "head"; // This is purposeful. - case ESM::PRT_Neck: - return "neck"; - case ESM::PRT_Cuirass: - return "chest"; - case ESM::PRT_Groin: - return "groin"; - case ESM::PRT_Skirt: - return "groin"; - case ESM::PRT_RHand: - return "right hand"; - case ESM::PRT_LHand: - return "left hand"; - case ESM::PRT_RWrist: - return "right wrist"; - case ESM::PRT_LWrist: - return "left wrist"; - case ESM::PRT_Shield: - return "shield bone"; - case ESM::PRT_RForearm: - return "right forearm"; - case ESM::PRT_LForearm: - return "left forearm"; - case ESM::PRT_RUpperarm: - return "right upper arm"; - case ESM::PRT_LUpperarm: - return "left upper arm"; - case ESM::PRT_RFoot: - return "right foot"; - case ESM::PRT_LFoot: - return "left foot"; - case ESM::PRT_RAnkle: - return "right ankle"; - case ESM::PRT_LAnkle: - return "left ankle"; - case ESM::PRT_RKnee: - return "right knee"; - case ESM::PRT_LKnee: - return "left knee"; - case ESM::PRT_RLeg: - return "right upper leg"; - case ESM::PRT_LLeg: - return "left upper leg"; - case ESM::PRT_RPauldron: - return "right clavicle"; - case ESM::PRT_LPauldron: - return "left clavicle"; - case ESM::PRT_Weapon: - return "weapon bone"; - case ESM::PRT_Tail: - return "tail"; - default: - throw std::runtime_error("unknown PartReferenceType"); - } - } - - std::string getMeshFilter(ESM::PartReferenceType type) - { - switch(type) - { - case ESM::PRT_Hair: - return "hair"; - default: - return getBoneName(type); - } - } -} diff --git a/components/esm/mappings.hpp b/components/esm/mappings.hpp deleted file mode 100644 index f930fef152..0000000000 --- a/components/esm/mappings.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef OPENMW_ESM_MAPPINGS_H -#define OPENMW_ESM_MAPPINGS_H - -#include - -#include -#include - -namespace ESM -{ - ESM::BodyPart::MeshPart getMeshPart(ESM::PartReferenceType type); - std::string getBoneName(ESM::PartReferenceType type); - std::string getMeshFilter(ESM::PartReferenceType type); -} - -#endif diff --git a/components/esm/player.cpp b/components/esm/player.cpp deleted file mode 100644 index e2e9219e22..0000000000 --- a/components/esm/player.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "player.hpp" - -#include "esmreader.hpp" -#include "esmwriter.hpp" - -void ESM::Player::load (ESMReader &esm) -{ - mObject.mRef.loadId(esm, true); - mObject.load (esm); - - mCellId.load (esm); - - esm.getHNT (mLastKnownExteriorPosition, "LKEP", 12); - - if (esm.isNextSub ("MARK")) - { - mHasMark = true; - esm.getHT (mMarkedPosition, 24); - mMarkedCell.load (esm); - } - else - mHasMark = false; - - // Automove, no longer used. - if (esm.isNextSub("AMOV")) - esm.skipHSub(); - - mBirthsign = esm.getHNString ("SIGN"); - - mCurrentCrimeId = -1; - esm.getHNOT (mCurrentCrimeId, "CURD"); - mPaidCrimeId = -1; - esm.getHNOT (mPaidCrimeId, "PAYD"); - - bool checkPrevItems = true; - while (checkPrevItems) - { - std::string boundItemId = esm.getHNOString("BOUN"); - std::string prevItemId = esm.getHNOString("PREV"); - - if (!boundItemId.empty()) - mPreviousItems[boundItemId] = prevItemId; - else - checkPrevItems = false; - } - - bool intFallback = esm.getFormat() < 11; - if (esm.hasMoreSubs()) - { - for (int i=0; ifirst); - esm.writeHNString ("PREV", it->second); - } - - for (int i=0; i +#include + +#include + +#include "components/esm3/esmreader.hpp" +#include "components/esm4/reader.hpp" + +namespace ESM +{ + Reader* Reader::getReader(const std::string &filename) + { + Files::IStreamPtr esmStream(Files::openConstrainedFileStream(filename)); + + std::uint32_t modVer = 0; // get the first 4 bytes of the record header only + esmStream->read((char*)&modVer, sizeof(modVer)); + if (esmStream->gcount() == sizeof(modVer)) + { + esmStream->seekg(0); + + if (modVer == ESM4::REC_TES4) + { + return new ESM4::Reader(std::move(esmStream), filename); + } + else + { + //return new ESM3::ESMReader(esmStream, filename); + } + } + + throw std::runtime_error("Unknown file format"); + } + + bool Reader::getStringImpl(std::string& str, std::size_t size, + std::istream& stream, const ToUTF8::StatelessUtf8Encoder* encoder, bool hasNull) + { + std::size_t newSize = size; + + if (encoder) + { + std::string input(size, '\0'); + stream.read(input.data(), size); + if (stream.gcount() == static_cast(size)) + { + encoder->getUtf8(input, ToUTF8::BufferAllocationPolicy::FitToRequiredSize, str); + return true; + } + } + else + { + if (hasNull) + newSize -= 1; // don't read the null terminator yet + + str.resize(newSize); // assumed C++11 + stream.read(&str[0], newSize); + if (static_cast(stream.gcount()) == newSize) + { + if (hasNull) + { + char ch; + stream.read(&ch, 1); // read the null terminator + assert (ch == '\0' + && "ESM4::Reader::getString string is not terminated with a null"); + } +#if 0 + else + { + // NOTE: normal ESMs don't but omwsave has locals or spells with null terminator + assert (str[newSize - 1] != '\0' + && "ESM4::Reader::getString string is unexpectedly terminated with a null"); + } +#endif + return true; + } + } + + str.clear(); + return false; // FIXME: throw instead? + } +} diff --git a/components/esm/reader.hpp b/components/esm/reader.hpp new file mode 100644 index 0000000000..a1ddf35641 --- /dev/null +++ b/components/esm/reader.hpp @@ -0,0 +1,59 @@ +#ifndef COMPONENT_ESM_READER_H +#define COMPONENT_ESM_READER_H + +#include + +#include + +#include "common.hpp" // MasterData + +namespace ToUTF8 +{ + class Utf8Encoder; +} + +namespace ESM +{ + class Reader + { + std::vector* mGlobalReaderList; + + public: + virtual ~Reader() {} + + static Reader* getReader(const std::string& filename); + + void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} + std::vector *getGlobalReaderList() {return mGlobalReaderList;} + + virtual inline bool isEsm4() const = 0; + + virtual inline bool hasMoreRecs() const = 0; + + virtual inline void setEncoder(const ToUTF8::StatelessUtf8Encoder* encoder) = 0; + + // used to check for dependencies e.g. CS::Editor::run() + virtual inline const std::vector& getGameFiles() const = 0; + + // used by ContentSelector::ContentModel::addFiles() + virtual inline const std::string getAuthor() const = 0; + virtual inline const std::string getDesc() const = 0; + virtual inline int getFormat() const = 0; + + virtual inline std::string getFileName() const = 0; + + // used by CSMWorld::Data::startLoading() and getTotalRecords() for loading progress bar + virtual inline int getRecordCount() const = 0; + + virtual void setModIndex(std::uint32_t index) = 0; + + // used by CSMWorld::Data::getTotalRecords() + virtual void close() = 0; + + protected: + bool getStringImpl(std::string& str, std::size_t size, + std::istream& stream, const ToUTF8::StatelessUtf8Encoder* encoder, bool hasNull = false); + }; +} + +#endif // COMPONENT_ESM_READER_H diff --git a/components/esm/records.hpp b/components/esm/records.hpp index 5c183b6f6d..50d2f90263 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -2,48 +2,48 @@ #define OPENMW_ESM_RECORDS_H #include "defs.hpp" -#include "loadacti.hpp" -#include "loadalch.hpp" -#include "loadappa.hpp" -#include "loadarmo.hpp" -#include "loadbody.hpp" -#include "loadbook.hpp" -#include "loadbsgn.hpp" -#include "loadcell.hpp" -#include "loadclas.hpp" -#include "loadclot.hpp" -#include "loadcont.hpp" -#include "loadcrea.hpp" -#include "loadinfo.hpp" -#include "loaddial.hpp" -#include "loaddoor.hpp" -#include "loadench.hpp" -#include "loadfact.hpp" -#include "loadglob.hpp" -#include "loadgmst.hpp" -#include "loadingr.hpp" -#include "loadland.hpp" -#include "loadlevlist.hpp" -#include "loadligh.hpp" -#include "loadlock.hpp" -#include "loadrepa.hpp" -#include "loadprob.hpp" -#include "loadltex.hpp" -#include "loadmgef.hpp" -#include "loadmisc.hpp" -#include "loadnpc.hpp" -#include "loadpgrd.hpp" -#include "loadrace.hpp" -#include "loadregn.hpp" -#include "loadscpt.hpp" -#include "loadskil.hpp" -#include "loadsndg.hpp" -#include "loadsoun.hpp" -#include "loadspel.hpp" -#include "loadsscr.hpp" -#include "loadstat.hpp" -#include "loadweap.hpp" +#include "components/esm3/loadacti.hpp" +#include "components/esm3/loadalch.hpp" +#include "components/esm3/loadappa.hpp" +#include "components/esm3/loadarmo.hpp" +#include "components/esm3/loadbody.hpp" +#include "components/esm3/loadbook.hpp" +#include "components/esm3/loadbsgn.hpp" +#include "components/esm3/loadcell.hpp" +#include "components/esm3/loadclas.hpp" +#include "components/esm3/loadclot.hpp" +#include "components/esm3/loadcont.hpp" +#include "components/esm3/loadcrea.hpp" +#include "components/esm3/loadinfo.hpp" +#include "components/esm3/loaddial.hpp" +#include "components/esm3/loaddoor.hpp" +#include "components/esm3/loadench.hpp" +#include "components/esm3/loadfact.hpp" +#include "components/esm3/loadglob.hpp" +#include "components/esm3/loadgmst.hpp" +#include "components/esm3/loadingr.hpp" +#include "components/esm3/loadland.hpp" +#include "components/esm3/loadlevlist.hpp" +#include "components/esm3/loadligh.hpp" +#include "components/esm3/loadlock.hpp" +#include "components/esm3/loadrepa.hpp" +#include "components/esm3/loadprob.hpp" +#include "components/esm3/loadltex.hpp" +#include "components/esm3/loadmgef.hpp" +#include "components/esm3/loadmisc.hpp" +#include "components/esm3/loadnpc.hpp" +#include "components/esm3/loadpgrd.hpp" +#include "components/esm3/loadrace.hpp" +#include "components/esm3/loadregn.hpp" +#include "components/esm3/loadscpt.hpp" +#include "components/esm3/loadskil.hpp" +#include "components/esm3/loadsndg.hpp" +#include "components/esm3/loadsoun.hpp" +#include "components/esm3/loadspel.hpp" +#include "components/esm3/loadsscr.hpp" +#include "components/esm3/loadstat.hpp" +#include "components/esm3/loadweap.hpp" // Special records which are not loaded from ESM -#include "attr.hpp" +#include "components/esm/attr.hpp" #endif diff --git a/components/esm/util.hpp b/components/esm/util.hpp index a80df2456f..204de371fb 100644 --- a/components/esm/util.hpp +++ b/components/esm/util.hpp @@ -13,7 +13,7 @@ struct Quaternion { float mValues[4]; - Quaternion() {} + Quaternion() = default; Quaternion(const osg::Quat& q) { @@ -33,7 +33,7 @@ struct Vector3 { float mValues[3]; - Vector3() {} + Vector3() = default; Vector3(const osg::Vec3f& v) { diff --git a/components/esm/variant.cpp b/components/esm/variant.cpp deleted file mode 100644 index c65eed5e09..0000000000 --- a/components/esm/variant.cpp +++ /dev/null @@ -1,341 +0,0 @@ -#include "variant.hpp" - -#include -#include - -#include "esmreader.hpp" -#include "variantimp.hpp" - -#include "defs.hpp" - -namespace -{ - const uint32_t STRV = ESM::FourCC<'S','T','R','V'>::value; - const uint32_t INTV = ESM::FourCC<'I','N','T','V'>::value; - const uint32_t FLTV = ESM::FourCC<'F','L','T','V'>::value; - const uint32_t STTV = ESM::FourCC<'S','T','T','V'>::value; -} - -ESM::Variant::Variant() : mType (VT_None), mData (0) {} - -ESM::Variant::Variant(const std::string &value) -{ - mData = 0; - mType = VT_None; - setType(VT_String); - setString(value); -} - -ESM::Variant::Variant(int value) -{ - mData = 0; - mType = VT_None; - setType(VT_Long); - setInteger(value); -} - -ESM::Variant::Variant(float value) -{ - mData = 0; - mType = VT_None; - setType(VT_Float); - setFloat(value); -} - -ESM::Variant::~Variant() -{ - delete mData; -} - -ESM::Variant& ESM::Variant::operator= (const Variant& variant) -{ - if (&variant!=this) - { - VariantDataBase *newData = variant.mData ? variant.mData->clone() : 0; - - delete mData; - - mType = variant.mType; - mData = newData; - } - - return *this; -} - -ESM::Variant::Variant (const Variant& variant) -: mType (variant.mType), mData (variant.mData ? variant.mData->clone() : 0) -{} - -ESM::VarType ESM::Variant::getType() const -{ - return mType; -} - -std::string ESM::Variant::getString() const -{ - if (!mData) - throw std::runtime_error ("can not convert empty variant to string"); - - return mData->getString(); -} - -int ESM::Variant::getInteger() const -{ - if (!mData) - throw std::runtime_error ("can not convert empty variant to integer"); - - return mData->getInteger(); -} - -float ESM::Variant::getFloat() const -{ - if (!mData) - throw std::runtime_error ("can not convert empty variant to float"); - - return mData->getFloat(); -} - -void ESM::Variant::read (ESMReader& esm, Format format) -{ - // type - VarType type = VT_Unknown; - - if (format==Format_Global) - { - std::string typeId = esm.getHNString ("FNAM"); - - if (typeId == "s") - type = VT_Short; - else if (typeId == "l") - type = VT_Long; - else if (typeId == "f") - type = VT_Float; - else - esm.fail ("illegal global variable type " + typeId); - } - else if (format==Format_Gmst) - { - if (!esm.hasMoreSubs()) - { - type = VT_None; - } - else - { - esm.getSubName(); - NAME name = esm.retSubName(); - - - - if (name==STRV) - { - type = VT_String; - } - else if (name==INTV) - { - type = VT_Int; - } - else if (name==FLTV) - { - type = VT_Float; - } - else - esm.fail ("invalid subrecord: " + name.toString()); - } - } - else if (format == Format_Info) - { - esm.getSubName(); - NAME name = esm.retSubName(); - - if (name==INTV) - { - type = VT_Int; - } - else if (name==FLTV) - { - type = VT_Float; - } - else - esm.fail ("invalid subrecord: " + name.toString()); - } - else if (format == Format_Local) - { - esm.getSubName(); - NAME name = esm.retSubName(); - - if (name==INTV) - { - type = VT_Int; - } - else if (name==FLTV) - { - type = VT_Float; - } - else if (name==STTV) - { - type = VT_Short; - } - else - esm.fail ("invalid subrecord: " + name.toString()); - } - - setType (type); - - // data - if (mData) - mData->read (esm, format, mType); -} - -void ESM::Variant::write (ESMWriter& esm, Format format) const -{ - if (mType==VT_Unknown) - { - throw std::runtime_error ("can not serialise variant of unknown type"); - } - else if (mType==VT_None) - { - if (format==Format_Global) - throw std::runtime_error ("can not serialise variant of type none to global format"); - - if (format==Format_Info) - throw std::runtime_error ("can not serialise variant of type none to info format"); - - if (format==Format_Local) - throw std::runtime_error ("can not serialise variant of type none to local format"); - - // nothing to do here for GMST format - } - else - mData->write (esm, format, mType); -} - -void ESM::Variant::write (std::ostream& stream) const -{ - switch (mType) - { - case VT_Unknown: - - stream << "variant unknown"; - break; - - case VT_None: - - stream << "variant none"; - break; - - case VT_Short: - - stream << "variant short: " << mData->getInteger(); - break; - - case VT_Int: - - stream << "variant int: " << mData->getInteger(); - break; - - case VT_Long: - - stream << "variant long: " << mData->getInteger(); - break; - - case VT_Float: - - stream << "variant float: " << mData->getFloat(); - break; - - case VT_String: - - stream << "variant string: \"" << mData->getString() << "\""; - break; - } -} - -void ESM::Variant::setType (VarType type) -{ - if (type!=mType) - { - VariantDataBase *newData = 0; - - switch (type) - { - case VT_Unknown: - case VT_None: - - break; // no data - - case VT_Short: - case VT_Int: - case VT_Long: - - newData = new VariantIntegerData (mData); - break; - - case VT_Float: - - newData = new VariantFloatData (mData); - break; - - case VT_String: - - newData = new VariantStringData (mData); - break; - } - - delete mData; - mData = newData; - mType = type; - } -} - -void ESM::Variant::setString (const std::string& value) -{ - if (!mData) - throw std::runtime_error ("can not assign string to empty variant"); - - mData->setString (value); -} - -void ESM::Variant::setInteger (int value) -{ - if (!mData) - throw std::runtime_error ("can not assign integer to empty variant"); - - mData->setInteger (value); -} - -void ESM::Variant::setFloat (float value) -{ - if (!mData) - throw std::runtime_error ("can not assign float to empty variant"); - - mData->setFloat (value); -} - -bool ESM::Variant::isEqual (const Variant& value) const -{ - if (mType!=value.mType) - return false; - - if (!mData) - return true; - - assert (value.mData); - - return mData->isEqual (*value.mData); -} - -std::ostream& ESM::operator<< (std::ostream& stream, const Variant& value) -{ - value.write (stream); - return stream; -} - -bool ESM::operator== (const Variant& left, const Variant& right) -{ - return left.isEqual (right); -} - -bool ESM::operator!= (const Variant& left, const Variant& right) -{ - return !(left==right); -} diff --git a/components/esm/variantimp.cpp b/components/esm/variantimp.cpp deleted file mode 100644 index aeea5017e1..0000000000 --- a/components/esm/variantimp.cpp +++ /dev/null @@ -1,306 +0,0 @@ -#include "variantimp.hpp" - -#include - -#include "esmreader.hpp" -#include "esmwriter.hpp" - -ESM::VariantDataBase::~VariantDataBase() {} - -std::string ESM::VariantDataBase::getString (bool default_) const -{ - if (default_) - return ""; - - throw std::runtime_error ("can not convert variant to string"); -} - -int ESM::VariantDataBase::getInteger (bool default_) const -{ - if (default_) - return 0; - - throw std::runtime_error ("can not convert variant to integer"); -} - -float ESM::VariantDataBase::getFloat (bool default_) const -{ - if (default_) - return 0; - - throw std::runtime_error ("can not convert variant to float"); -} - -void ESM::VariantDataBase::setString (const std::string& value) -{ - throw std::runtime_error ("conversion of string to variant not possible"); -} - -void ESM::VariantDataBase::setInteger (int value) -{ - throw std::runtime_error ("conversion of integer to variant not possible"); -} - -void ESM::VariantDataBase::setFloat (float value) -{ - throw std::runtime_error ("conversion of float to variant not possible"); -} - - - -ESM::VariantStringData::VariantStringData (const VariantDataBase *data) -{ - if (data) - mValue = data->getString (true); -} - -ESM::VariantDataBase *ESM::VariantStringData::clone() const -{ - return new VariantStringData (*this); -} - -std::string ESM::VariantStringData::getString (bool default_) const -{ - return mValue; -} - -void ESM::VariantStringData::setString (const std::string& value) -{ - mValue = value; -} - -void ESM::VariantStringData::read (ESMReader& esm, Variant::Format format, VarType type) -{ - if (type!=VT_String) - throw std::logic_error ("not a string type"); - - if (format==Variant::Format_Global) - esm.fail ("global variables of type string not supported"); - - if (format==Variant::Format_Info) - esm.fail ("info variables of type string not supported"); - - if (format==Variant::Format_Local) - esm.fail ("local variables of type string not supported"); - - // GMST - mValue = esm.getHString(); -} - -void ESM::VariantStringData::write (ESMWriter& esm, Variant::Format format, VarType type) const -{ - if (type!=VT_String) - throw std::logic_error ("not a string type"); - - if (format==Variant::Format_Global) - throw std::runtime_error ("global variables of type string not supported"); - - if (format==Variant::Format_Info) - throw std::runtime_error ("info variables of type string not supported"); - - // GMST - esm.writeHNString ("STRV", mValue); -} - -bool ESM::VariantStringData::isEqual (const VariantDataBase& value) const -{ - return dynamic_cast (value).mValue==mValue; -} - - - -ESM::VariantIntegerData::VariantIntegerData (const VariantDataBase *data) : mValue (0) -{ - if (data) - mValue = data->getInteger (true); -} - -ESM::VariantDataBase *ESM::VariantIntegerData::clone() const -{ - return new VariantIntegerData (*this); -} - -int ESM::VariantIntegerData::getInteger (bool default_) const -{ - return mValue; -} - -float ESM::VariantIntegerData::getFloat (bool default_) const -{ - return static_cast(mValue); -} - -void ESM::VariantIntegerData::setInteger (int value) -{ - mValue = value; -} - -void ESM::VariantIntegerData::setFloat (float value) -{ - mValue = static_cast (value); -} - -void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarType type) -{ - if (type!=VT_Short && type!=VT_Long && type!=VT_Int) - throw std::logic_error ("not an integer type"); - - if (format==Variant::Format_Global) - { - float value; - esm.getHNT (value, "FLTV"); - - if (type==VT_Short) - { - if (value!=value) - mValue = 0; // nan - else - mValue = static_cast (value); - } - else if (type==VT_Long) - mValue = static_cast (value); - else - esm.fail ("unsupported global variable integer type"); - } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info) - { - if (type!=VT_Int) - { - std::ostringstream stream; - stream - << "unsupported " <<(format==Variant::Format_Gmst ? "gmst" : "info") - << " variable integer type"; - esm.fail (stream.str()); - } - - esm.getHT (mValue); - } - else if (format==Variant::Format_Local) - { - if (type==VT_Short) - { - short value; - esm.getHT(value); - mValue = value; - } - else if (type==VT_Int) - { - esm.getHT(mValue); - } - else - esm.fail("unsupported local variable integer type"); - } -} - -void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, VarType type) const -{ - if (type!=VT_Short && type!=VT_Long && type!=VT_Int) - throw std::logic_error ("not an integer type"); - - if (format==Variant::Format_Global) - { - if (type==VT_Short || type==VT_Long) - { - float value = static_cast(mValue); - esm.writeHNString ("FNAM", type==VT_Short ? "s" : "l"); - esm.writeHNT ("FLTV", value); - } - else - throw std::runtime_error ("unsupported global variable integer type"); - } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info) - { - if (type!=VT_Int) - { - std::ostringstream stream; - stream - << "unsupported " <<(format==Variant::Format_Gmst ? "gmst" : "info") - << " variable integer type"; - throw std::runtime_error (stream.str()); - } - - esm.writeHNT ("INTV", mValue); - } - else if (format==Variant::Format_Local) - { - if (type==VT_Short) - esm.writeHNT ("STTV", (short)mValue); - else if (type == VT_Int) - esm.writeHNT ("INTV", mValue); - else - throw std::runtime_error("unsupported local variable integer type"); - } -} - -bool ESM::VariantIntegerData::isEqual (const VariantDataBase& value) const -{ - return dynamic_cast (value).mValue==mValue; -} - - -ESM::VariantFloatData::VariantFloatData (const VariantDataBase *data) : mValue (0) -{ - if (data) - mValue = data->getFloat (true); -} - -ESM::VariantDataBase *ESM::VariantFloatData::clone() const -{ - return new VariantFloatData (*this); -} - -int ESM::VariantFloatData::getInteger (bool default_) const -{ - return static_cast (mValue); -} - -float ESM::VariantFloatData::getFloat (bool default_) const -{ - return mValue; -} - -void ESM::VariantFloatData::setInteger (int value) -{ - mValue = static_cast(value); -} - -void ESM::VariantFloatData::setFloat (float value) -{ - mValue = value; -} - -void ESM::VariantFloatData::read (ESMReader& esm, Variant::Format format, VarType type) -{ - if (type!=VT_Float) - throw std::logic_error ("not a float type"); - - if (format==Variant::Format_Global) - { - esm.getHNT (mValue, "FLTV"); - } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) - { - esm.getHT (mValue); - } -} - -void ESM::VariantFloatData::write (ESMWriter& esm, Variant::Format format, VarType type) const -{ - if (type!=VT_Float) - throw std::logic_error ("not a float type"); - - if (format==Variant::Format_Global) - { - esm.writeHNString ("FNAM", "f"); - esm.writeHNT ("FLTV", mValue); - } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) - { - esm.writeHNT ("FLTV", mValue); - } -} - -bool ESM::VariantFloatData::isEqual (const VariantDataBase& value) const -{ - return dynamic_cast (value).mValue==mValue; -} diff --git a/components/esm/variantimp.hpp b/components/esm/variantimp.hpp deleted file mode 100644 index e7ac722b1e..0000000000 --- a/components/esm/variantimp.hpp +++ /dev/null @@ -1,179 +0,0 @@ -#ifndef OPENMW_ESM_VARIANTIMP_H -#define OPENMW_ESM_VARIANTIMP_H - -#include - -#include "variant.hpp" - -namespace ESM -{ - class VariantDataBase - { - public: - - virtual ~VariantDataBase(); - - virtual VariantDataBase *clone() const = 0; - - virtual std::string getString (bool default_ = false) const; - ///< Will throw an exception, if value can not be represented as a string. - /// - /// \note Numeric values are not converted to strings. - /// - /// \param default_ Return a default value instead of throwing an exception. - /// - /// Default-implementation: throw an exception. - - virtual int getInteger (bool default_ = false) const; - ///< Will throw an exception, if value can not be represented as an integer (implicit - /// casting of float values is permitted). - /// - /// \param default_ Return a default value instead of throwing an exception. - /// - /// Default-implementation: throw an exception. - - virtual float getFloat (bool default_ = false) const; - ///< Will throw an exception, if value can not be represented as a float value. - /// - /// \param default_ Return a default value instead of throwing an exception. - /// - /// Default-implementation: throw an exception. - - virtual void setString (const std::string& value); - ///< Will throw an exception, if type is not compatible with string. - /// - /// Default-implementation: throw an exception. - - virtual void setInteger (int value); - ///< Will throw an exception, if type is not compatible with integer. - /// - /// Default-implementation: throw an exception. - - virtual void setFloat (float value); - ///< Will throw an exception, if type is not compatible with float. - /// - /// Default-implementation: throw an exception. - - virtual void read (ESMReader& esm, Variant::Format format, VarType type) = 0; - ///< If \a type is not supported by \a format, an exception is thrown via ESMReader::fail - - virtual void write (ESMWriter& esm, Variant::Format format, VarType type) const = 0; - ///< If \a type is not supported by \a format, an exception is thrown. - - virtual bool isEqual (const VariantDataBase& value) const = 0; - ///< If the (C++) type of \a value does not match the type of *this, an exception is thrown. - - }; - - class VariantStringData : public VariantDataBase - { - std::string mValue; - - public: - - VariantStringData (const VariantDataBase *data = 0); - ///< Calling the constructor with an incompatible data type will result in a silent - /// default initialisation. - - VariantDataBase *clone() const override; - - std::string getString (bool default_ = false) const override; - ///< Will throw an exception, if value can not be represented as a string. - /// - /// \note Numeric values are not converted to strings. - /// - /// \param default_ Return a default value instead of throwing an exception. - - void setString (const std::string& value) override; - ///< Will throw an exception, if type is not compatible with string. - - void read (ESMReader& esm, Variant::Format format, VarType type) override; - ///< If \a type is not supported by \a format, an exception is thrown via ESMReader::fail - - void write (ESMWriter& esm, Variant::Format format, VarType type) const override; - ///< If \a type is not supported by \a format, an exception is thrown. - - bool isEqual (const VariantDataBase& value) const override; - ///< If the (C++) type of \a value does not match the type of *this, an exception is thrown. - }; - - class VariantIntegerData : public VariantDataBase - { - int mValue; - - public: - - VariantIntegerData (const VariantDataBase *data = 0); - ///< Calling the constructor with an incompatible data type will result in a silent - /// default initialisation. - - VariantDataBase *clone() const override; - - int getInteger (bool default_ = false) const override; - ///< Will throw an exception, if value can not be represented as an integer (implicit - /// casting of float values is permitted). - /// - /// \param default_ Return a default value instead of throwing an exception. - - float getFloat (bool default_ = false) const override; - ///< Will throw an exception, if value can not be represented as a float value. - /// - /// \param default_ Return a default value instead of throwing an exception. - - void setInteger (int value) override; - ///< Will throw an exception, if type is not compatible with integer. - - void setFloat (float value) override; - ///< Will throw an exception, if type is not compatible with float. - - void read (ESMReader& esm, Variant::Format format, VarType type) override; - ///< If \a type is not supported by \a format, an exception is thrown via ESMReader::fail - - void write (ESMWriter& esm, Variant::Format format, VarType type) const override; - ///< If \a type is not supported by \a format, an exception is thrown. - - bool isEqual (const VariantDataBase& value) const override; - ///< If the (C++) type of \a value does not match the type of *this, an exception is thrown. - }; - - class VariantFloatData : public VariantDataBase - { - float mValue; - - public: - - VariantFloatData (const VariantDataBase *data = 0); - ///< Calling the constructor with an incompatible data type will result in a silent - /// default initialisation. - - VariantDataBase *clone() const override; - - int getInteger (bool default_ = false) const override; - ///< Will throw an exception, if value can not be represented as an integer (implicit - /// casting of float values is permitted). - /// - /// \param default_ Return a default value instead of throwing an exception. - - float getFloat (bool default_ = false) const override; - ///< Will throw an exception, if value can not be represented as a float value. - /// - /// \param default_ Return a default value instead of throwing an exception. - - void setInteger (int value) override; - ///< Will throw an exception, if type is not compatible with integer. - - void setFloat (float value) override; - ///< Will throw an exception, if type is not compatible with float. - - void read (ESMReader& esm, Variant::Format format, VarType type) override; - ///< If \a type is not supported by \a format, an exception is thrown via ESMReader::fail - - void write (ESMWriter& esm, Variant::Format format, VarType type) const override; - ///< If \a type is not supported by \a format, an exception is thrown. - - bool isEqual (const VariantDataBase& value) const override; - ///< If the (C++) type of \a value does not match the type of *this, an exception is thrown. - }; -} - -#endif diff --git a/components/esm3/activespells.cpp b/components/esm3/activespells.cpp new file mode 100644 index 0000000000..014c5c2e77 --- /dev/null +++ b/components/esm3/activespells.cpp @@ -0,0 +1,124 @@ +#include "activespells.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +namespace ESM +{ +namespace +{ + void saveImpl(ESMWriter& esm, const std::vector& spells, NAME tag) + { + for (const auto& params : spells) + { + esm.writeHNString (tag, params.mId); + + esm.writeHNT ("CAST", params.mCasterActorId); + esm.writeHNString ("DISP", params.mDisplayName); + esm.writeHNT ("TYPE", params.mType); + if(params.mItem.isSet()) + params.mItem.save(esm, true, "ITEM"); + if(params.mWorsenings >= 0) + { + esm.writeHNT ("WORS", params.mWorsenings); + esm.writeHNT ("TIME", params.mNextWorsening); + } + + for (auto effect : params.mEffects) + { + esm.writeHNT ("MGEF", effect.mEffectId); + if (effect.mArg != -1) + esm.writeHNT ("ARG_", effect.mArg); + esm.writeHNT ("MAGN", effect.mMagnitude); + esm.writeHNT ("MAGN", effect.mMinMagnitude); + esm.writeHNT ("MAGN", effect.mMaxMagnitude); + esm.writeHNT ("DURA", effect.mDuration); + esm.writeHNT ("EIND", effect.mEffectIndex); + esm.writeHNT ("LEFT", effect.mTimeLeft); + esm.writeHNT ("FLAG", effect.mFlags); + } + } + } + + void loadImpl(ESMReader& esm, std::vector& spells, NAME tag) + { + int format = esm.getFormat(); + + while (esm.isNextSub(tag)) + { + ActiveSpells::ActiveSpellParams params; + params.mId = esm.getHString(); + esm.getHNT (params.mCasterActorId, "CAST"); + params.mDisplayName = esm.getHNString ("DISP"); + params.mItem.unset(); + if (format < 17) + params.mType = ActiveSpells::Type_Temporary; + else + { + esm.getHNT (params.mType, "TYPE"); + if(esm.peekNextSub("ITEM")) + params.mItem.load(esm, true, "ITEM"); + } + if(esm.isNextSub("WORS")) + { + esm.getHT(params.mWorsenings); + esm.getHNT(params.mNextWorsening, "TIME"); + } + else + params.mWorsenings = -1; + + // spell casting timestamp, no longer used + if (esm.isNextSub("TIME")) + esm.skipHSub(); + + while (esm.isNextSub("MGEF")) + { + ActiveEffect effect; + esm.getHT(effect.mEffectId); + effect.mArg = -1; + esm.getHNOT(effect.mArg, "ARG_"); + esm.getHNT (effect.mMagnitude, "MAGN"); + if (format < 17) + { + effect.mMinMagnitude = effect.mMagnitude; + effect.mMaxMagnitude = effect.mMagnitude; + } + else + { + esm.getHNT (effect.mMinMagnitude, "MAGN"); + esm.getHNT (effect.mMaxMagnitude, "MAGN"); + } + esm.getHNT (effect.mDuration, "DURA"); + effect.mEffectIndex = -1; + esm.getHNOT (effect.mEffectIndex, "EIND"); + if (format < 9) + effect.mTimeLeft = effect.mDuration; + else + esm.getHNT (effect.mTimeLeft, "LEFT"); + if (format < 17) + effect.mFlags = ActiveEffect::Flag_None; + else + esm.getHNT (effect.mFlags, "FLAG"); + + params.mEffects.push_back(effect); + } + spells.emplace_back(params); + } + } +} +} + +namespace ESM +{ + void ActiveSpells::save(ESMWriter &esm) const + { + saveImpl(esm, mSpells, "ID__"); + saveImpl(esm, mQueue, "QID_"); + } + + void ActiveSpells::load(ESMReader &esm) + { + loadImpl(esm, mSpells, "ID__"); + loadImpl(esm, mQueue, "QID_"); + } +} diff --git a/components/esm/activespells.hpp b/components/esm3/activespells.hpp similarity index 50% rename from components/esm/activespells.hpp rename to components/esm3/activespells.hpp index 1b7f8b319c..91b3f495f5 100644 --- a/components/esm/activespells.hpp +++ b/components/esm3/activespells.hpp @@ -1,11 +1,12 @@ #ifndef OPENMW_ESM_ACTIVESPELLS_H #define OPENMW_ESM_ACTIVESPELLS_H +#include "cellref.hpp" +#include "components/esm/defs.hpp" #include "effectlist.hpp" -#include "defs.hpp" #include -#include +#include namespace ESM { @@ -14,29 +15,55 @@ namespace ESM // Parameters of an effect concerning lasting effects. // Note we are not using ENAMstruct since the magnitude may be modified by magic resistance, etc. - // It could also be a negative magnitude, in case of inversing an effect, e.g. Absorb spell causes damage on target, but heals the caster. struct ActiveEffect { + enum Flags + { + Flag_None = 0, + Flag_Applied = 1 << 0, + Flag_Remove = 1 << 1, + Flag_Ignore_Resistances = 1 << 2, + Flag_Ignore_Reflect = 1 << 3, + Flag_Ignore_SpellAbsorption = 1 << 4 + }; + int mEffectId; float mMagnitude; + float mMinMagnitude; + float mMaxMagnitude; int mArg; // skill or attribute float mDuration; float mTimeLeft; int mEffectIndex; + int mFlags; }; // format 0, saved games only struct ActiveSpells { + enum EffectType + { + Type_Temporary, + Type_Ability, + Type_Enchantment, + Type_Permanent, + Type_Consumable + }; + struct ActiveSpellParams { + std::string mId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; + RefNum mItem; + EffectType mType; + int mWorsenings; + TimeStamp mNextWorsening; }; - typedef std::multimap TContainer; - TContainer mSpells; + std::vector mSpells; + std::vector mQueue; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/aipackage.cpp b/components/esm3/aipackage.cpp similarity index 87% rename from components/esm/aipackage.cpp rename to components/esm3/aipackage.cpp index abbd2c62cd..55ee64db9c 100644 --- a/components/esm/aipackage.cpp +++ b/components/esm3/aipackage.cpp @@ -15,7 +15,12 @@ namespace ESM { AIPackage pack; if (esm.retSubName() == AI_CNDT) { - mList.back().mCellName = esm.getHString(); + if (mList.empty()) + { + esm.fail("AIPackge with an AI_CNDT applying to no cell."); + } else { + mList.back().mCellName = esm.getHString(); + } } else if (esm.retSubName() == AI_Wander) { pack.mType = AI_Wander; esm.getHExact(&pack.mWander, 14); @@ -60,7 +65,7 @@ namespace ESM case AI_Escort: case AI_Follow: { - const char *name = (it->mType == AI_Escort) ? "AI_E" : "AI_F"; + const NAME name = (it->mType == AI_Escort) ? NAME("AI_E") : NAME("AI_F"); esm.writeHNT(name, it->mTarget, sizeof(it->mTarget)); esm.writeHNOCString("CNDT", it->mCellName); break; diff --git a/components/esm/aipackage.hpp b/components/esm3/aipackage.hpp similarity index 90% rename from components/esm/aipackage.hpp rename to components/esm3/aipackage.hpp index 026e65dd82..90e1d1cf9e 100644 --- a/components/esm/aipackage.hpp +++ b/components/esm3/aipackage.hpp @@ -4,7 +4,7 @@ #include #include -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -37,7 +37,8 @@ namespace ESM struct AITravel { float mX, mY, mZ; - int mUnk; + unsigned char mShouldRepeat; + unsigned char mPadding[3]; }; struct AITarget @@ -45,13 +46,14 @@ namespace ESM float mX, mY, mZ; short mDuration; NAME32 mId; - short mUnk; + unsigned char mShouldRepeat; + unsigned char mPadding; }; struct AIActivate { NAME32 mName; - unsigned char mUnk; + unsigned char mShouldRepeat; }; #pragma pack(pop) diff --git a/components/esm/aisequence.cpp b/components/esm3/aisequence.cpp similarity index 57% rename from components/esm/aisequence.cpp rename to components/esm3/aisequence.cpp index df3c1ca9d0..a9670a598d 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm3/aisequence.cpp @@ -3,6 +3,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include #include namespace ESM @@ -34,12 +35,16 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); esm.getHNOT (mHidden, "HIDD"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); } void AiTravel::save(ESMWriter &esm) const { esm.writeHNT ("DATA", mData); esm.writeHNT ("HIDD", mHidden); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiEscort::load(ESMReader &esm) @@ -50,6 +55,15 @@ namespace AiSequence esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); + if(esm.getFormat() < 18) + { + // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. + // The exact value of mDuration only matters for repeating packages. + // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. + mData.mDuration = std::max(mRemainingDuration > 0, mRemainingDuration); + } } void AiEscort::save(ESMWriter &esm) const @@ -60,6 +74,8 @@ namespace AiSequence esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiFollow::load(ESMReader &esm) @@ -75,6 +91,15 @@ namespace AiSequence esm.getHNOT (mCommanded, "CMND"); mActive = false; esm.getHNOT (mActive, "ACTV"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); + if(esm.getFormat() < 18) + { + // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. + // The exact value of mDuration only matters for repeating packages. + // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. + mData.mDuration = std::max(mRemainingDuration > 0, mRemainingDuration); + } } void AiFollow::save(ESMWriter &esm) const @@ -89,16 +114,22 @@ namespace AiSequence esm.writeHNT ("CMND", mCommanded); if (mActive) esm.writeHNT("ACTV", mActive); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiActivate::load(ESMReader &esm) { mTargetId = esm.getHNString("TARG"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); } void AiActivate::save(ESMWriter &esm) const { esm.writeHNString("TARG", mTargetId); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiCombat::load(ESMReader &esm) @@ -121,12 +152,6 @@ namespace AiSequence esm.writeHNT ("TARG", mTargetActorId); } - AiSequence::~AiSequence() - { - for (std::vector::iterator it = mPackages.begin(); it != mPackages.end(); ++it) - delete it->mPackage; - } - void AiSequence::save(ESMWriter &esm) const { for (std::vector::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) @@ -135,25 +160,25 @@ namespace AiSequence switch (it->mType) { case Ai_Wander: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Travel: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Escort: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Follow: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Activate: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Combat: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; case Ai_Pursue: - static_cast(it->mPackage)->save(esm); + static_cast(*it->mPackage).save(esm); break; default: @@ -166,6 +191,7 @@ namespace AiSequence void AiSequence::load(ESMReader &esm) { + int count = 0; while (esm.isNextSub("AIPK")) { int type; @@ -178,51 +204,56 @@ namespace AiSequence { case Ai_Wander: { - std::unique_ptr ptr (new AiWander()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); + ++count; break; } case Ai_Travel: { - std::unique_ptr ptr (new AiTravel()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); + ++count; break; } case Ai_Escort: { - std::unique_ptr ptr (new AiEscort()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); + ++count; break; } case Ai_Follow: { - std::unique_ptr ptr (new AiFollow()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); + ++count; break; } case Ai_Activate: { - std::unique_ptr ptr (new AiActivate()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); + ++count; break; } case Ai_Combat: { - std::unique_ptr ptr (new AiCombat()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); break; } case Ai_Pursue: { - std::unique_ptr ptr (new AiPursue()); + std::unique_ptr ptr = std::make_unique(); ptr->load(esm); - mPackages.back().mPackage = ptr.release(); + mPackages.back().mPackage = std::move(ptr); break; } default: @@ -231,6 +262,23 @@ namespace AiSequence } esm.getHNOT (mLastAiPackage, "LAST"); + + if(count > 1 && esm.getFormat() < 18) + { + for(auto& pkg : mPackages) + { + if(pkg.mType == Ai_Wander) + static_cast(*pkg.mPackage).mData.mShouldRepeat = true; + else if(pkg.mType == Ai_Travel) + static_cast(*pkg.mPackage).mRepeat = true; + else if(pkg.mType == Ai_Escort) + static_cast(*pkg.mPackage).mRepeat = true; + else if(pkg.mType == Ai_Follow) + static_cast(*pkg.mPackage).mRepeat = true; + else if(pkg.mType == Ai_Activate) + static_cast(*pkg.mPackage).mRepeat = true; + } + } } } } diff --git a/components/esm/aisequence.hpp b/components/esm3/aisequence.hpp similarity index 82% rename from components/esm/aisequence.hpp rename to components/esm3/aisequence.hpp index d8c20185f8..f71771a4d5 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm3/aisequence.hpp @@ -3,10 +3,11 @@ #include #include +#include -#include "defs.hpp" +#include "components/esm/defs.hpp" -#include "util.hpp" +#include "components/esm/util.hpp" namespace ESM { @@ -21,13 +22,13 @@ namespace ESM enum AiPackages { - Ai_Wander = ESM::FourCC<'W','A','N','D'>::value, - Ai_Travel = ESM::FourCC<'T','R','A','V'>::value, - Ai_Escort = ESM::FourCC<'E','S','C','O'>::value, - Ai_Follow = ESM::FourCC<'F','O','L','L'>::value, - Ai_Activate = ESM::FourCC<'A','C','T','I'>::value, - Ai_Combat = ESM::FourCC<'C','O','M','B'>::value, - Ai_Pursue = ESM::FourCC<'P','U','R','S'>::value + Ai_Wander = fourCC("WAND"), + Ai_Travel = fourCC("TRAV"), + Ai_Escort = fourCC("ESCO"), + Ai_Follow = fourCC("FOLL"), + Ai_Activate = fourCC("ACTI"), + Ai_Combat = fourCC("COMB"), + Ai_Pursue = fourCC("PURS") }; @@ -66,10 +67,10 @@ namespace ESM struct AiWander : AiPackage { AiWanderData mData; - AiWanderDuration mDurationData; // was ESM::TimeStamp mStartTime + AiWanderDuration mDurationData; // was TimeStamp mStartTime bool mStoredInitialActorPosition; - ESM::Vector3 mInitialActorPosition; + Vector3 mInitialActorPosition; /// \todo add more AiWander state @@ -81,6 +82,7 @@ namespace ESM { AiTravelData mData; bool mHidden; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -94,6 +96,7 @@ namespace ESM std::string mTargetId; std::string mCellId; float mRemainingDuration; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -112,6 +115,7 @@ namespace ESM bool mCommanded; bool mActive; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -120,6 +124,7 @@ namespace ESM struct AiActivate : AiPackage { std::string mTargetId; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -145,7 +150,7 @@ namespace ESM { int mType; - AiPackage* mPackage; + std::unique_ptr mPackage; }; struct AiSequence @@ -154,7 +159,6 @@ namespace ESM { mLastAiPackage = -1; } - ~AiSequence(); std::vector mPackages; int mLastAiPackage; diff --git a/components/esm/animationstate.cpp b/components/esm3/animationstate.cpp similarity index 100% rename from components/esm/animationstate.cpp rename to components/esm3/animationstate.cpp diff --git a/components/esm/animationstate.hpp b/components/esm3/animationstate.hpp similarity index 100% rename from components/esm/animationstate.hpp rename to components/esm3/animationstate.hpp diff --git a/components/esm/cellid.cpp b/components/esm3/cellid.cpp similarity index 73% rename from components/esm/cellid.cpp rename to components/esm3/cellid.cpp index ad91d30e04..154ef53056 100644 --- a/components/esm/cellid.cpp +++ b/components/esm3/cellid.cpp @@ -3,22 +3,25 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -const std::string ESM::CellId::sDefaultWorldspace = "sys::default"; +namespace ESM +{ + +const std::string CellId::sDefaultWorldspace = "sys::default"; -void ESM::CellId::load (ESMReader &esm) +void CellId::load (ESMReader &esm) { mWorldspace = esm.getHNString ("SPAC"); if (esm.isNextSub ("CIDX")) { - esm.getHT (mIndex, 8); + esm.getHTSized<8>(mIndex); mPaged = true; } else mPaged = false; } -void ESM::CellId::save (ESMWriter &esm) const +void CellId::save (ESMWriter &esm) const { esm.writeHNString ("SPAC", mWorldspace); @@ -26,18 +29,18 @@ void ESM::CellId::save (ESMWriter &esm) const esm.writeHNT ("CIDX", mIndex, 8); } -bool ESM::operator== (const CellId& left, const CellId& right) +bool operator== (const CellId& left, const CellId& right) { return left.mWorldspace==right.mWorldspace && left.mPaged==right.mPaged && (!left.mPaged || (left.mIndex.mX==right.mIndex.mX && left.mIndex.mY==right.mIndex.mY)); } -bool ESM::operator!= (const CellId& left, const CellId& right) +bool operator!= (const CellId& left, const CellId& right) { return !(left==right); } -bool ESM::operator < (const CellId& left, const CellId& right) +bool operator < (const CellId& left, const CellId& right) { if (left.mPaged < right.mPaged) return true; @@ -59,3 +62,5 @@ bool ESM::operator < (const CellId& left, const CellId& right) return left.mWorldspace < right.mWorldspace; } + +} diff --git a/components/esm/cellid.hpp b/components/esm3/cellid.hpp similarity index 100% rename from components/esm/cellid.hpp rename to components/esm3/cellid.hpp diff --git a/components/esm3/cellref.cpp b/components/esm3/cellref.cpp new file mode 100644 index 0000000000..770680b8e2 --- /dev/null +++ b/components/esm3/cellref.cpp @@ -0,0 +1,286 @@ +#include "cellref.hpp" + +#include + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +namespace ESM +{ + namespace + { + template + void loadIdImpl(ESMReader& esm, bool wideRefNum, CellRef& cellRef) + { + // According to Hrnchamd, this does not belong to the actual ref. Instead, it is a marker indicating that + // the following refs are part of a "temp refs" section. A temp ref is not being tracked by the moved references system. + // Its only purpose is a performance optimization for "immovable" things. We don't need this, and it's problematic anyway, + // because any item can theoretically be moved by a script. + if (esm.isNextSub("NAM0")) + esm.skipHSub(); + + if constexpr (load) + { + cellRef.blank(); + cellRef.mRefNum.load (esm, wideRefNum); + cellRef.mRefID = esm.getHNOString("NAME"); + + if (cellRef.mRefID.empty()) + Log(Debug::Warning) << "Warning: got CellRef with empty RefId in " << esm.getName() << " 0x" << std::hex << esm.getFileOffset(); + } + else + { + RefNum {}.load(esm, wideRefNum); + esm.skipHNOString("NAME"); + } + } + + template + void loadDataImpl(ESMReader &esm, bool &isDeleted, CellRef& cellRef) + { + const auto getHStringOrSkip = [&] (std::string& value) + { + if constexpr (load) + value = esm.getHString(); + else + esm.skipHString(); + }; + + const auto getHTOrSkip = [&] (auto& value) + { + if constexpr (load) + esm.getHT(value); + else + esm.skipHT>(); + }; + + if constexpr (load) + isDeleted = false; + + bool isLoaded = false; + while (!isLoaded && esm.hasMoreSubs()) + { + esm.getSubName(); + switch (esm.retSubName().toInt()) + { + case fourCC("UNAM"): + getHTOrSkip(cellRef.mReferenceBlocked); + break; + case fourCC("XSCL"): + getHTOrSkip(cellRef.mScale); + if constexpr (load) + cellRef.mScale = std::clamp(cellRef.mScale, 0.5f, 2.0f); + break; + case fourCC("ANAM"): + getHStringOrSkip(cellRef.mOwner); + break; + case fourCC("BNAM"): + getHStringOrSkip(cellRef.mGlobalVariable); + break; + case fourCC("XSOL"): + getHStringOrSkip(cellRef.mSoul); + break; + case fourCC("CNAM"): + getHStringOrSkip(cellRef.mFaction); + break; + case fourCC("INDX"): + getHTOrSkip(cellRef.mFactionRank); + break; + case fourCC("XCHG"): + getHTOrSkip(cellRef.mEnchantmentCharge); + break; + case fourCC("INTV"): + getHTOrSkip(cellRef.mChargeInt); + break; + case fourCC("NAM9"): + getHTOrSkip(cellRef.mGoldValue); + break; + case fourCC("DODT"): + getHTOrSkip(cellRef.mDoorDest); + if constexpr (load) + cellRef.mTeleport = true; + break; + case fourCC("DNAM"): + getHStringOrSkip(cellRef.mDestCell); + break; + case fourCC("FLTV"): + getHTOrSkip(cellRef.mLockLevel); + break; + case fourCC("KNAM"): + getHStringOrSkip(cellRef.mKey); + break; + case fourCC("TNAM"): + getHStringOrSkip(cellRef.mTrap); + break; + case fourCC("DATA"): + if constexpr (load) + esm.getHTSized<24>(cellRef.mPos); + else + esm.skipHTSized<24, decltype(cellRef.mPos)>(); + break; + case fourCC("NAM0"): + { + esm.skipHSub(); + break; + } + case SREC_DELE: + esm.skipHSub(); + if constexpr (load) + isDeleted = true; + break; + default: + esm.cacheSubName(); + isLoaded = true; + break; + } + } + + if constexpr (load) + { + if (cellRef.mLockLevel == 0 && !cellRef.mKey.empty()) + { + cellRef.mLockLevel = UnbreakableLock; + cellRef.mTrap.clear(); + } + } + } + } + +void RefNum::load(ESMReader& esm, bool wide, NAME tag) +{ + if (wide) + esm.getHNTSized<8>(*this, tag); + else + esm.getHNT(mIndex, tag); +} + +void RefNum::save(ESMWriter &esm, bool wide, NAME tag) const +{ + if (wide) + esm.writeHNT (tag, *this, 8); + else + { + if (isSet() && !hasContentFile()) + Log(Debug::Error) << "Generated RefNum can not be saved in 32bit format"; + int refNum = (mIndex & 0xffffff) | ((hasContentFile() ? mContentFile : 0xff)<<24); + esm.writeHNT (tag, refNum, 4); + } +} + +void CellRef::load (ESMReader& esm, bool &isDeleted, bool wideRefNum) +{ + loadId(esm, wideRefNum); + loadData(esm, isDeleted); +} + +void CellRef::loadId (ESMReader& esm, bool wideRefNum) +{ + loadIdImpl(esm, wideRefNum, *this); +} + +void CellRef::loadData(ESMReader &esm, bool &isDeleted) +{ + loadDataImpl(esm, isDeleted, *this); +} + +void CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory, bool isDeleted) const +{ + mRefNum.save (esm, wideRefNum); + + esm.writeHNCString("NAME", mRefID); + + if (isDeleted) { + esm.writeHNString("DELE", "", 3); + return; + } + + if (mScale != 1.0) { + esm.writeHNT("XSCL", std::clamp(mScale, 0.5f, 2.0f)); + } + + if (!inInventory) + esm.writeHNOCString("ANAM", mOwner); + + esm.writeHNOCString("BNAM", mGlobalVariable); + esm.writeHNOCString("XSOL", mSoul); + + if (!inInventory) + { + esm.writeHNOCString("CNAM", mFaction); + if (mFactionRank != -2) + { + esm.writeHNT("INDX", mFactionRank); + } + } + + if (mEnchantmentCharge != -1) + esm.writeHNT("XCHG", mEnchantmentCharge); + + if (mChargeInt != -1) + esm.writeHNT("INTV", mChargeInt); + + if (mGoldValue > 1) + esm.writeHNT("NAM9", mGoldValue); + + if (!inInventory && mTeleport) + { + esm.writeHNT("DODT", mDoorDest); + esm.writeHNOCString("DNAM", mDestCell); + } + + if (!inInventory && mLockLevel != 0) { + esm.writeHNT("FLTV", mLockLevel); + } + + if (!inInventory) + { + esm.writeHNOCString ("KNAM", mKey); + esm.writeHNOCString ("TNAM", mTrap); + } + + if (mReferenceBlocked != -1) + esm.writeHNT("UNAM", mReferenceBlocked); + + if (!inInventory) + esm.writeHNT("DATA", mPos, 24); +} + +void CellRef::blank() +{ + mRefNum.unset(); + mRefID.clear(); + mScale = 1; + mOwner.clear(); + mGlobalVariable.clear(); + mSoul.clear(); + mFaction.clear(); + mFactionRank = -2; + mChargeInt = -1; + mChargeIntRemainder = 0.0f; + mEnchantmentCharge = -1; + mGoldValue = 1; + mDestCell.clear(); + mLockLevel = 0; + mKey.clear(); + mTrap.clear(); + mReferenceBlocked = -1; + mTeleport = false; + + for (int i=0; i<3; ++i) + { + mDoorDest.pos[i] = 0; + mDoorDest.rot[i] = 0; + mPos.pos[i] = 0; + mPos.rot[i] = 0; + } +} + +void skipLoadCellRef(ESMReader& esm, bool wideRefNum) +{ + CellRef cellRef; + loadIdImpl(esm, wideRefNum, cellRef); + bool isDeleted; + loadDataImpl(esm, isDeleted, cellRef); +} + +} diff --git a/components/esm/cellref.hpp b/components/esm3/cellref.hpp similarity index 90% rename from components/esm/cellref.hpp rename to components/esm3/cellref.hpp index 5bb7fbc531..f710167afc 100644 --- a/components/esm/cellref.hpp +++ b/components/esm3/cellref.hpp @@ -4,7 +4,8 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -18,13 +19,14 @@ namespace ESM unsigned int mIndex; int mContentFile; - void load (ESMReader& esm, bool wide = false, const std::string& tag = "FRMR"); + void load(ESMReader& esm, bool wide = false, NAME tag = "FRMR"); - void save (ESMWriter &esm, bool wide = false, const std::string& tag = "FRMR") const; + void save(ESMWriter &esm, bool wide = false, NAME tag = "FRMR") const; - enum { RefNum_NoContentFile = -1 }; - inline bool hasContentFile() const { return mContentFile != RefNum_NoContentFile; } - inline void unset() { mIndex = 0; mContentFile = RefNum_NoContentFile; } + inline bool hasContentFile() const { return mContentFile >= 0; } + + inline bool isSet() const { return mIndex != 0 || mContentFile != -1; } + inline void unset() { *this = {0, -1}; } }; /* Cell reference. This represents ONE object (of many) inside the @@ -114,6 +116,8 @@ namespace ESM void blank(); }; + void skipLoadCellRef(ESMReader& esm, bool wideRefNum = false); + inline bool operator== (const RefNum& left, const RefNum& right) { return left.mIndex==right.mIndex && left.mContentFile==right.mContentFile; diff --git a/components/esm/cellstate.cpp b/components/esm3/cellstate.cpp similarity index 81% rename from components/esm/cellstate.cpp rename to components/esm3/cellstate.cpp index 83b130dcd9..d5773d5f26 100644 --- a/components/esm/cellstate.cpp +++ b/components/esm3/cellstate.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::CellState::load (ESMReader &esm) +namespace ESM +{ + +void CellState::load (ESMReader &esm) { mWaterLevel = 0; esm.getHNOT (mWaterLevel, "WLVL"); @@ -16,7 +19,7 @@ void ESM::CellState::load (ESMReader &esm) esm.getHNOT (mLastRespawn, "RESP"); } -void ESM::CellState::save (ESMWriter &esm) const +void CellState::save (ESMWriter &esm) const { if (!mId.mPaged) esm.writeHNT ("WLVL", mWaterLevel); @@ -25,3 +28,5 @@ void ESM::CellState::save (ESMWriter &esm) const esm.writeHNT ("RESP", mLastRespawn); } + +} diff --git a/components/esm/cellstate.hpp b/components/esm3/cellstate.hpp similarity index 87% rename from components/esm/cellstate.hpp rename to components/esm3/cellstate.hpp index 55c1e51550..bf332ae5e8 100644 --- a/components/esm/cellstate.hpp +++ b/components/esm3/cellstate.hpp @@ -3,7 +3,7 @@ #include "cellid.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -21,7 +21,7 @@ namespace ESM int mHasFogOfWar; // Do we have fog of war state (0 or 1)? (see fogstate.hpp) - ESM::TimeStamp mLastRespawn; + TimeStamp mLastRespawn; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/containerstate.cpp b/components/esm3/containerstate.cpp similarity index 56% rename from components/esm/containerstate.cpp rename to components/esm3/containerstate.cpp index 301549d597..b54736cb8b 100644 --- a/components/esm/containerstate.cpp +++ b/components/esm3/containerstate.cpp @@ -1,15 +1,20 @@ #include "containerstate.hpp" -void ESM::ContainerState::load (ESMReader &esm) +namespace ESM +{ + +void ContainerState::load (ESMReader &esm) { ObjectState::load (esm); mInventory.load (esm); } -void ESM::ContainerState::save (ESMWriter &esm, bool inInventory) const +void ContainerState::save (ESMWriter &esm, bool inInventory) const { ObjectState::save (esm, inInventory); mInventory.save (esm); } + +} diff --git a/components/esm/containerstate.hpp b/components/esm3/containerstate.hpp similarity index 100% rename from components/esm/containerstate.hpp rename to components/esm3/containerstate.hpp diff --git a/components/esm/controlsstate.cpp b/components/esm3/controlsstate.cpp similarity index 89% rename from components/esm/controlsstate.cpp rename to components/esm3/controlsstate.cpp index ae4e1dff16..c985c23f15 100644 --- a/components/esm/controlsstate.cpp +++ b/components/esm3/controlsstate.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -ESM::ControlsState::ControlsState() +namespace ESM +{ + +ControlsState::ControlsState() : mViewSwitchDisabled(false), mControlsDisabled(false), mJumpingDisabled(false), @@ -14,7 +17,7 @@ ESM::ControlsState::ControlsState() { } -void ESM::ControlsState::load(ESM::ESMReader& esm) +void ControlsState::load(ESMReader& esm) { int flags; esm.getHNT(flags, "CFLG"); @@ -28,7 +31,7 @@ void ESM::ControlsState::load(ESM::ESMReader& esm) mSpellDrawingDisabled = flags & SpellDrawingDisabled; } -void ESM::ControlsState::save(ESM::ESMWriter& esm) const +void ControlsState::save(ESMWriter& esm) const { int flags = 0; if (mViewSwitchDisabled) flags |= ViewSwitchDisabled; @@ -41,3 +44,5 @@ void ESM::ControlsState::save(ESM::ESMWriter& esm) const esm.writeHNT("CFLG", flags); } + +} diff --git a/components/esm/controlsstate.hpp b/components/esm3/controlsstate.hpp similarity index 100% rename from components/esm/controlsstate.hpp rename to components/esm3/controlsstate.hpp diff --git a/components/esm/creaturelevliststate.cpp b/components/esm3/creaturelevliststate.cpp similarity index 100% rename from components/esm/creaturelevliststate.cpp rename to components/esm3/creaturelevliststate.cpp diff --git a/components/esm/creaturelevliststate.hpp b/components/esm3/creaturelevliststate.hpp similarity index 100% rename from components/esm/creaturelevliststate.hpp rename to components/esm3/creaturelevliststate.hpp diff --git a/components/esm/creaturestate.cpp b/components/esm3/creaturestate.cpp similarity index 70% rename from components/esm/creaturestate.cpp rename to components/esm3/creaturestate.cpp index bffa4e5e45..e9a9f52cf1 100644 --- a/components/esm/creaturestate.cpp +++ b/components/esm3/creaturestate.cpp @@ -1,6 +1,9 @@ #include "creaturestate.hpp" -void ESM::CreatureState::load (ESMReader &esm) +namespace ESM +{ + +void CreatureState::load (ESMReader &esm) { ObjectState::load (esm); @@ -12,7 +15,7 @@ void ESM::CreatureState::load (ESMReader &esm) } } -void ESM::CreatureState::save (ESMWriter &esm, bool inInventory) const +void CreatureState::save (ESMWriter &esm, bool inInventory) const { ObjectState::save (esm, inInventory); @@ -24,8 +27,10 @@ void ESM::CreatureState::save (ESMWriter &esm, bool inInventory) const } } -void ESM::CreatureState::blank() +void CreatureState::blank() { ObjectState::blank(); mCreatureStats.blank(); } + +} diff --git a/components/esm/creaturestate.hpp b/components/esm3/creaturestate.hpp similarity index 100% rename from components/esm/creaturestate.hpp rename to components/esm3/creaturestate.hpp diff --git a/components/esm/creaturestats.cpp b/components/esm3/creaturestats.cpp similarity index 81% rename from components/esm/creaturestats.cpp rename to components/esm3/creaturestats.cpp index cb383992c6..1a1eabd2ab 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm3/creaturestats.cpp @@ -2,7 +2,12 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::CreatureStats::load (ESMReader &esm) +#include + +namespace ESM +{ + +void CreatureStats::load (ESMReader &esm) { bool intFallback = esm.getFormat() < 11; for (int i=0; i<8; ++i) @@ -110,16 +115,31 @@ void ESM::CreatureStats::load (ESMReader &esm) mAiSequence.load(esm); mMagicEffects.load(esm); - while (esm.isNextSub("SUMM")) + if (esm.getFormat() < 17) { - int magicEffect; - esm.getHT(magicEffect); - std::string source = esm.getHNOString("SOUR"); - int effectIndex = -1; - esm.getHNOT (effectIndex, "EIND"); - int actorId; - esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + std::string source = esm.getHNOString("SOUR"); + int effectIndex = -1; + esm.getHNOT (effectIndex, "EIND"); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + mSummonedCreatures.emplace(magicEffect, actorId); + } + } + else + { + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatures.emplace(magicEffect, actorId); + } } while (esm.isNextSub("GRAV")) @@ -148,9 +168,16 @@ void ESM::CreatureStats::load (ESMReader &esm) mCorprusSpells[id] = stats; } + if(esm.getFormat() <= 18) + mMissingACDT = mGoldPool == std::numeric_limits::min(); + else + { + mMissingACDT = false; + esm.getHNOT(mMissingACDT, "NOAC"); + } } -void ESM::CreatureStats::save (ESMWriter &esm) const +void CreatureStats::save (ESMWriter &esm) const { for (int i=0; i<8; ++i) mAttributes[i].save (esm); @@ -159,7 +186,7 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mDynamic[i].save (esm); if (mGoldPool) - esm.writeHNT ("GOLD", mGoldPool); + esm.writeHNT("GOLD", mGoldPool); if (mTradeTime.mDay != 0 || mTradeTime.mHour != 0) esm.writeHNT ("TIME", mTradeTime); @@ -214,14 +241,10 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSequence.save(esm); mMagicEffects.save(esm); - for (const auto& summon : mSummonedCreatureMap) + for (const auto& [effectId, actorId] : mSummonedCreatures) { - esm.writeHNT ("SUMM", summon.first.mEffectId); - esm.writeHNString ("SOUR", summon.first.mSourceId); - int effectIndex = summon.first.mEffectIndex; - if (effectIndex != -1) - esm.writeHNT ("EIND", effectIndex); - esm.writeHNT ("ACID", summon.second); + esm.writeHNT ("SUMM", effectId); + esm.writeHNT ("ACID", actorId); } for (int key : mSummonGraveyard) @@ -235,18 +258,11 @@ void ESM::CreatureStats::save (ESMWriter &esm) const for (int i=0; i<4; ++i) mAiSettings[i].save(esm); } - - for (const auto& corprusSpell : mCorprusSpells) - { - esm.writeHNString("CORP", corprusSpell.first); - - const CorprusStats & stats = corprusSpell.second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } + if (mMissingACDT) + esm.writeHNT("NOAC", mMissingACDT); } -void ESM::CreatureStats::blank() +void CreatureStats::blank() { mTradeTime.mHour = 0; mTradeTime.mDay = 0; @@ -272,4 +288,7 @@ void ESM::CreatureStats::blank() mDeathAnimation = -1; mLevel = 1; mCorprusSpells.clear(); + mMissingACDT = false; +} + } diff --git a/components/esm/creaturestats.hpp b/components/esm3/creaturestats.hpp similarity index 92% rename from components/esm/creaturestats.hpp rename to components/esm3/creaturestats.hpp index 13bc50008c..7ad168b12f 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm3/creaturestats.hpp @@ -7,9 +7,9 @@ #include "statstate.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" -#include "attr.hpp" +#include "components/esm/attr.hpp" #include "spellstate.hpp" #include "activespells.hpp" #include "magiceffects.hpp" @@ -40,9 +40,10 @@ namespace ESM StatState mAiSettings[4]; std::map mSummonedCreatureMap; + std::multimap mSummonedCreatures; std::vector mSummonGraveyard; - ESM::TimeStamp mTradeTime; + TimeStamp mTradeTime; int mGoldPool; int mActorId; //int mHitAttemptActorId; @@ -82,8 +83,9 @@ namespace ESM bool mRecalcDynamicStats; int mDrawState; signed char mDeathAnimation; - ESM::TimeStamp mTimeOfDeath; + TimeStamp mTimeOfDeath; int mLevel; + bool mMissingACDT; std::map mCorprusSpells; SpellState mSpells; diff --git a/components/esm/custommarkerstate.cpp b/components/esm3/custommarkerstate.cpp similarity index 80% rename from components/esm/custommarkerstate.cpp rename to components/esm3/custommarkerstate.cpp index dc81c123d4..7752cc146f 100644 --- a/components/esm/custommarkerstate.cpp +++ b/components/esm3/custommarkerstate.cpp @@ -6,7 +6,7 @@ namespace ESM { -void CustomMarker::save(ESM::ESMWriter &esm) const +void CustomMarker::save(ESMWriter &esm) const { esm.writeHNT("POSX", mWorldX); esm.writeHNT("POSY", mWorldY); @@ -15,7 +15,7 @@ void CustomMarker::save(ESM::ESMWriter &esm) const esm.writeHNString("NOTE", mNote); } -void CustomMarker::load(ESM::ESMReader &esm) +void CustomMarker::load(ESMReader &esm) { esm.getHNT(mWorldX, "POSX"); esm.getHNT(mWorldY, "POSY"); diff --git a/components/esm/custommarkerstate.hpp b/components/esm3/custommarkerstate.hpp similarity index 79% rename from components/esm/custommarkerstate.hpp rename to components/esm3/custommarkerstate.hpp index 2be43c53bf..0b527c0a92 100644 --- a/components/esm/custommarkerstate.hpp +++ b/components/esm3/custommarkerstate.hpp @@ -12,7 +12,7 @@ struct CustomMarker float mWorldX; float mWorldY; - ESM::CellId mCell; + CellId mCell; std::string mNote; @@ -21,8 +21,8 @@ struct CustomMarker return mNote == other.mNote && mCell == other.mCell && mWorldX == other.mWorldX && mWorldY == other.mWorldY; } - void load (ESM::ESMReader& reader); - void save (ESM::ESMWriter& writer) const; + void load (ESMReader& reader); + void save (ESMWriter& writer) const; }; } diff --git a/components/esm/debugprofile.cpp b/components/esm3/debugprofile.cpp similarity index 63% rename from components/esm/debugprofile.cpp rename to components/esm3/debugprofile.cpp index 17249d8a3b..69f33d5f2d 100644 --- a/components/esm/debugprofile.cpp +++ b/components/esm3/debugprofile.cpp @@ -2,32 +2,33 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" -unsigned int ESM::DebugProfile::sRecordId = REC_DBGP; +namespace ESM +{ -void ESM::DebugProfile::load (ESMReader& esm, bool &isDeleted) +void DebugProfile::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; - case ESM::FourCC<'S','C','R','P'>::value: + case fourCC("SCRP"): mScriptText = esm.getHString(); break; - case ESM::FourCC<'F','L','A','G'>::value: + case fourCC("FLAG"): esm.getHT(mFlags); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -38,13 +39,13 @@ void ESM::DebugProfile::load (ESMReader& esm, bool &isDeleted) } } -void ESM::DebugProfile::save (ESMWriter& esm, bool isDeleted) const +void DebugProfile::save (ESMWriter& esm, bool isDeleted) const { esm.writeHNCString ("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -53,9 +54,11 @@ void ESM::DebugProfile::save (ESMWriter& esm, bool isDeleted) const esm.writeHNT ("FLAG", mFlags); } -void ESM::DebugProfile::blank() +void DebugProfile::blank() { mDescription.clear(); mScriptText.clear(); mFlags = 0; } + +} diff --git a/components/esm/debugprofile.hpp b/components/esm3/debugprofile.hpp similarity index 86% rename from components/esm/debugprofile.hpp rename to components/esm3/debugprofile.hpp index c056750a88..e6fcbcd7f0 100644 --- a/components/esm/debugprofile.hpp +++ b/components/esm3/debugprofile.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { class ESMReader; @@ -10,7 +12,8 @@ namespace ESM struct DebugProfile { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_DBGP; + enum Flags { @@ -19,6 +22,7 @@ namespace ESM Flag_Global = 4 // make available from main menu (i.e. not location specific) }; + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/dialoguestate.cpp b/components/esm3/dialoguestate.cpp similarity index 92% rename from components/esm/dialoguestate.cpp rename to components/esm3/dialoguestate.cpp index 2b1887e4eb..bdae1dbf4c 100644 --- a/components/esm/dialoguestate.cpp +++ b/components/esm3/dialoguestate.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::DialogueState::load (ESMReader &esm) +namespace ESM +{ + +void DialogueState::load (ESMReader &esm) { while (esm.isNextSub ("TOPI")) mKnownTopics.push_back (esm.getHString()); @@ -30,7 +33,7 @@ void ESM::DialogueState::load (ESMReader &esm) } } -void ESM::DialogueState::save (ESMWriter &esm) const +void DialogueState::save (ESMWriter &esm) const { for (std::vector::const_iterator iter (mKnownTopics.begin()); iter!=mKnownTopics.end(); ++iter) @@ -51,3 +54,5 @@ void ESM::DialogueState::save (ESMWriter &esm) const } } } + +} diff --git a/components/esm/dialoguestate.hpp b/components/esm3/dialoguestate.hpp similarity index 100% rename from components/esm/dialoguestate.hpp rename to components/esm3/dialoguestate.hpp diff --git a/components/esm/doorstate.cpp b/components/esm3/doorstate.cpp similarity index 100% rename from components/esm/doorstate.cpp rename to components/esm3/doorstate.cpp diff --git a/components/esm/doorstate.hpp b/components/esm3/doorstate.hpp similarity index 100% rename from components/esm/doorstate.hpp rename to components/esm3/doorstate.hpp diff --git a/components/esm/effectlist.cpp b/components/esm3/effectlist.cpp similarity index 92% rename from components/esm/effectlist.cpp rename to components/esm3/effectlist.cpp index f6d5a6e071..c578e4095a 100644 --- a/components/esm/effectlist.cpp +++ b/components/esm3/effectlist.cpp @@ -3,7 +3,8 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM { +namespace ESM +{ void EffectList::load(ESMReader &esm) { @@ -16,7 +17,7 @@ void EffectList::load(ESMReader &esm) void EffectList::add(ESMReader &esm) { ENAMstruct s; - esm.getHT(s, 24); + esm.getHTSized<24>(s); mList.push_back(s); } diff --git a/components/esm/effectlist.hpp b/components/esm3/effectlist.hpp similarity index 100% rename from components/esm/effectlist.hpp rename to components/esm3/effectlist.hpp diff --git a/components/esm/esmreader.cpp b/components/esm3/esmreader.cpp similarity index 58% rename from components/esm/esmreader.cpp rename to components/esm3/esmreader.cpp index 1b6eca7346..3e318cb2a6 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm3/esmreader.cpp @@ -1,17 +1,20 @@ #include "esmreader.hpp" +#include "readerscache.hpp" + +#include +#include + #include +#include +#include +#include namespace ESM { using namespace Misc; - std::string ESMReader::getName() const - { - return mCtx.filename; - } - ESM_Context ESMReader::getContext() { // Update the file position before returning @@ -22,16 +25,11 @@ ESM_Context ESMReader::getContext() ESMReader::ESMReader() : mRecordFlags(0) , mBuffer(50*1024) - , mGlobalReaderList(nullptr) , mEncoder(nullptr) , mFileSize(0) { clearCtx(); -} - -int ESMReader::getFormat() const -{ - return mHeader.mFormat; + mCtx.index = 0; } void ESMReader::restoreContext(const ESM_Context &rc) @@ -65,24 +63,48 @@ void ESMReader::clearCtx() mCtx.subName.clear(); } -void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name) +void ESMReader::resolveParentFileIndices(ReadersCache& readers) +{ + mCtx.parentFileIndices.clear(); + for (const Header::MasterData &mast : getGameFiles()) + { + const std::string& fname = mast.name; + int index = getIndex(); + for (int i = 0; i < getIndex(); i++) + { + const ESM::ReadersCache::BusyItem reader = readers.get(static_cast(i)); + if (reader->getFileSize() == 0) + continue; // Content file in non-ESM format + const std::string& candidate = reader->getName(); + std::string fnamecandidate = std::filesystem::path(candidate).filename().string(); + if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) + { + index = i; + break; + } + } + mCtx.parentFileIndices.push_back(index); + } +} + +void ESMReader::openRaw(std::unique_ptr&& stream, std::string_view name) { close(); - mEsm = _esm; + mEsm = std::move(stream); mCtx.filename = name; mEsm->seekg(0, mEsm->end); mCtx.leftFile = mFileSize = mEsm->tellg(); mEsm->seekg(0, mEsm->beg); } -void ESMReader::openRaw(const std::string& filename) +void ESMReader::openRaw(std::string_view filename) { - openRaw(Files::openConstrainedFileStream(filename.c_str()), filename); + openRaw(Files::openBinaryInputFileStream(std::string(filename)), filename); } -void ESMReader::open(Files::IStreamPtr _esm, const std::string &name) +void ESMReader::open(std::unique_ptr&& stream, const std::string &name) { - openRaw(_esm, name); + openRaw(std::move(stream), name); if (getRecName() != "TES3") fail("Not a valid Morrowind file"); @@ -94,24 +116,23 @@ void ESMReader::open(Files::IStreamPtr _esm, const std::string &name) void ESMReader::open(const std::string &file) { - open (Files::openConstrainedFileStream (file.c_str ()), file); + open(Files::openBinaryInputFileStream(file), file); } -int64_t ESMReader::getHNLong(const char *name) +std::string ESMReader::getHNOString(NAME name) { - int64_t val; - getHNT(val, name); - return val; + if (isNextSub(name)) + return getHString(); + return ""; } -std::string ESMReader::getHNOString(const char* name) +void ESMReader::skipHNOString(NAME name) { if (isNextSub(name)) - return getHString(); - return ""; + skipHString(); } -std::string ESMReader::getHNString(const char* name) +std::string ESMReader::getHNString(NAME name) { getSubNameIs(name); return getHString(); @@ -126,50 +147,64 @@ std::string ESMReader::getHString() // them. For some reason, they break the rules, and contain a byte // (value 0) even if the header says there is no data. If // Morrowind accepts it, so should we. - if (mCtx.leftSub == 0 && !mEsm->peek()) + if (mCtx.leftSub == 0 && hasMoreSubs() && !mEsm->peek()) { // Skip the following zero byte mCtx.leftRec--; char c; - getExact(&c, 1); - return ""; + getT(c); + return std::string(); } return getString(mCtx.leftSub); } -void ESMReader::getHExact(void*p, int size) +void ESMReader::skipHString() { getSubHeader(); - if (size != static_cast (mCtx.leftSub)) + + // Hack to make MultiMark.esp load. Zero-length strings do not + // occur in any of the official mods, but MultiMark makes use of + // them. For some reason, they break the rules, and contain a byte + // (value 0) even if the header says there is no data. If + // Morrowind accepts it, so should we. + if (mCtx.leftSub == 0 && hasMoreSubs() && !mEsm->peek()) { - std::stringstream error; - error << "getHExact(): size mismatch (requested " << size << ", got " << mCtx.leftSub << ")"; - fail(error.str()); + // Skip the following zero byte + mCtx.leftRec--; + skipT(); + return; } + + skip(mCtx.leftSub); +} + +void ESMReader::getHExact(void*p, int size) +{ + getSubHeader(); + if (size != static_cast (mCtx.leftSub)) + reportSubSizeMismatch(size, mCtx.leftSub); getExact(p, size); } // Read the given number of bytes from a named subrecord -void ESMReader::getHNExact(void*p, int size, const char* name) +void ESMReader::getHNExact(void*p, int size, NAME name) { getSubNameIs(name); getHExact(p, size); } // Get the next subrecord name and check if it matches the parameter -void ESMReader::getSubNameIs(const char* name) +void ESMReader::getSubNameIs(NAME name) { getSubName(); if (mCtx.subName != name) - fail( - "Expected subrecord " + std::string(name) + " but got " - + mCtx.subName.toString()); + fail("Expected subrecord " + name.toString() + " but got " + mCtx.subName.toString()); } -bool ESMReader::isNextSub(const char* name) +bool ESMReader::isNextSub(NAME name) { - if (!mCtx.leftRec) + if (!hasMoreSubs()) return false; getSubName(); @@ -182,9 +217,9 @@ bool ESMReader::isNextSub(const char* name) return !mCtx.subCached; } -bool ESMReader::peekNextSub(const char *name) +bool ESMReader::peekNextSub(NAME name) { - if (!mCtx.leftRec) + if (!hasMoreSubs()) return false; getSubName(); @@ -193,11 +228,6 @@ bool ESMReader::peekNextSub(const char *name) return mCtx.subName == name; } -void ESMReader::cacheSubName() -{ - mCtx.subCached = true; -} - // Read subrecord name. This gets called a LOT, so I've optimized it // slightly. void ESMReader::getSubName() @@ -210,21 +240,9 @@ void ESMReader::getSubName() } // reading the subrecord data anyway. - const size_t subNameSize = mCtx.subName.data_size(); - getExact(mCtx.subName.rw_data(), subNameSize); - mCtx.leftRec -= subNameSize; -} - -bool ESMReader::isEmptyOrGetName() -{ - if (mCtx.leftRec) - { - const size_t subNameSize = mCtx.subName.data_size(); - getExact(mCtx.subName.rw_data(), subNameSize); - mCtx.leftRec -= subNameSize; - return false; - } - return true; + const std::size_t subNameSize = decltype(mCtx.subName)::sCapacity; + getExact(mCtx.subName.mData, static_cast(subNameSize)); + mCtx.leftRec -= static_cast(subNameSize); } void ESMReader::skipHSub() @@ -237,10 +255,10 @@ void ESMReader::skipHSubSize(int size) { skipHSub(); if (static_cast (mCtx.leftSub) != size) - fail("skipHSubSize() mismatch"); + reportSubSizeMismatch(mCtx.leftSub, size); } -void ESMReader::skipHSubUntil(const char *name) +void ESMReader::skipHSubUntil(NAME name) { while (hasMoreSubs() && !isNextSub(name)) { @@ -253,21 +271,17 @@ void ESMReader::skipHSubUntil(const char *name) void ESMReader::getSubHeader() { - if (mCtx.leftRec < 4) + if (mCtx.leftRec < sizeof(mCtx.leftSub)) fail("End of record while reading sub-record header"); // Get subrecord size getT(mCtx.leftSub); + mCtx.leftRec -= sizeof(mCtx.leftSub); // Adjust number of record bytes left - mCtx.leftRec -= mCtx.leftSub + 4; -} - -void ESMReader::getSubHeaderIs(int size) -{ - getSubHeader(); - if (size != static_cast (mCtx.leftSub)) - fail("getSubHeaderIs(): Sub header mismatch"); + if (mCtx.leftRec < mCtx.leftSub) + fail("Record size is larger than rest of file"); + mCtx.leftRec -= mCtx.leftSub; } NAME ESMReader::getRecName() @@ -275,7 +289,7 @@ NAME ESMReader::getRecName() if (!hasMoreRecs()) fail("No more records, getRecName() failed"); getName(mCtx.recName); - mCtx.leftFile -= mCtx.recName.data_size(); + mCtx.leftFile -= decltype(mCtx.recName)::sCapacity; // Make sure we don't carry over any old cached subrecord // names. This can happen in some cases when we skip parts of a @@ -295,7 +309,7 @@ void ESMReader::skipRecord() void ESMReader::getRecHeader(uint32_t &flags) { // General error checking - if (mCtx.leftFile < 12) + if (mCtx.leftFile < 3 * sizeof(uint32_t)) fail("End of file while reading record header"); if (mCtx.leftRec) fail("Previous record contains unread bytes"); @@ -303,11 +317,11 @@ void ESMReader::getRecHeader(uint32_t &flags) getUint(mCtx.leftRec); getUint(flags);// This header entry is always zero getUint(flags); - mCtx.leftFile -= 12; + mCtx.leftFile -= 3 * sizeof(uint32_t); // Check that sizes add up if (mCtx.leftFile < mCtx.leftRec) - fail("Record size is larger than rest of file"); + reportSubSizeMismatch(mCtx.leftFile, mCtx.leftRec); // Adjust number of bytes mCtx.left in file mCtx.leftFile -= mCtx.leftRec; @@ -319,18 +333,6 @@ void ESMReader::getRecHeader(uint32_t &flags) * *************************************************************************/ -void ESMReader::getExact(void*x, int size) -{ - try - { - mEsm->read((char*)x, size); - } - catch (std::exception& e) - { - fail(std::string("Read error: ") + e.what()); - } -} - std::string ESMReader::getString(int size) { size_t s = size; @@ -343,46 +345,29 @@ std::string ESMReader::getString(int size) mBuffer[s] = 0; // read ESM data - char *ptr = &mBuffer[0]; + char *ptr = mBuffer.data(); getExact(ptr, size); - size = strnlen(ptr, size); + size = static_cast(strnlen(ptr, size)); // Convert to UTF8 and return if (mEncoder) - return mEncoder->getUtf8(ptr, size); + return std::string(mEncoder->getUtf8(std::string_view(ptr, size))); return std::string (ptr, size); } -void ESMReader::fail(const std::string &msg) +[[noreturn]] void ESMReader::fail(const std::string &msg) { - using namespace std; - - stringstream ss; + std::stringstream ss; ss << "ESM Error: " << msg; ss << "\n File: " << mCtx.filename; - ss << "\n Record: " << mCtx.recName.toString(); - ss << "\n Subrecord: " << mCtx.subName.toString(); + ss << "\n Record: " << mCtx.recName.toStringView(); + ss << "\n Subrecord: " << mCtx.subName.toStringView(); if (mEsm.get()) - ss << "\n Offset: 0x" << hex << mEsm->tellg(); + ss << "\n Offset: 0x" << std::hex << mEsm->tellg(); throw std::runtime_error(ss.str()); } -void ESMReader::setEncoder(ToUTF8::Utf8Encoder* encoder) -{ - mEncoder = encoder; -} - -size_t ESMReader::getFileOffset() -{ - return mEsm->tellg(); -} - -void ESMReader::skip(int bytes) -{ - mEsm->seekg(getFileOffset()+bytes); -} - } diff --git a/components/esm/esmreader.hpp b/components/esm3/esmreader.hpp similarity index 68% rename from components/esm/esmreader.hpp rename to components/esm3/esmreader.hpp index 761756e8fb..4255428729 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm3/esmreader.hpp @@ -2,20 +2,21 @@ #define OPENMW_ESM_READER_H #include -#include #include -#include - -#include +#include +#include #include #include -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" #include "loadtes3.hpp" -namespace ESM { +namespace ESM +{ + +class ReadersCache; class ESMReader { @@ -32,14 +33,15 @@ public: int getVer() const { return mHeader.mData.version; } int getRecordCount() const { return mHeader.mData.records; } float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; } - const std::string getAuthor() const { return mHeader.mData.author; } - const std::string getDesc() const { return mHeader.mData.desc; } + const std::string& getAuthor() const { return mHeader.mData.author; } + const std::string& getDesc() const { return mHeader.mData.desc; } const std::vector &getGameFiles() const { return mHeader.mMaster; } const Header& getHeader() const { return mHeader; } - int getFormat() const; + int getFormat() const { return mHeader.mFormat; }; const NAME &retSubName() const { return mCtx.subName; } uint32_t getSubSize() const { return mCtx.leftSub; } - std::string getName() const; + const std::string& getName() const { return mCtx.filename; }; + bool isOpen() const { return mEsm != nullptr; } /************************************************************************* * @@ -62,30 +64,31 @@ public: /// Raw opening. Opens the file and sets everything up but doesn't /// parse the header. - void openRaw(Files::IStreamPtr _esm, const std::string &name); + void openRaw(std::unique_ptr&& stream, std::string_view name); /// Load ES file from a new stream, parses the header. Closes the /// currently open file first, if any. - void open(Files::IStreamPtr _esm, const std::string &name); + void open(std::unique_ptr&& stream, const std::string &name); void open(const std::string &file); - void openRaw(const std::string &filename); + void openRaw(std::string_view filename); /// Get the current position in the file. Make sure that the file has been opened! - size_t getFileOffset(); + size_t getFileOffset() const { return mEsm->tellg(); }; // This is a quick hack for multiple esm/esp files. Each plugin introduces its own // terrain palette, but ESMReader does not pass a reference to the correct plugin // to the individual load() methods. This hack allows to pass this reference // indirectly to the load() method. void setIndex(const int index) { mCtx.index = index;} - int getIndex() {return mCtx.index;} - - void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} - std::vector *getGlobalReaderList() {return mGlobalReaderList;} + int getIndex() const {return mCtx.index;} - void addParentFileIndex(int index) { mCtx.parentFileIndices.push_back(index); } + // Assign parent esX files by tracking their indices in the global list of + // all files/readers used by the engine. This is required for correct adjustRefNum() results + // as required for handling moved, deleted and edited CellRefs. + /// @note Does not validate. + void resolveParentFileIndices(ReadersCache& readers); const std::vector& getParentFileIndices() const { return mCtx.parentFileIndices; } /************************************************************************* @@ -96,7 +99,7 @@ public: // Read data of a given type, stored in a subrecord of a given name template - void getHNT(X &x, const char* name) + void getHNT(X &x, NAME name) { getSubNameIs(name); getHT(x); @@ -104,7 +107,7 @@ public: // Optional version of getHNT template - void getHNOT(X &x, const char* name) + void getHNOT(X &x, NAME name) { if(isNextSub(name)) getHT(x); @@ -112,61 +115,73 @@ public: // Version with extra size checking, to make sure the compiler // doesn't mess up our struct padding. - template - void getHNT(X &x, const char* name, int size) + template + void getHNTSized(X &x, NAME name) { - assert(sizeof(X) == size); - getSubNameIs(name); - getHT(x); + static_assert(sizeof(X) == size); + getHNT(x, name); } - template - void getHNOT(X &x, const char* name, int size) + template + void getHNOTSized(X &x, NAME name) { - assert(sizeof(X) == size); - if(isNextSub(name)) - getHT(x); + static_assert(sizeof(X) == size); + getHNOT(x, name); } - int64_t getHNLong(const char *name); - // Get data of a given type/size, including subrecord header template void getHT(X &x) { getSubHeader(); if (mCtx.leftSub != sizeof(X)) - { - std::stringstream error; - error << "getHT(): subrecord size mismatch (requested " << sizeof(X) << ", got " << mCtx.leftSub << ")"; - fail(error.str()); - } + reportSubSizeMismatch(sizeof(X), mCtx.leftSub); getT(x); } + template + void skipHT() + { + getSubHeader(); + if (mCtx.leftSub != sizeof(T)) + reportSubSizeMismatch(sizeof(T), mCtx.leftSub); + skipT(); + } + // Version with extra size checking, to make sure the compiler // doesn't mess up our struct padding. - template - void getHT(X &x, int size) + template + void getHTSized(X &x) { - assert(sizeof(X) == size); + static_assert(sizeof(X) == size); getHT(x); } + template + void skipHTSized() + { + static_assert(sizeof(T) == size); + skipHT(); + } + // Read a string by the given name if it is the next record. - std::string getHNOString(const char* name); + std::string getHNOString(NAME name); + + void skipHNOString(NAME name); // Read a string with the given sub-record name - std::string getHNString(const char* name); + std::string getHNString(NAME name); // Read a string, including the sub-record header (but not the name) std::string getHString(); + void skipHString(); + // Read the given number of bytes from a subrecord void getHExact(void*p, int size); // Read the given number of bytes from a named subrecord - void getHNExact(void*p, int size, const char* name); + void getHNExact(void*p, int size, NAME name); /************************************************************************* * @@ -175,27 +190,24 @@ public: *************************************************************************/ // Get the next subrecord name and check if it matches the parameter - void getSubNameIs(const char* name); + void getSubNameIs(NAME name); /** Checks if the next sub record name matches the parameter. If it does, it is read into 'subName' just as if getSubName() was called. If not, the read name will still be available for future calls to getSubName(), isNextSub() and getSubNameIs(). */ - bool isNextSub(const char* name); + bool isNextSub(NAME name); - bool peekNextSub(const char* name); + bool peekNextSub(NAME name); // Store the current subrecord name for the next call of getSubName() - void cacheSubName(); + void cacheSubName() {mCtx.subCached = true; }; // Read subrecord name. This gets called a LOT, so I've optimized it // slightly. void getSubName(); - // This is specially optimized for LoadINFO. - bool isEmptyOrGetName(); - // Skip current sub record, including header (but not including // name.) void skipHSub(); @@ -204,17 +216,13 @@ public: void skipHSubSize(int size); // Skip all subrecords until the given subrecord or no more subrecords remaining - void skipHSubUntil(const char* name); + void skipHSubUntil(NAME name); /* Sub-record header. This updates leftRec beyond the current sub-record as well. leftSub contains size of current sub-record. */ void getSubHeader(); - /** Get sub header and check the size - */ - void getSubHeaderIs(int size); - /************************************************************************* * * Low level record methods @@ -248,7 +256,10 @@ public: template void getT(X &x) { getExact(&x, sizeof(X)); } - void getExact(void*x, int size); + template + void skipT() { skip(sizeof(T)); } + + void getExact(void* x, int size) { mEsm->read((char*)x, size); } void getName(NAME &name) { getT(name); } void getUint(uint32_t &u) { getT(u); } @@ -256,13 +267,20 @@ public: // them from native encoding to UTF8 in the process. std::string getString(int size); - void skip(int bytes); + void skip(std::size_t bytes) + { + char buffer[4096]; + if (bytes > std::size(buffer)) + mEsm->seekg(getFileOffset() + bytes); + else + mEsm->read(buffer, bytes); + } /// Used for error handling - void fail(const std::string &msg); + [[noreturn]] void fail(const std::string &msg); /// Sets font encoder for ESM strings - void setEncoder(ToUTF8::Utf8Encoder* encoder); + void setEncoder(ToUTF8::Utf8Encoder* encoder) { mEncoder = encoder; }; /// Get record flags of last record unsigned int getRecordFlags() { return mRecordFlags; } @@ -270,9 +288,16 @@ public: size_t getFileSize() const { return mFileSize; } private: + [[noreturn]] void reportSubSizeMismatch(size_t want, size_t got) { + fail("record size mismatch, requested " + + std::to_string(want) + + ", got" + + std::to_string(got)); + } + void clearCtx(); - Files::IStreamPtr mEsm; + std::unique_ptr mEsm; ESM_Context mCtx; @@ -285,7 +310,6 @@ private: Header mHeader; - std::vector *mGlobalReaderList; ToUTF8::Utf8Encoder* mEncoder; size_t mFileSize; diff --git a/components/esm/esmwriter.cpp b/components/esm3/esmwriter.cpp similarity index 79% rename from components/esm/esmwriter.cpp rename to components/esm3/esmwriter.cpp index 09fca4b262..351de8612a 100644 --- a/components/esm/esmwriter.cpp +++ b/components/esm3/esmwriter.cpp @@ -86,7 +86,7 @@ namespace ESM throw std::runtime_error ("Unclosed record remaining"); } - void ESMWriter::startRecord(const std::string& name, uint32_t flags) + void ESMWriter::startRecord(NAME name, uint32_t flags) { mRecordCount++; @@ -105,15 +105,10 @@ namespace ESM void ESMWriter::startRecord (uint32_t name, uint32_t flags) { - std::string type; - for (int i=0; i<4; ++i) - /// \todo make endianess agnostic - type += reinterpret_cast (&name)[i]; - - startRecord (type, flags); + startRecord(NAME(name), flags); } - void ESMWriter::startSubRecord(const std::string& name) + void ESMWriter::startSubRecord(NAME name) { // Sub-record hierarchies are not properly supported in ESMReader. This should be fixed later. assert (mRecords.size() <= 1); @@ -129,7 +124,7 @@ namespace ESM assert(mRecords.back().size == 0); } - void ESMWriter::endRecord(const std::string& name) + void ESMWriter::endRecord(NAME name) { RecordData rec = mRecords.back(); assert(rec.name == name); @@ -147,22 +142,17 @@ namespace ESM void ESMWriter::endRecord (uint32_t name) { - std::string type; - for (int i=0; i<4; ++i) - /// \todo make endianess agnostic - type += reinterpret_cast (&name)[i]; - - endRecord (type); + endRecord(NAME(name)); } - void ESMWriter::writeHNString(const std::string& name, const std::string& data) + void ESMWriter::writeHNString(NAME name, const std::string& data) { startSubRecord(name); writeHString(data); endRecord(name); } - void ESMWriter::writeHNString(const std::string& name, const std::string& data, size_t size) + void ESMWriter::writeHNString(NAME name, const std::string& data, size_t size) { assert(data.size() <= size); startSubRecord(name); @@ -177,7 +167,7 @@ namespace ESM endRecord(name); } - void ESMWriter::writeFixedSizeString(const std::string &data, int size) + void ESMWriter::writeFixedSizeString(const std::string& data, int size) { std::string string; if (!data.empty()) @@ -193,9 +183,9 @@ namespace ESM else { // Convert to UTF8 and return - std::string string = mEncoder ? mEncoder->getLegacyEnc(data) : data; + const std::string_view string = mEncoder != nullptr ? mEncoder->getLegacyEnc(data) : data; - write(string.c_str(), string.size()); + write(string.data(), string.size()); } } @@ -206,10 +196,9 @@ namespace ESM write("\0", 1); } - void ESMWriter::writeName(const std::string& name) + void ESMWriter::writeName(NAME name) { - assert((name.size() == 4 && name[3] != '\0')); - write(name.c_str(), name.size()); + write(name.mData, NAME::sCapacity); } void ESMWriter::write(const char* data, size_t size) @@ -217,7 +206,7 @@ namespace ESM if (mCounting && !mRecords.empty()) { for (std::list::iterator it = mRecords.begin(); it != mRecords.end(); ++it) - it->size += size; + it->size += static_cast(size); } mStream->write(data, size); diff --git a/components/esm/esmwriter.hpp b/components/esm3/esmwriter.hpp similarity index 73% rename from components/esm/esmwriter.hpp rename to components/esm3/esmwriter.hpp index ba5800f67c..ddb9844796 100644 --- a/components/esm/esmwriter.hpp +++ b/components/esm3/esmwriter.hpp @@ -3,8 +3,9 @@ #include #include +#include -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" #include "loadtes3.hpp" namespace ToUTF8 @@ -12,13 +13,14 @@ namespace ToUTF8 class Utf8Encoder; } -namespace ESM { +namespace ESM +{ class ESMWriter { struct RecordData { - std::string name; + NAME name; std::streampos position; uint32_t size; }; @@ -29,13 +31,14 @@ class ESMWriter unsigned int getVersion() const; - // Set various header data (ESM::Header::Data). All of the below functions must be called before writing, + // Set various header data (Header::Data). All of the below functions must be called before writing, // otherwise this data will be left uninitialized. void setVersion(unsigned int ver = 0x3fa66666); void setType(int type); void setEncoder(ToUTF8::Utf8Encoder *encoding); void setAuthor(const std::string& author); void setDescription(const std::string& desc); + void setHeader(const Header& value) { mHeader = value; } // Set the record count for writing it in the file header void setRecordCount (int count); @@ -55,27 +58,27 @@ class ESMWriter void close(); ///< \note Does not close the stream. - void writeHNString(const std::string& name, const std::string& data); - void writeHNString(const std::string& name, const std::string& data, size_t size); - void writeHNCString(const std::string& name, const std::string& data) + void writeHNString(NAME name, const std::string& data); + void writeHNString(NAME name, const std::string& data, size_t size); + void writeHNCString(NAME name, const std::string& data) { startSubRecord(name); writeHCString(data); endRecord(name); } - void writeHNOString(const std::string& name, const std::string& data) + void writeHNOString(NAME name, const std::string& data) { if (!data.empty()) writeHNString(name, data); } - void writeHNOCString(const std::string& name, const std::string& data) + void writeHNOCString(NAME name, const std::string& data) { if (!data.empty()) writeHNCString(name, data); } template - void writeHNT(const std::string& name, const T& data) + void writeHNT(NAME name, const T& data) { startSubRecord(name); writeT(data); @@ -83,7 +86,7 @@ class ESMWriter } template - void writeHNT(const std::string& name, const T (&data)[size]) + void writeHNT(NAME name, const T (&data)[size]) { startSubRecord(name); writeT(data); @@ -93,15 +96,15 @@ class ESMWriter // Prevent using writeHNT with strings. This already happened by accident and results in // state being discarded without any error on writing or reading it. :( // writeHNString and friends must be used instead. - void writeHNT(const std::string& name, const std::string& data) = delete; + void writeHNT(NAME name, const std::string& data) = delete; - void writeT(const std::string& data) = delete; + void writeT(NAME data) = delete; template - void writeHNT(const std::string& name, const T (&data)[size], int) = delete; + void writeHNT(NAME name, const T (&data)[size], int) = delete; template - void writeHNT(const std::string& name, const T& data, int size) + void writeHNT(NAME name, const T& data, int size) { startSubRecord(name); writeT(data, size); @@ -111,7 +114,8 @@ class ESMWriter template void writeT(const T& data) { - write((char*)&data, sizeof(T)); + static_assert(!std::is_pointer_v); + write(reinterpret_cast(&data), sizeof(T)); } template @@ -123,19 +127,20 @@ class ESMWriter template void writeT(const T& data, size_t size) { + static_assert(!std::is_pointer_v); write((char*)&data, size); } - void startRecord(const std::string& name, uint32_t flags = 0); + void startRecord(NAME name, uint32_t flags = 0); void startRecord(uint32_t name, uint32_t flags = 0); /// @note Sub-record hierarchies are not properly supported in ESMReader. This should be fixed later. - void startSubRecord(const std::string& name); - void endRecord(const std::string& name); + void startSubRecord(NAME name); + void endRecord(NAME name); void endRecord(uint32_t name); void writeFixedSizeString(const std::string& data, int size); void writeHString(const std::string& data); void writeHCString(const std::string& data); - void writeName(const std::string& data); + void writeName(NAME data); void write(const char* data, size_t size); private: diff --git a/components/esm/filter.cpp b/components/esm3/filter.cpp similarity index 63% rename from components/esm/filter.cpp rename to components/esm3/filter.cpp index 4f0519f80b..217b090b3a 100644 --- a/components/esm/filter.cpp +++ b/components/esm3/filter.cpp @@ -2,30 +2,31 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" -unsigned int ESM::Filter::sRecordId = REC_FILT; +namespace ESM +{ -void ESM::Filter::load (ESMReader& esm, bool &isDeleted) +void Filter::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { esm.getSubName(); - uint32_t name = esm.retSubName().intval; + uint32_t name = esm.retSubName().toInt(); switch (name) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); break; - case ESM::FourCC<'F','I','L','T'>::value: + case fourCC("FILT"): mFilter = esm.getHString(); break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -36,13 +37,13 @@ void ESM::Filter::load (ESMReader& esm, bool &isDeleted) } } -void ESM::Filter::save (ESMWriter& esm, bool isDeleted) const +void Filter::save (ESMWriter& esm, bool isDeleted) const { esm.writeHNCString ("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -50,8 +51,11 @@ void ESM::Filter::save (ESMWriter& esm, bool isDeleted) const esm.writeHNCString ("DESC", mDescription); } -void ESM::Filter::blank() +void Filter::blank() { + mRecordFlags = 0; mFilter.clear(); mDescription.clear(); } + +} diff --git a/components/esm/filter.hpp b/components/esm3/filter.hpp similarity index 78% rename from components/esm/filter.hpp rename to components/esm3/filter.hpp index b1c511ebba..2284718d8f 100644 --- a/components/esm/filter.hpp +++ b/components/esm3/filter.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_ESM_FILTER_H #include +#include "components/esm/defs.hpp" namespace ESM { @@ -10,8 +11,10 @@ namespace ESM struct Filter { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_FILT; + + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/fogstate.cpp b/components/esm3/fogstate.cpp similarity index 94% rename from components/esm/fogstate.cpp rename to components/esm3/fogstate.cpp index ff20f339f9..8b072d1ce9 100644 --- a/components/esm/fogstate.cpp +++ b/components/esm3/fogstate.cpp @@ -10,6 +10,10 @@ #include "savedgame.hpp" +namespace ESM +{ +namespace +{ void convertFogOfWar(std::vector& imageData) { if (imageData.empty()) @@ -52,7 +56,9 @@ void convertFogOfWar(std::vector& imageData) imageData = std::vector(str.begin(), str.end()); } -void ESM::FogState::load (ESMReader &esm) +} + +void FogState::load (ESMReader &esm) { esm.getHNOT(mBounds, "BOUN"); esm.getHNOT(mNorthMarkerAngle, "ANGL"); @@ -76,7 +82,7 @@ void ESM::FogState::load (ESMReader &esm) } } -void ESM::FogState::save (ESMWriter &esm, bool interiorCell) const +void FogState::save (ESMWriter &esm, bool interiorCell) const { if (interiorCell) { @@ -92,3 +98,5 @@ void ESM::FogState::save (ESMWriter &esm, bool interiorCell) const esm.endRecord("FTEX"); } } + +} diff --git a/components/esm/fogstate.hpp b/components/esm3/fogstate.hpp similarity index 100% rename from components/esm/fogstate.hpp rename to components/esm3/fogstate.hpp diff --git a/components/esm/globalmap.cpp b/components/esm3/globalmap.cpp similarity index 83% rename from components/esm/globalmap.cpp rename to components/esm3/globalmap.cpp index 190329c61e..8a0dc016b5 100644 --- a/components/esm/globalmap.cpp +++ b/components/esm3/globalmap.cpp @@ -2,11 +2,11 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" -unsigned int ESM::GlobalMap::sRecordId = ESM::REC_GMAP; +namespace ESM +{ -void ESM::GlobalMap::load (ESMReader &esm) +void GlobalMap::load (ESMReader &esm) { esm.getHNT(mBounds, "BNDS"); @@ -25,7 +25,7 @@ void ESM::GlobalMap::load (ESMReader &esm) } } -void ESM::GlobalMap::save (ESMWriter &esm) const +void GlobalMap::save (ESMWriter &esm) const { esm.writeHNT("BNDS", mBounds); @@ -41,3 +41,5 @@ void ESM::GlobalMap::save (ESMWriter &esm) const esm.endRecord("MRK_"); } } + +} diff --git a/components/esm/globalmap.hpp b/components/esm3/globalmap.hpp similarity index 87% rename from components/esm/globalmap.hpp rename to components/esm3/globalmap.hpp index e89123f898..593f8726b9 100644 --- a/components/esm/globalmap.hpp +++ b/components/esm3/globalmap.hpp @@ -4,6 +4,8 @@ #include #include +#include "components/esm/defs.hpp" + namespace ESM { class ESMReader; @@ -14,7 +16,8 @@ namespace ESM ///< \brief An image containing the explored areas on the global map. struct GlobalMap { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_GMAP; + // The minimum and maximum cell coordinates struct Bounds diff --git a/components/esm/globalscript.cpp b/components/esm3/globalscript.cpp similarity index 72% rename from components/esm/globalscript.cpp rename to components/esm3/globalscript.cpp index 239d162f29..0eb1e31e9c 100644 --- a/components/esm/globalscript.cpp +++ b/components/esm3/globalscript.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::GlobalScript::load (ESMReader &esm) +namespace ESM +{ + +void GlobalScript::load (ESMReader &esm) { mId = esm.getHNString ("NAME"); @@ -13,13 +16,12 @@ void ESM::GlobalScript::load (ESMReader &esm) esm.getHNOT (mRunning, "RUN_"); mTargetRef.unset(); - if (esm.peekNextSub("TARG")) - mTargetId = esm.getHNString ("TARG"); + mTargetId = esm.getHNOString ("TARG"); if (esm.peekNextSub("FRMR")) mTargetRef.load(esm, true, "FRMR"); } -void ESM::GlobalScript::save (ESMWriter &esm) const +void GlobalScript::save (ESMWriter &esm) const { esm.writeHNString ("NAME", mId); @@ -31,7 +33,9 @@ void ESM::GlobalScript::save (ESMWriter &esm) const if (!mTargetId.empty()) { esm.writeHNOString ("TARG", mTargetId); - if (mTargetRef.hasContentFile()) + if (mTargetRef.isSet()) mTargetRef.save (esm, true, "FRMR"); } } + +} diff --git a/components/esm/globalscript.hpp b/components/esm3/globalscript.hpp similarity index 100% rename from components/esm/globalscript.hpp rename to components/esm3/globalscript.hpp diff --git a/components/esm/inventorystate.cpp b/components/esm3/inventorystate.cpp similarity index 96% rename from components/esm/inventorystate.cpp rename to components/esm3/inventorystate.cpp index 980d67f7ef..7dc971d205 100644 --- a/components/esm/inventorystate.cpp +++ b/components/esm3/inventorystate.cpp @@ -5,7 +5,10 @@ #include -void ESM::InventoryState::load (ESMReader &esm) +namespace ESM +{ + +void InventoryState::load (ESMReader &esm) { // obsolete int index = 0; @@ -56,7 +59,7 @@ void ESM::InventoryState::load (ESMReader &esm) //Get its name std::string id = esm.getHString(); int count; - std::string parentGroup = ""; + std::string parentGroup; //Then get its count esm.getHNT (count, "COUN"); //Old save formats don't have information about parent group; check for that @@ -123,7 +126,7 @@ void ESM::InventoryState::load (ESMReader &esm) } } -void ESM::InventoryState::save (ESMWriter &esm) const +void InventoryState::save (ESMWriter &esm) const { int itemsCount = static_cast(mItems.size()); if (itemsCount > 0) @@ -170,3 +173,5 @@ void ESM::InventoryState::save (ESMWriter &esm) const if (mSelectedEnchantItem != -1) esm.writeHNT ("SELE", mSelectedEnchantItem); } + +} diff --git a/components/esm/inventorystate.hpp b/components/esm3/inventorystate.hpp similarity index 100% rename from components/esm/inventorystate.hpp rename to components/esm3/inventorystate.hpp diff --git a/components/esm/journalentry.cpp b/components/esm3/journalentry.cpp similarity index 88% rename from components/esm/journalentry.cpp rename to components/esm3/journalentry.cpp index 93011e581b..869a5df29e 100644 --- a/components/esm/journalentry.cpp +++ b/components/esm3/journalentry.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::JournalEntry::load (ESMReader &esm) +namespace ESM +{ + +void JournalEntry::load (ESMReader &esm) { esm.getHNOT (mType, "JETY"); mTopic = esm.getHNString ("YETO"); @@ -20,7 +23,7 @@ void ESM::JournalEntry::load (ESMReader &esm) mActorName = esm.getHNOString("ACT_"); } -void ESM::JournalEntry::save (ESMWriter &esm) const +void JournalEntry::save (ESMWriter &esm) const { esm.writeHNT ("JETY", mType); esm.writeHNString ("YETO", mTopic); @@ -36,3 +39,5 @@ void ESM::JournalEntry::save (ESMWriter &esm) const else if (mType==Type_Topic) esm.writeHNString ("ACT_", mActorName); } + +} diff --git a/components/esm/journalentry.hpp b/components/esm3/journalentry.hpp similarity index 100% rename from components/esm/journalentry.hpp rename to components/esm3/journalentry.hpp diff --git a/components/esm/loadacti.cpp b/components/esm3/loadacti.cpp similarity index 77% rename from components/esm/loadacti.cpp rename to components/esm3/loadacti.cpp index ba35535b8f..4ef11f8563 100644 --- a/components/esm/loadacti.cpp +++ b/components/esm3/loadacti.cpp @@ -2,36 +2,34 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Activator::sRecordId = REC_ACTI; - void Activator::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -50,7 +48,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -61,6 +59,7 @@ namespace ESM void Activator::blank() { + mRecordFlags = 0; mName.clear(); mScript.clear(); mModel.clear(); diff --git a/components/esm/loadacti.hpp b/components/esm3/loadacti.hpp similarity index 72% rename from components/esm/loadacti.hpp rename to components/esm3/loadacti.hpp index 4cc72d5283..e0dac3f85a 100644 --- a/components/esm/loadacti.hpp +++ b/components/esm3/loadacti.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_ACTI_H #include +#include "components/esm/defs.hpp" namespace ESM { @@ -11,10 +12,12 @@ class ESMWriter; struct Activator { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_ACTI; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Activator"; } + static std::string_view getRecordType() { return "Activator"; } + unsigned int mRecordFlags; std::string mId, mName, mScript, mModel; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadalch.cpp b/components/esm3/loadalch.cpp similarity index 74% rename from components/esm/loadalch.cpp rename to components/esm3/loadalch.cpp index 85c24dc2d1..e6504c61d8 100644 --- a/components/esm/loadalch.cpp +++ b/components/esm3/loadalch.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Potion::sRecordId = REC_ALCH; - void Potion::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); @@ -19,32 +17,32 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'T','E','X','T'>::value: // not ITEX here for some reason + case fourCC("TEXT"): // not ITEX here for some reason mIcon = esm.getHString(); break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'A','L','D','T'>::value: - esm.getHT(mData, 12); + case fourCC("ALDT"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEffects.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -65,7 +63,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -79,6 +77,7 @@ namespace ESM void Potion::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mAutoCalc = 0; diff --git a/components/esm/loadalch.hpp b/components/esm3/loadalch.hpp similarity index 79% rename from components/esm/loadalch.hpp rename to components/esm3/loadalch.hpp index 9ef390ebd9..76d87bed63 100644 --- a/components/esm/loadalch.hpp +++ b/components/esm3/loadalch.hpp @@ -4,6 +4,7 @@ #include #include "effectlist.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -17,10 +18,11 @@ class ESMWriter; struct Potion { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_ALCH; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Potion"; } + static std::string_view getRecordType() { return "Potion"; } struct ALDTstruct { @@ -30,6 +32,7 @@ struct Potion }; ALDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; EffectList mEffects; diff --git a/components/esm/loadappa.cpp b/components/esm3/loadappa.cpp similarity index 78% rename from components/esm/loadappa.cpp rename to components/esm3/loadappa.cpp index 1bd1f93798..124d709973 100644 --- a/components/esm/loadappa.cpp +++ b/components/esm3/loadappa.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Apparatus::sRecordId = REC_APPA; - void Apparatus::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'A','A','D','T'>::value: + case fourCC("AADT"): esm.getHT(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -61,7 +59,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -74,6 +72,7 @@ namespace ESM void Apparatus::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mQuality = 0; mData.mWeight = 0; diff --git a/components/esm/loadappa.hpp b/components/esm3/loadappa.hpp similarity index 80% rename from components/esm/loadappa.hpp rename to components/esm3/loadappa.hpp index 74f8dd6aea..67dd462523 100644 --- a/components/esm/loadappa.hpp +++ b/components/esm3/loadappa.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -15,9 +17,10 @@ class ESMWriter; struct Apparatus { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_APPA; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Apparatus"; } + static std::string_view getRecordType() { return "Apparatus"; } enum AppaType { @@ -36,6 +39,7 @@ struct Apparatus }; AADTstruct mData; + unsigned int mRecordFlags; std::string mId, mModel, mIcon, mScript, mName; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadarmo.cpp b/components/esm3/loadarmo.cpp similarity index 79% rename from components/esm/loadarmo.cpp rename to components/esm3/loadarmo.cpp index 929c111a98..6b9225d883 100644 --- a/components/esm/loadarmo.cpp +++ b/components/esm3/loadarmo.cpp @@ -2,7 +2,6 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { @@ -36,11 +35,10 @@ namespace ESM } } - unsigned int Armor::sRecordId = REC_ARMO; - void Armor::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mParts.mParts.clear(); @@ -49,35 +47,35 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'A','O','D','T'>::value: - esm.getHT(mData, 24); + case fourCC("AODT"): + esm.getHTSized<24>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEnchant = esm.getHString(); break; - case ESM::FourCC<'I','N','D','X'>::value: + case fourCC("INDX"): mParts.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -90,7 +88,7 @@ namespace ESM if (!hasName) esm.fail("Missing NAME subrecord"); if (!hasData && !isDeleted) - esm.fail("Missing CTDT subrecord"); + esm.fail("Missing AODT subrecord"); } void Armor::save(ESMWriter &esm, bool isDeleted) const @@ -99,7 +97,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -114,6 +112,7 @@ namespace ESM void Armor::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mWeight = 0; mData.mValue = 0; diff --git a/components/esm/loadarmo.hpp b/components/esm3/loadarmo.hpp similarity index 91% rename from components/esm/loadarmo.hpp rename to components/esm3/loadarmo.hpp index ef3bb734c0..3c16e9c6cf 100644 --- a/components/esm/loadarmo.hpp +++ b/components/esm3/loadarmo.hpp @@ -4,6 +4,8 @@ #include #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -65,9 +67,10 @@ struct PartReferenceList struct Armor { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_ARMO; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Armor"; } + static std::string_view getRecordType() { return "Armor"; } enum Type { @@ -94,6 +97,7 @@ struct Armor AODTstruct mData; PartReferenceList mParts; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript, mEnchant; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadbody.cpp b/components/esm3/loadbody.cpp similarity index 77% rename from components/esm/loadbody.cpp rename to components/esm3/loadbody.cpp index 3e5895c2c6..6b00d9a650 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm3/loadbody.cpp @@ -2,38 +2,36 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int BodyPart::sRecordId = REC_BODY; - void BodyPart::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mRace = esm.getHString(); break; - case ESM::FourCC<'B','Y','D','T'>::value: - esm.getHT(mData, 4); + case fourCC("BYDT"): + esm.getHTSized<4>(mData); hasData = true; break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -55,7 +53,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -66,6 +64,7 @@ namespace ESM void BodyPart::blank() { + mRecordFlags = 0; mData.mPart = 0; mData.mVampire = 0; mData.mFlags = 0; diff --git a/components/esm/loadbody.hpp b/components/esm3/loadbody.hpp similarity index 87% rename from components/esm/loadbody.hpp rename to components/esm3/loadbody.hpp index bf320330ff..7186b5bb52 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm3/loadbody.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_BODY_H #include +#include "components/esm/defs.hpp" namespace ESM { @@ -11,9 +12,10 @@ class ESMWriter; struct BodyPart { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_BODY; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "BodyPart"; } + static std::string_view getRecordType() { return "BodyPart"; } enum MeshPart { @@ -58,6 +60,7 @@ struct BodyPart }; BYDTstruct mData; + unsigned int mRecordFlags; std::string mId, mModel, mRace; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadbook.cpp b/components/esm3/loadbook.cpp similarity index 75% rename from components/esm/loadbook.cpp rename to components/esm3/loadbook.cpp index ee3c0c4af5..4bd04cd810 100644 --- a/components/esm/loadbook.cpp +++ b/components/esm3/loadbook.cpp @@ -2,50 +2,48 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Book::sRecordId = REC_BOOK; - void Book::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'B','K','D','T'>::value: - esm.getHT(mData, 20); + case fourCC("BKDT"): + esm.getHTSized<20>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEnchant = esm.getHString(); break; - case ESM::FourCC<'T','E','X','T'>::value: + case fourCC("TEXT"): mText = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -66,7 +64,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -81,6 +79,7 @@ namespace ESM void Book::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mIsScroll = 0; diff --git a/components/esm/loadbook.hpp b/components/esm3/loadbook.hpp similarity index 79% rename from components/esm/loadbook.hpp rename to components/esm3/loadbook.hpp index bb2d7912f7..0e4b83c7ea 100644 --- a/components/esm/loadbook.hpp +++ b/components/esm3/loadbook.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_BOOK_H #include +#include "components/esm/defs.hpp" namespace ESM { @@ -14,9 +15,10 @@ class ESMWriter; struct Book { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_BOOK; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Book"; } + static std::string_view getRecordType() { return "Book"; } struct BKDTstruct { @@ -26,6 +28,7 @@ struct Book BKDTstruct mData; std::string mName, mModel, mIcon, mScript, mEnchant, mText; + unsigned int mRecordFlags; std::string mId; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadbsgn.cpp b/components/esm3/loadbsgn.cpp similarity index 76% rename from components/esm/loadbsgn.cpp rename to components/esm3/loadbsgn.cpp index e3b2f76a3a..431a36a40f 100644 --- a/components/esm/loadbsgn.cpp +++ b/components/esm3/loadbsgn.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int BirthSign::sRecordId = REC_BSGN; - void BirthSign::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); @@ -18,25 +16,25 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'T','N','A','M'>::value: + case fourCC("TNAM"): mTexture = esm.getHString(); break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; - case ESM::FourCC<'N','P','C','S'>::value: + case fourCC("NPCS"): mPowers.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -56,7 +54,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } esm.writeHNOCString("FNAM", mName); @@ -68,6 +66,7 @@ namespace ESM void BirthSign::blank() { + mRecordFlags = 0; mName.clear(); mDescription.clear(); mTexture.clear(); diff --git a/components/esm/loadbsgn.hpp b/components/esm3/loadbsgn.hpp similarity index 76% rename from components/esm/loadbsgn.hpp rename to components/esm3/loadbsgn.hpp index 24d27a7f85..89832d0a43 100644 --- a/components/esm/loadbsgn.hpp +++ b/components/esm3/loadbsgn.hpp @@ -4,6 +4,7 @@ #include #include "spelllist.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -13,10 +14,12 @@ class ESMWriter; struct BirthSign { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_BSGN; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "BirthSign"; } + static std::string_view getRecordType() { return "BirthSign"; } + unsigned int mRecordFlags; std::string mId, mName, mDescription, mTexture; // List of powers and abilities that come with this birth sign. diff --git a/components/esm/loadcell.cpp b/components/esm3/loadcell.cpp similarity index 70% rename from components/esm/loadcell.cpp rename to components/esm3/loadcell.cpp index bf70aad96f..c8cb58b190 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm3/loadcell.cpp @@ -1,21 +1,22 @@ #include "loadcell.hpp" #include +#include #include -#include - +#include #include #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" #include "cellid.hpp" +namespace ESM +{ namespace { ///< Translate 8bit/24bit code (stored in refNum.mIndex) into a proper refNum - void adjustRefNum (ESM::RefNum& refNum, ESM::ESMReader& reader) + void adjustRefNum (RefNum& refNum, const ESMReader& reader) { unsigned int local = (refNum.mIndex & 0xff000000) >> 24; @@ -35,11 +36,10 @@ namespace } } } +} namespace ESM { - unsigned int Cell::sRecordId = REC_CELL; - // Some overloaded compare operators. bool operator== (const MovedCellRef& ref, const RefNum& refNum) { @@ -68,16 +68,16 @@ namespace ESM while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mName = esm.getHString(); break; - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mData, 12); + case fourCC("DATA"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -95,7 +95,7 @@ namespace ESM if (mCellId.mPaged) { - mCellId.mWorldspace = ESM::CellId::sDefaultWorldspace; + mCellId.mWorldspace = CellId::sDefaultWorldspace; mCellId.mIndex.mX = mData.mX; mCellId.mIndex.mY = mData.mY; } @@ -109,34 +109,44 @@ namespace ESM void Cell::loadCell(ESMReader &esm, bool saveContext) { + bool overriding = !mName.empty(); bool isLoaded = false; mHasAmbi = false; while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'I','N','T','V'>::value: + case fourCC("INTV"): int waterl; esm.getHT(waterl); mWater = static_cast(waterl); mWaterInt = true; break; - case ESM::FourCC<'W','H','G','T'>::value: - esm.getHT(mWater); + case fourCC("WHGT"): + float waterLevel; + esm.getHT(waterLevel); mWaterInt = false; + if(!std::isfinite(waterLevel)) + { + if(!overriding) + mWater = std::numeric_limits::max(); + Log(Debug::Warning) << "Warning: Encountered invalid water level in cell " << mName << " defined in " << esm.getContext().filename; + } + else + mWater = waterLevel; break; - case ESM::FourCC<'A','M','B','I'>::value: + case fourCC("AMBI"): esm.getHT(mAmbi); mHasAmbi = true; break; - case ESM::FourCC<'R','G','N','N'>::value: + case fourCC("RGNN"): mRegion = esm.getHString(); break; - case ESM::FourCC<'N','A','M','5'>::value: + case fourCC("NAM5"): esm.getHT(mMapColor); break; - case ESM::FourCC<'N','A','M','0'>::value: + case fourCC("NAM0"): esm.getHT(mRefNumCounter); break; default: @@ -146,7 +156,7 @@ namespace ESM } } - if (saveContext) + if (saveContext) { mContextList.push_back(esm.getContext()); esm.skipRecord(); @@ -167,7 +177,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -197,9 +207,12 @@ namespace ESM if (mMapColor != 0) esm.writeHNT("NAM5", mMapColor); } + } - if (mRefNumCounter != 0) - esm.writeHNT("NAM0", mRefNumCounter); + void Cell::saveTempMarker(ESMWriter &esm, int tempCount) const + { + if (tempCount != 0) + esm.writeHNT("NAM0", tempCount); } void Cell::restore(ESMReader &esm, int iCtx) const @@ -221,7 +234,7 @@ namespace ESM return region + ' ' + cellGrid; } - bool Cell::getNextRef(ESMReader &esm, CellRef &ref, bool &isDeleted, bool ignoreMoves, MovedCellRef *mref) + bool Cell::getNextRef(ESMReader& esm, CellRef& ref, bool& isDeleted) { isDeleted = false; @@ -229,28 +242,26 @@ namespace ESM if (!esm.hasMoreSubs()) return false; - // NOTE: We should not need this check. It is a safety check until we have checked - // more plugins, and how they treat these moved references. - if (esm.isNextSub("MVRF")) + // MVRF are FRMR are present in pairs. MVRF indicates that following FRMR describes moved CellRef. + // This function has to skip all moved CellRefs therefore read all such pairs to ignored values. + while (esm.isNextSub("MVRF")) { - if (ignoreMoves) - { - esm.getHT (mref->mRefNum.mIndex); - esm.getHNOT (mref->mTarget, "CNDT"); - adjustRefNum (mref->mRefNum, esm); - } - else - { - // skip rest of cell record (moved references), they are handled elsewhere - esm.skipRecord(); // skip MVRF, CNDT + MovedCellRef movedCellRef; + esm.getHT(movedCellRef.mRefNum.mIndex); + esm.getHNOT(movedCellRef.mTarget, "CNDT"); + CellRef skippedCellRef; + if (!esm.peekNextSub("FRMR")) return false; - } + bool skippedDeleted; + skippedCellRef.load(esm, skippedDeleted); } if (esm.peekNextSub("FRMR")) { ref.load (esm, isDeleted); + // TODO: should count the number of temp refs and validate the number + // Identify references belonging to a parent file and adapt the ID accordingly. adjustRefNum (ref.mRefNum, esm); return true; @@ -258,6 +269,37 @@ namespace ESM return false; } + bool Cell::getNextRef(ESMReader& esm, CellRef& cellRef, bool& deleted, MovedCellRef& movedCellRef, bool& moved, + GetNextRefMode mode) + { + deleted = false; + moved = false; + + if (!esm.hasMoreSubs()) + return false; + + if (esm.isNextSub("MVRF")) + { + moved = true; + getNextMVRF(esm, movedCellRef); + } + + if (!esm.peekNextSub("FRMR")) + return false; + + if ((!moved && mode == GetNextRefMode::LoadOnlyMoved) + || (moved && mode == GetNextRefMode::LoadOnlyNotMoved)) + { + skipLoadCellRef(esm); + return true; + } + + cellRef.load(esm, deleted); + adjustRefNum(cellRef.mRefNum, esm); + + return true; + } + bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) { esm.getHT(mref.mRefNum.mIndex); diff --git a/components/esm/loadcell.hpp b/components/esm3/loadcell.hpp similarity index 90% rename from components/esm/loadcell.hpp rename to components/esm3/loadcell.hpp index 132c869ad5..41876dfc6f 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm3/loadcell.hpp @@ -5,8 +5,8 @@ #include #include -#include "esmcommon.hpp" -#include "defs.hpp" +#include "components/esm/esmcommon.hpp" +#include "components/esm/defs.hpp" #include "cellref.hpp" #include "cellid.hpp" @@ -63,9 +63,17 @@ struct CellRefTrackerPredicate */ struct Cell { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_CELL; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Cell"; } + static std::string_view getRecordType() { return "Cell"; } + + enum class GetNextRefMode + { + LoadAll, + LoadOnlyMoved, + LoadOnlyNotMoved, + }; enum Flags { @@ -133,6 +141,7 @@ struct Cell void loadCell(ESMReader &esm, bool saveContext = true); // Load everything, except NAME, DATAstruct and references void save(ESMWriter &esm, bool isDeleted = false) const; + void saveTempMarker(ESMWriter &esm, int tempCount) const; bool isExterior() const { @@ -180,12 +189,10 @@ struct Cell All fields of the CellRef struct are overwritten. You can safely reuse one memory location without blanking it between calls. */ - /// \param ignoreMoves ignore MVRF record and read reference like a regular CellRef. - static bool getNextRef(ESMReader &esm, - CellRef &ref, - bool &isDeleted, - bool ignoreMoves = false, - MovedCellRef *mref = 0); + static bool getNextRef(ESMReader& esm, CellRef& ref, bool& deleted); + + static bool getNextRef(ESMReader& esm, CellRef& cellRef, bool& deleted, MovedCellRef& movedCellRef, bool& moved, + GetNextRefMode mode = GetNextRefMode::LoadAll); /* This fetches an MVRF record, which is used to track moved references. * Since they are comparably rare, we use a separate method for this. diff --git a/components/esm/loadclas.cpp b/components/esm3/loadclas.cpp similarity index 85% rename from components/esm/loadclas.cpp rename to components/esm3/loadclas.cpp index 1e18bc0540..2166382013 100644 --- a/components/esm/loadclas.cpp +++ b/components/esm3/loadclas.cpp @@ -4,12 +4,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - unsigned int Class::sRecordId = REC_CLAS; - const Class::Specialization Class::sSpecializationIds[3] = { Class::Combat, Class::Magic, @@ -41,31 +39,32 @@ namespace ESM void Class::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'C','L','D','T'>::value: - esm.getHT(mData, 60); + case fourCC("CLDT"): + esm.getHTSized<60>(mData); if (mData.mIsPlayable > 1) esm.fail("Unknown bool value"); hasData = true; break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -86,7 +85,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -97,6 +96,7 @@ namespace ESM void Class::blank() { + mRecordFlags = 0; mName.clear(); mDescription.clear(); diff --git a/components/esm/loadclas.hpp b/components/esm3/loadclas.hpp similarity index 91% rename from components/esm/loadclas.hpp rename to components/esm3/loadclas.hpp index 833dd6757d..ce22622975 100644 --- a/components/esm/loadclas.hpp +++ b/components/esm3/loadclas.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -17,9 +19,10 @@ class ESMWriter; // class struct Class { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_CLAS; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Class"; } + static std::string_view getRecordType() { return "Class"; } enum AutoCalc { @@ -70,6 +73,7 @@ struct Class ///< Throws an exception for invalid values of \a index. }; // 60 bytes + unsigned int mRecordFlags; std::string mId, mName, mDescription; CLDTstruct mData; diff --git a/components/esm/loadclot.cpp b/components/esm3/loadclot.cpp similarity index 75% rename from components/esm/loadclot.cpp rename to components/esm3/loadclot.cpp index 12f0d495d5..a9a4121997 100644 --- a/components/esm/loadclot.cpp +++ b/components/esm3/loadclot.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Clothing::sRecordId = REC_CLOT; - void Clothing::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mParts.mParts.clear(); @@ -19,35 +17,35 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'C','T','D','T'>::value: - esm.getHT(mData, 12); + case fourCC("CTDT"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEnchant = esm.getHString(); break; - case ESM::FourCC<'I','N','D','X'>::value: + case fourCC("INDX"): mParts.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -69,7 +67,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -87,6 +85,7 @@ namespace ESM void Clothing::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mWeight = 0; mData.mValue = 0; diff --git a/components/esm/loadclot.hpp b/components/esm3/loadclot.hpp similarity index 83% rename from components/esm/loadclot.hpp rename to components/esm3/loadclot.hpp index 4db791c0c1..d007e7f76e 100644 --- a/components/esm/loadclot.hpp +++ b/components/esm3/loadclot.hpp @@ -4,6 +4,7 @@ #include #include "loadarmo.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -17,9 +18,10 @@ class ESMWriter; struct Clothing { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_CLOT; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Clothing"; } + static std::string_view getRecordType() { return "Clothing"; } enum Type { @@ -46,6 +48,7 @@ struct Clothing PartReferenceList mParts; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mEnchant, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadcont.cpp b/components/esm3/loadcont.cpp similarity index 80% rename from components/esm/loadcont.cpp rename to components/esm3/loadcont.cpp index 107aea7cf6..21ed46310a 100644 --- a/components/esm/loadcont.cpp +++ b/components/esm3/loadcont.cpp @@ -2,7 +2,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -27,11 +27,10 @@ namespace ESM } } - unsigned int Container::sRecordId = REC_CONT; - void Container::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mInventory.mList.clear(); @@ -41,37 +40,37 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'C','N','D','T'>::value: - esm.getHT(mWeight, 4); + case fourCC("CNDT"): + esm.getHTSized<4>(mWeight); hasWeight = true; break; - case ESM::FourCC<'F','L','A','G'>::value: - esm.getHT(mFlags, 4); + case fourCC("FLAG"): + esm.getHTSized<4>(mFlags); if (mFlags & 0xf4) esm.fail("Unknown flags"); if (!(mFlags & 0x8)) esm.fail("Flag 8 not set"); hasFlags = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'N','P','C','O'>::value: + case fourCC("NPCO"): mInventory.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -95,7 +94,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -111,6 +110,7 @@ namespace ESM void Container::blank() { + mRecordFlags = 0; mName.clear(); mModel.clear(); mScript.clear(); diff --git a/components/esm/loadcont.hpp b/components/esm3/loadcont.hpp similarity index 81% rename from components/esm/loadcont.hpp rename to components/esm3/loadcont.hpp index 0cac580748..0a28a89015 100644 --- a/components/esm/loadcont.hpp +++ b/components/esm3/loadcont.hpp @@ -4,7 +4,8 @@ #include #include -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -18,7 +19,7 @@ class ESMWriter; struct ContItem { - int mCount; + int mCount{0}; std::string mItem; }; @@ -35,9 +36,10 @@ struct InventoryList struct Container { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_CONT; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Container"; } + static std::string_view getRecordType() { return "Container"; } enum Flags { @@ -46,6 +48,7 @@ struct Container Unknown = 8 }; + unsigned int mRecordFlags; std::string mId, mName, mModel, mScript; float mWeight; // Not sure, might be max total weight allowed? diff --git a/components/esm/loadcrea.cpp b/components/esm3/loadcrea.cpp similarity index 79% rename from components/esm/loadcrea.cpp rename to components/esm3/loadcrea.cpp index 52138e2232..5f44f0f655 100644 --- a/components/esm/loadcrea.cpp +++ b/components/esm3/loadcrea.cpp @@ -4,17 +4,14 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" -namespace ESM { - - unsigned int Creature::sRecordId = REC_CREA; +namespace ESM +{ void Creature::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; - - mPersistent = (esm.getRecordFlags() & 0x0400) != 0; + mRecordFlags = esm.getRecordFlags(); mAiPackage.mList.clear(); mInventory.mList.clear(); @@ -32,49 +29,49 @@ namespace ESM { while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'C','N','A','M'>::value: + case fourCC("CNAM"): mOriginal = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'N','P','D','T'>::value: - esm.getHT(mData, 96); + case fourCC("NPDT"): + esm.getHTSized<96>(mData); hasNpdt = true; break; - case ESM::FourCC<'F','L','A','G'>::value: + case fourCC("FLAG"): int flags; esm.getHT(flags); mFlags = flags & 0xFF; mBloodType = ((flags >> 8) & 0xFF) >> 2; hasFlags = true; break; - case ESM::FourCC<'X','S','C','L'>::value: + case fourCC("XSCL"): esm.getHT(mScale); break; - case ESM::FourCC<'N','P','C','O'>::value: + case fourCC("NPCO"): mInventory.add(esm); break; - case ESM::FourCC<'N','P','C','S'>::value: + case fourCC("NPCS"): mSpells.add(esm); break; - case ESM::FourCC<'A','I','D','T'>::value: + case fourCC("AIDT"): esm.getHExact(&mAiData, sizeof(mAiData)); break; - case ESM::FourCC<'D','O','D','T'>::value: - case ESM::FourCC<'D','N','A','M'>::value: + case fourCC("DODT"): + case fourCC("DNAM"): mTransport.add(esm); break; case AI_Wander: @@ -85,11 +82,11 @@ namespace ESM { case AI_CNDT: mAiPackage.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; - case ESM::FourCC<'I','N','D','X'>::value: + case fourCC("INDX"): // seems to occur only in .ESS files, unsure of purpose int index; esm.getHT(index); @@ -115,7 +112,7 @@ namespace ESM { if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -138,6 +135,7 @@ namespace ESM { void Creature::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mLevel = 0; mData.mStrength = mData.mIntelligence = mData.mWillpower = mData.mAgility = diff --git a/components/esm/loadcrea.hpp b/components/esm3/loadcrea.hpp similarity index 92% rename from components/esm/loadcrea.hpp rename to components/esm3/loadcrea.hpp index 0ab09ee122..e5734e9b32 100644 --- a/components/esm/loadcrea.hpp +++ b/components/esm3/loadcrea.hpp @@ -8,6 +8,8 @@ #include "aipackage.hpp" #include "transport.hpp" +#include "components/esm/defs.hpp" + namespace ESM { @@ -21,9 +23,10 @@ class ESMWriter; struct Creature { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_CREA; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Creature"; } + static std::string_view getRecordType() { return "Creature"; } // Default is 0x48? enum Flags @@ -76,10 +79,9 @@ struct Creature int mBloodType; unsigned char mFlags; - bool mPersistent; - float mScale; + unsigned int mRecordFlags; std::string mId, mModel, mName, mScript; std::string mOriginal; // Base creature that this is a modification of diff --git a/components/esm/loaddial.cpp b/components/esm3/loaddial.cpp similarity index 67% rename from components/esm/loaddial.cpp rename to components/esm3/loaddial.cpp index d7e0a6ee17..f1c102250d 100644 --- a/components/esm/loaddial.cpp +++ b/components/esm3/loaddial.cpp @@ -4,11 +4,9 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Dialogue::sRecordId = REC_DIAL; void Dialogue::load(ESMReader &esm, bool &isDeleted) { @@ -28,9 +26,9 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'D','A','T','A'>::value: + case fourCC("DATA"): { esm.getSubHeader(); int size = esm.getSubSize(); @@ -44,7 +42,7 @@ namespace ESM } break; } - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); mType = Unknown; isDeleted = true; @@ -61,7 +59,7 @@ namespace ESM esm.writeHNCString("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); } else { @@ -76,7 +74,7 @@ namespace ESM void Dialogue::readInfo(ESMReader &esm, bool merge) { - ESM::DialInfo info; + DialInfo info; bool isDeleted = false; info.load(esm, isDeleted); @@ -86,49 +84,30 @@ namespace ESM return; } - InfoContainer::iterator it = mInfo.end(); - - LookupMap::iterator lookup; - lookup = mLookup.find(info.mId); + LookupMap::iterator lookup = mLookup.find(info.mId); if (lookup != mLookup.end()) { - it = lookup->second.first; + auto it = lookup->second.first; // Since the new version of this record may have changed the next/prev linked list connection, we need to re-insert the record mInfo.erase(it); mLookup.erase(lookup); } - if (info.mNext.empty()) - { - mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.end(), info), isDeleted); - return; - } - if (info.mPrev.empty()) - { - mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.begin(), info), isDeleted); - return; - } - - lookup = mLookup.find(info.mPrev); - if (lookup != mLookup.end()) + if (!info.mPrev.empty()) { - it = lookup->second.first; - - mLookup[info.mId] = std::make_pair(mInfo.insert(++it, info), isDeleted); - return; - } - - lookup = mLookup.find(info.mNext); - if (lookup != mLookup.end()) - { - it = lookup->second.first; + lookup = mLookup.find(info.mPrev); + if (lookup != mLookup.end()) + { + auto it = lookup->second.first; - mLookup[info.mId] = std::make_pair(mInfo.insert(it, info), isDeleted); - return; + mLookup[info.mId] = std::make_pair(mInfo.insert(++it, info), isDeleted); + } + else + mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.end(), info), isDeleted); } - - Log(Debug::Warning) << "Warning: Failed to insert info " << info.mId; + else + mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.begin(), info), isDeleted); } void Dialogue::clearDeletedInfos() diff --git a/components/esm/loaddial.hpp b/components/esm3/loaddial.hpp similarity index 89% rename from components/esm/loaddial.hpp rename to components/esm3/loaddial.hpp index b80cbd74c3..8f02c00425 100644 --- a/components/esm/loaddial.hpp +++ b/components/esm3/loaddial.hpp @@ -5,6 +5,7 @@ #include #include +#include "components/esm/defs.hpp" #include "loadinfo.hpp" namespace ESM @@ -20,9 +21,9 @@ class ESMWriter; struct Dialogue { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_DIAL; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Dialogue"; } + static std::string_view getRecordType() { return "Dialogue"; } enum Type { @@ -61,7 +62,7 @@ struct Dialogue /// Read the next info record /// @param merge Merge with existing list, or just push each record to the end of the list? - void readInfo (ESM::ESMReader& esm, bool merge); + void readInfo (ESMReader& esm, bool merge); void blank(); ///< Set record to default state (does not touch the ID and does not change the type). diff --git a/components/esm/loaddoor.cpp b/components/esm3/loaddoor.cpp similarity index 76% rename from components/esm/loaddoor.cpp rename to components/esm3/loaddoor.cpp index 523d8a1efc..a1f0b16a5f 100644 --- a/components/esm/loaddoor.cpp +++ b/components/esm3/loaddoor.cpp @@ -2,42 +2,40 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Door::sRecordId = REC_DOOR; - void Door::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'S','N','A','M'>::value: + case fourCC("SNAM"): mOpenSound = esm.getHString(); break; - case ESM::FourCC<'A','N','A','M'>::value: + case fourCC("ANAM"): mCloseSound = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -57,7 +55,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -70,6 +68,7 @@ namespace ESM void Door::blank() { + mRecordFlags = 0; mName.clear(); mModel.clear(); mScript.clear(); diff --git a/components/esm/loaddoor.hpp b/components/esm3/loaddoor.hpp similarity index 73% rename from components/esm/loaddoor.hpp rename to components/esm3/loaddoor.hpp index 3afe5d5e4b..37b8ea85a9 100644 --- a/components/esm/loaddoor.hpp +++ b/components/esm3/loaddoor.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -11,10 +13,12 @@ class ESMWriter; struct Door { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_DOOR; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Door"; } + static std::string_view getRecordType() { return "Door"; } + unsigned int mRecordFlags; std::string mId, mName, mModel, mScript, mOpenSound, mCloseSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadench.cpp b/components/esm3/loadench.cpp similarity index 78% rename from components/esm/loadench.cpp rename to components/esm3/loadench.cpp index 8c4dd8c644..32a65b35aa 100644 --- a/components/esm/loadench.cpp +++ b/components/esm3/loadench.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Enchantment::sRecordId = REC_ENCH; - void Enchantment::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); bool hasName = false; @@ -18,20 +16,20 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'E','N','D','T'>::value: - esm.getHT(mData, 16); + case fourCC("ENDT"): + esm.getHTSized<16>(mData); hasData = true; break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEffects.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -53,7 +51,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -63,6 +61,7 @@ namespace ESM void Enchantment::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mCost = 0; mData.mCharge = 0; diff --git a/components/esm/loadench.hpp b/components/esm3/loadench.hpp similarity index 81% rename from components/esm/loadench.hpp rename to components/esm3/loadench.hpp index b98549ef35..d20cf5a593 100644 --- a/components/esm/loadench.hpp +++ b/components/esm3/loadench.hpp @@ -3,6 +3,7 @@ #include +#include "components/esm/defs.hpp" #include "effectlist.hpp" namespace ESM @@ -17,9 +18,10 @@ class ESMWriter; struct Enchantment { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_ENCH; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Enchantment"; } + static std::string_view getRecordType() { return "Enchantment"; } enum Type { @@ -42,6 +44,7 @@ struct Enchantment int mFlags; }; + unsigned int mRecordFlags; std::string mId; ENDTstruct mData; EffectList mEffects; diff --git a/components/esm/loadfact.cpp b/components/esm3/loadfact.cpp similarity index 86% rename from components/esm/loadfact.cpp rename to components/esm3/loadfact.cpp index ff2fbf66d1..350cfa3891 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm3/loadfact.cpp @@ -4,12 +4,9 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Faction::sRecordId = REC_FACT; - int& Faction::FADTstruct::getSkill (int index, bool ignored) { if (index<0 || index>=7) @@ -29,6 +26,7 @@ namespace ESM void Faction::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mReactions.clear(); for (int i=0;i<10;++i) @@ -40,27 +38,27 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'R','N','A','M'>::value: + case fourCC("RNAM"): if (rankCounter >= 10) esm.fail("Rank out of range"); mRanks[rankCounter++] = esm.getHString(); break; - case ESM::FourCC<'F','A','D','T'>::value: - esm.getHT(mData, 240); + case fourCC("FADT"): + esm.getHTSized<240>(mData); if (mData.mIsHidden > 1) esm.fail("Unknown flag!"); hasData = true; break; - case ESM::FourCC<'A','N','A','M'>::value: + case fourCC("ANAM"): { std::string faction = esm.getHString(); int reaction; @@ -68,7 +66,7 @@ namespace ESM mReactions[faction] = reaction; break; } - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -90,7 +88,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -115,6 +113,7 @@ namespace ESM void Faction::blank() { + mRecordFlags = 0; mName.clear(); mData.mAttribute[0] = mData.mAttribute[1] = 0; mData.mIsHidden = 0; diff --git a/components/esm/loadfact.hpp b/components/esm3/loadfact.hpp similarity index 85% rename from components/esm/loadfact.hpp rename to components/esm3/loadfact.hpp index 098ed43096..ec2df70c0e 100644 --- a/components/esm/loadfact.hpp +++ b/components/esm3/loadfact.hpp @@ -4,6 +4,8 @@ #include #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -30,10 +32,12 @@ struct RankData struct Faction { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_FACT; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Faction"; } + static std::string_view getRecordType() { return "Faction"; } + unsigned int mRecordFlags; std::string mId, mName; struct FADTstruct @@ -44,7 +48,7 @@ struct Faction RankData mRankData[10]; int mSkills[7]; // IDs of skills this faction require - // Each element will either contain an ESM::Skill index, or -1. + // Each element will either contain an Skill index, or -1. int mIsHidden; // 1 - hidden from player diff --git a/components/esm/loadglob.cpp b/components/esm3/loadglob.cpp similarity index 77% rename from components/esm/loadglob.cpp rename to components/esm3/loadglob.cpp index 72ecce503c..595d31c85d 100644 --- a/components/esm/loadglob.cpp +++ b/components/esm3/loadglob.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Global::sRecordId = REC_GLOB; - void Global::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString ("NAME"); @@ -21,7 +19,7 @@ namespace ESM } else { - mValue.read (esm, ESM::Variant::Format_Global); + mValue.read (esm, Variant::Format_Global); } } @@ -35,13 +33,14 @@ namespace ESM } else { - mValue.write (esm, ESM::Variant::Format_Global); + mValue.write (esm, Variant::Format_Global); } } void Global::blank() { - mValue.setType (ESM::VT_None); + mRecordFlags = 0; + mValue.setType (VT_None); } bool operator== (const Global& left, const Global& right) diff --git a/components/esm/loadglob.hpp b/components/esm3/loadglob.hpp similarity index 76% rename from components/esm/loadglob.hpp rename to components/esm3/loadglob.hpp index 0533cc95ea..138d64e765 100644 --- a/components/esm/loadglob.hpp +++ b/components/esm3/loadglob.hpp @@ -5,6 +5,8 @@ #include "variant.hpp" +#include "components/esm/defs.hpp" + namespace ESM { @@ -17,10 +19,12 @@ class ESMWriter; struct Global { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_GLOB; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Global"; } + static std::string_view getRecordType() { return "Global"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadgmst.cpp b/components/esm3/loadgmst.cpp similarity index 73% rename from components/esm/loadgmst.cpp rename to components/esm3/loadgmst.cpp index da8d256e7d..832ba80f63 100644 --- a/components/esm/loadgmst.cpp +++ b/components/esm3/loadgmst.cpp @@ -2,29 +2,28 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int GameSetting::sRecordId = REC_GMST; - void GameSetting::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; // GameSetting record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString("NAME"); - mValue.read (esm, ESM::Variant::Format_Gmst); + mValue.read (esm, Variant::Format_Gmst); } void GameSetting::save (ESMWriter &esm, bool /*isDeleted*/) const { esm.writeHNCString("NAME", mId); - mValue.write (esm, ESM::Variant::Format_Gmst); + mValue.write (esm, Variant::Format_Gmst); } void GameSetting::blank() { - mValue.setType (ESM::VT_None); + mRecordFlags = 0; + mValue.setType (VT_None); } bool operator== (const GameSetting& left, const GameSetting& right) diff --git a/components/esm/loadgmst.hpp b/components/esm3/loadgmst.hpp similarity index 76% rename from components/esm/loadgmst.hpp rename to components/esm3/loadgmst.hpp index c40d348fe4..da5a1baea6 100644 --- a/components/esm/loadgmst.hpp +++ b/components/esm3/loadgmst.hpp @@ -5,6 +5,8 @@ #include "variant.hpp" +#include "components/esm/defs.hpp" + namespace ESM { @@ -18,10 +20,12 @@ class ESMWriter; struct GameSetting { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_GMST; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "GameSetting"; } + static std::string_view getRecordType() { return "GameSetting"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadinfo.cpp b/components/esm3/loadinfo.cpp similarity index 75% rename from components/esm/loadinfo.cpp rename to components/esm3/loadinfo.cpp index 38bb163e61..366582c10b 100644 --- a/components/esm/loadinfo.cpp +++ b/components/esm3/loadinfo.cpp @@ -2,12 +2,9 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int DialInfo::sRecordId = REC_INFO; - void DialInfo::load(ESMReader &esm, bool &isDeleted) { mId = esm.getHNString("INAM"); @@ -23,21 +20,21 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mData, 12); + case fourCC("DATA"): + esm.getHTSized<12>(mData); break; - case ESM::FourCC<'O','N','A','M'>::value: + case fourCC("ONAM"): mActor = esm.getHString(); break; - case ESM::FourCC<'R','N','A','M'>::value: + case fourCC("RNAM"): mRace = esm.getHString(); break; - case ESM::FourCC<'C','N','A','M'>::value: + case fourCC("CNAM"): mClass = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): { mFaction = esm.getHString(); if (mFaction == "FFFF") @@ -46,19 +43,19 @@ namespace ESM } break; } - case ESM::FourCC<'A','N','A','M'>::value: + case fourCC("ANAM"): mCell = esm.getHString(); break; - case ESM::FourCC<'D','N','A','M'>::value: + case fourCC("DNAM"): mPcFaction = esm.getHString(); break; - case ESM::FourCC<'S','N','A','M'>::value: + case fourCC("SNAM"): mSound = esm.getHString(); break; - case ESM::SREC_NAME: + case SREC_NAME: mResponse = esm.getHString(); break; - case ESM::FourCC<'S','C','V','R'>::value: + case fourCC("SCVR"): { SelectStruct ss; ss.mSelectRule = esm.getHString(); @@ -66,22 +63,22 @@ namespace ESM mSelects.push_back(ss); break; } - case ESM::FourCC<'B','N','A','M'>::value: + case fourCC("BNAM"): mResultScript = esm.getHString(); break; - case ESM::FourCC<'Q','S','T','N'>::value: + case fourCC("QSTN"): mQuestStatus = QS_Name; esm.skipRecord(); break; - case ESM::FourCC<'Q','S','T','F'>::value: + case fourCC("QSTF"): mQuestStatus = QS_Finished; esm.skipRecord(); break; - case ESM::FourCC<'Q','S','T','R'>::value: + case fourCC("QSTR"): mQuestStatus = QS_Restart; esm.skipRecord(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -100,7 +97,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -133,12 +130,7 @@ namespace ESM void DialInfo::blank() { - mData.mUnknown1 = 0; - mData.mDisposition = 0; - mData.mRank = 0; - mData.mGender = 0; - mData.mPCrank = 0; - mData.mUnknown2 = 0; + mData = {}; mSelects.clear(); mPrev.clear(); diff --git a/components/esm/loadinfo.hpp b/components/esm3/loadinfo.hpp similarity index 85% rename from components/esm/loadinfo.hpp rename to components/esm3/loadinfo.hpp index 2fbc782ec9..088ce074aa 100644 --- a/components/esm/loadinfo.hpp +++ b/components/esm3/loadinfo.hpp @@ -4,7 +4,7 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" #include "variant.hpp" namespace ESM @@ -20,9 +20,10 @@ class ESMWriter; struct DialInfo { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_INFO; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "DialInfo"; } + static std::string_view getRecordType() { return "DialInfo"; } enum Gender { @@ -33,16 +34,16 @@ struct DialInfo struct DATAstruct { - int mUnknown1; + int mUnknown1 = 0; union { - int mDisposition; // Used for dialogue responses + int mDisposition = 0; // Used for dialogue responses int mJournalIndex; // Used for journal entries }; - signed char mRank; // Rank of NPC - signed char mGender; // See Gender enum - signed char mPCrank; // Player rank - signed char mUnknown2; + signed char mRank = -1; // Rank of NPC + signed char mGender = Gender::NA; // See Gender enum + signed char mPCrank = -1; // Player rank + signed char mUnknown2 = 0; }; // 12 bytes DATAstruct mData; diff --git a/components/esm/loadingr.cpp b/components/esm3/loadingr.cpp similarity index 82% rename from components/esm/loadingr.cpp rename to components/esm3/loadingr.cpp index a18e321ff7..0bb2cdd6d7 100644 --- a/components/esm/loadingr.cpp +++ b/components/esm3/loadingr.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Ingredient::sRecordId = REC_INGR; - void Ingredient::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'I','R','D','T'>::value: - esm.getHT(mData, 56); + case fourCC("IRDT"): + esm.getHTSized<56>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -84,7 +82,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -97,6 +95,7 @@ namespace ESM void Ingredient::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; for (int i=0; i<4; ++i) diff --git a/components/esm/loadingr.hpp b/components/esm3/loadingr.hpp similarity index 80% rename from components/esm/loadingr.hpp rename to components/esm3/loadingr.hpp index c0f4450238..587a342407 100644 --- a/components/esm/loadingr.hpp +++ b/components/esm3/loadingr.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -15,9 +17,10 @@ class ESMWriter; struct Ingredient { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_INGR; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Ingredient"; } + static std::string_view getRecordType() { return "Ingredient"; } struct IRDTstruct { @@ -29,6 +32,7 @@ struct Ingredient }; IRDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadland.cpp b/components/esm3/loadland.cpp similarity index 88% rename from components/esm/loadland.cpp rename to components/esm3/loadland.cpp index 2aa8f21dbe..e9012a92f0 100644 --- a/components/esm/loadland.cpp +++ b/components/esm3/loadland.cpp @@ -5,17 +5,14 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - unsigned int Land::sRecordId = REC_LAND; - Land::Land() : mFlags(0) , mX(0) , mY(0) - , mPlugin(0) , mDataTypes(0) , mLandData(nullptr) { @@ -40,25 +37,25 @@ namespace ESM { isDeleted = false; - mPlugin = esm.getIndex(); - bool hasLocation = false; bool isLoaded = false; while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'I','N','T','V'>::value: - esm.getSubHeaderIs(8); + case fourCC("INTV"): + esm.getSubHeader(); + if (esm.getSubSize() != 8) + esm.fail("Subrecord size is not equal to 8"); esm.getT(mX); esm.getT(mY); hasLocation = true; break; - case ESM::FourCC<'D','A','T','A'>::value: + case fourCC("DATA"): esm.getHT(mFlags); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -81,25 +78,25 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'V','N','M','L'>::value: + case fourCC("VNML"): esm.skipHSub(); mDataTypes |= DATA_VNML; break; - case ESM::FourCC<'V','H','G','T'>::value: + case fourCC("VHGT"): esm.skipHSub(); mDataTypes |= DATA_VHGT; break; - case ESM::FourCC<'W','N','A','M'>::value: + case fourCC("WNAM"): esm.getHExact(mWnam, sizeof(mWnam)); mDataTypes |= DATA_WNAM; break; - case ESM::FourCC<'V','C','L','R'>::value: + case fourCC("VCLR"): esm.skipHSub(); mDataTypes |= DATA_VCLR; break; - case ESM::FourCC<'V','T','E','X'>::value: + case fourCC("VTEX"): esm.skipHSub(); mDataTypes |= DATA_VTEX; break; @@ -121,7 +118,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -161,16 +158,16 @@ namespace ESM { // Generate WNAM record signed char wnam[LAND_GLOBAL_MAP_LOD_SIZE]; - float max = std::numeric_limits::max(); - float min = std::numeric_limits::min(); - float vertMult = static_cast(ESM::Land::LAND_SIZE - 1) / LAND_GLOBAL_MAP_LOD_SIZE_SQRT; + constexpr float max = std::numeric_limits::max(); + constexpr float min = std::numeric_limits::min(); + constexpr float vertMult = static_cast(Land::LAND_SIZE - 1) / LAND_GLOBAL_MAP_LOD_SIZE_SQRT; for (int row = 0; row < LAND_GLOBAL_MAP_LOD_SIZE_SQRT; ++row) { for (int col = 0; col < LAND_GLOBAL_MAP_LOD_SIZE_SQRT; ++col) { - float height = mLandData->mHeights[int(row * vertMult) * ESM::Land::LAND_SIZE + int(col * vertMult)]; + float height = mLandData->mHeights[int(row * vertMult) * Land::LAND_SIZE + int(col * vertMult)]; height /= height > 0 ? 128.f : 16.f; - height = std::min(max, std::max(min, height)); + height = std::clamp(height, min, max); wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast(height); } } @@ -190,7 +187,7 @@ namespace ESM void Land::blank() { - mPlugin = 0; + setPlugin(0); std::fill(std::begin(mWnam), std::end(mWnam), 0); @@ -247,7 +244,7 @@ namespace ESM return; } - ESM::ESMReader reader; + ESMReader reader; reader.restoreContext(mContext); if (reader.isNextSub("VNML")) { @@ -307,7 +304,7 @@ namespace ESM } } - bool Land::condLoad(ESM::ESMReader& reader, int flags, int& targetFlags, int dataFlag, void *ptr, unsigned int size) const + bool Land::condLoad(ESMReader& reader, int flags, int& targetFlags, int dataFlag, void *ptr, unsigned int size) const { if ((targetFlags & dataFlag) == 0 && (flags & dataFlag) != 0) { reader.getHExact(ptr, size); @@ -324,7 +321,7 @@ namespace ESM } Land::Land (const Land& land) - : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mPlugin (land.mPlugin), + : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mContext (land.mContext), mDataTypes (land.mDataTypes), mLandData (land.mLandData ? new LandData (*land.mLandData) : nullptr) { @@ -343,7 +340,6 @@ namespace ESM std::swap (mFlags, land.mFlags); std::swap (mX, land.mX); std::swap (mY, land.mY); - std::swap (mPlugin, land.mPlugin); std::swap (mContext, land.mContext); std::swap (mDataTypes, land.mDataTypes); std::swap (mLandData, land.mLandData); diff --git a/components/esm/loadland.hpp b/components/esm3/loadland.hpp similarity index 80% rename from components/esm/loadland.hpp rename to components/esm3/loadland.hpp index 2a1140ad2e..bb71350b4f 100644 --- a/components/esm/loadland.hpp +++ b/components/esm3/loadland.hpp @@ -5,7 +5,8 @@ #include -#include "esmcommon.hpp" +#include "components/esm/defs.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -19,9 +20,10 @@ class ESMWriter; struct Land { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_LAND; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Land"; } + static std::string_view getRecordType() { return "Land"; } Land(); ~Land(); @@ -29,7 +31,10 @@ struct Land int mFlags; // Only first four bits seem to be used, don't know what // they mean. int mX, mY; // Map coordinates. - int mPlugin; // Plugin index, used to reference the correct material palette. + + // Plugin index, used to reference the correct material palette. + int getPlugin() const { return mContext.index; } + void setPlugin(int index) { mContext.index = index; } // File context. This allows the ESM reader to be 'reset' to this // location later when we are ready to load the full data set. @@ -49,28 +54,28 @@ struct Land }; // default height to use in case there is no Land record - static const int DEFAULT_HEIGHT = -2048; + static constexpr int DEFAULT_HEIGHT = -2048; // number of vertices per side - static const int LAND_SIZE = 65; + static constexpr int LAND_SIZE = 65; // cell terrain size in world coords - static const int REAL_SIZE = Constants::CellSizeInUnits; + static constexpr int REAL_SIZE = Constants::CellSizeInUnits; // total number of vertices - static const int LAND_NUM_VERTS = LAND_SIZE * LAND_SIZE; + static constexpr int LAND_NUM_VERTS = LAND_SIZE * LAND_SIZE; - static const int HEIGHT_SCALE = 8; + static constexpr int HEIGHT_SCALE = 8; //number of textures per side of land - static const int LAND_TEXTURE_SIZE = 16; + static constexpr int LAND_TEXTURE_SIZE = 16; //total number of textures per land - static const int LAND_NUM_TEXTURES = LAND_TEXTURE_SIZE * LAND_TEXTURE_SIZE; + static constexpr int LAND_NUM_TEXTURES = LAND_TEXTURE_SIZE * LAND_TEXTURE_SIZE; - static const int LAND_GLOBAL_MAP_LOD_SIZE = 81; + static constexpr int LAND_GLOBAL_MAP_LOD_SIZE = 81; - static const int LAND_GLOBAL_MAP_LOD_SIZE_SQRT = 9; + static constexpr int LAND_GLOBAL_MAP_LOD_SIZE_SQRT = 9; #pragma pack(push,1) struct VHGT @@ -106,7 +111,7 @@ struct Land // 24-bit normals, these aren't always correct though. Edge and corner normals may be garbage. VNML mNormals[LAND_NUM_VERTS * 3]; - // 2D array of texture indices. An index can be used to look up an ESM::LandTexture, + // 2D array of texture indices. An index can be used to look up an LandTexture, // but to do so you must subtract 1 from the index first! // An index of 0 indicates the default texture. uint16_t mTextures[LAND_NUM_TEXTURES]; @@ -176,7 +181,7 @@ struct Land /// Loads data and marks it as loaded /// \return true if data is actually loaded from file, false otherwise /// including the case when data is already loaded - bool condLoad(ESM::ESMReader& reader, int flags, int& targetFlags, int dataFlag, void *ptr, unsigned int size) const; + bool condLoad(ESMReader& reader, int flags, int& targetFlags, int dataFlag, void *ptr, unsigned int size) const; mutable LandData *mLandData; }; diff --git a/components/esm/loadlevlist.cpp b/components/esm3/loadlevlist.cpp similarity index 79% rename from components/esm/loadlevlist.cpp rename to components/esm3/loadlevlist.cpp index 0aed15aa94..d16ab89231 100644 --- a/components/esm/loadlevlist.cpp +++ b/components/esm3/loadlevlist.cpp @@ -2,32 +2,33 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - void LevelledListBase::load(ESMReader &esm, bool &isDeleted) + void LevelledListBase::load(ESMReader& esm, NAME recName, bool& isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasList = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'D','A','T','A'>::value: + case fourCC("DATA"): esm.getHT(mFlags); break; - case ESM::FourCC<'N','N','A','M'>::value: + case fourCC("NNAM"): esm.getHT(mChanceNone); break; - case ESM::FourCC<'I','N','D','X'>::value: + case fourCC("INDX"): { int length = 0; esm.getHT(length); @@ -42,14 +43,14 @@ namespace ESM for (size_t i = 0; i < mList.size(); i++) { LevelItem &li = mList[i]; - li.mId = esm.getHNString(mRecName); + li.mId = esm.getHNString(recName); esm.getHNT(li.mLevel, "INTV"); } hasList = true; break; } - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -74,13 +75,13 @@ namespace ESM esm.fail("Missing NAME subrecord"); } - void LevelledListBase::save(ESMWriter &esm, bool isDeleted) const + void LevelledListBase::save(ESMWriter& esm, NAME recName, bool isDeleted) const { esm.writeHNCString("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -90,19 +91,16 @@ namespace ESM for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) { - esm.writeHNCString(mRecName, it->mId); + esm.writeHNCString(recName, it->mId); esm.writeHNT("INTV", it->mLevel); } } void LevelledListBase::blank() { + mRecordFlags = 0; mFlags = 0; mChanceNone = 0; mList.clear(); } - - unsigned int CreatureLevList::sRecordId = REC_LEVC; - - unsigned int ItemLevList::sRecordId = REC_LEVI; } diff --git a/components/esm/loadlevlist.hpp b/components/esm3/loadlevlist.hpp similarity index 59% rename from components/esm/loadlevlist.hpp rename to components/esm3/loadlevlist.hpp index ed4131c165..d73003fdda 100644 --- a/components/esm/loadlevlist.hpp +++ b/components/esm3/loadlevlist.hpp @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace ESM { @@ -22,12 +25,9 @@ struct LevelledListBase { int mFlags; unsigned char mChanceNone; // Chance that none are selected (0-100) + unsigned int mRecordFlags; std::string mId; - // Record name used to read references. Must be set before load() is - // called. - const char *mRecName; - struct LevelItem { std::string mId; @@ -36,18 +36,27 @@ struct LevelledListBase std::vector mList; - void load(ESMReader &esm, bool &isDeleted); - void save(ESMWriter &esm, bool isDeleted = false) const; + void load(ESMReader& esm, NAME recName, bool& isDeleted); + void save(ESMWriter& esm, NAME recName, bool isDeleted) const; void blank(); ///< Set record to default state (does not touch the ID). }; -struct CreatureLevList: LevelledListBase +template +struct CustomLevelledListBase : LevelledListBase +{ + void load(ESMReader &esm, bool& isDeleted) { LevelledListBase::load(esm, Base::sRecName, isDeleted); } + void save(ESMWriter &esm, bool isDeleted = false) const { LevelledListBase::save(esm, Base::sRecName, isDeleted); } +}; + +struct CreatureLevList : CustomLevelledListBase { - static unsigned int sRecordId; + /// Record name used to read references. + static constexpr NAME sRecName {"CNAM"}; + static constexpr RecNameInts sRecordId = RecNameInts::REC_LEVC; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "CreatureLevList"; } + static std::string_view getRecordType() { return "CreatureLevList"; } enum Flags { @@ -56,18 +65,15 @@ struct CreatureLevList: LevelledListBase // level, not just the closest below // player. }; - - CreatureLevList() - { - mRecName = "CNAM"; - } }; -struct ItemLevList: LevelledListBase +struct ItemLevList : CustomLevelledListBase { - static unsigned int sRecordId; + /// Record name used to read references. + static constexpr NAME sRecName {"INAM"}; + static constexpr RecNameInts sRecordId = RecNameInts::REC_LEVI; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "ItemLevList"; } + static std::string_view getRecordType() { return "ItemLevList"; } enum Flags { @@ -82,11 +88,6 @@ struct ItemLevList: LevelledListBase // level, not just the closest below // player. }; - - ItemLevList() - { - mRecName = "INAM"; - } }; } diff --git a/components/esm/loadligh.cpp b/components/esm3/loadligh.cpp similarity index 76% rename from components/esm/loadligh.cpp rename to components/esm3/loadligh.cpp index 2a6dac14b0..a8797b347a 100644 --- a/components/esm/loadligh.cpp +++ b/components/esm3/loadligh.cpp @@ -2,47 +2,45 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Light::sRecordId = REC_LIGH; - void Light::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'L','H','D','T'>::value: - esm.getHT(mData, 24); + case fourCC("LHDT"): + esm.getHTSized<24>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'S','N','A','M'>::value: + case fourCC("SNAM"): mSound = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -63,7 +61,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -77,6 +75,7 @@ namespace ESM void Light::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mTime = 0; diff --git a/components/esm/loadligh.hpp b/components/esm3/loadligh.hpp similarity index 87% rename from components/esm/loadligh.hpp rename to components/esm3/loadligh.hpp index 8509c64b6d..422d2b1d4e 100644 --- a/components/esm/loadligh.hpp +++ b/components/esm3/loadligh.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -16,9 +18,10 @@ class ESMWriter; struct Light { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_LIGH; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Light"; } + static std::string_view getRecordType() { return "Light"; } enum Flags { @@ -45,6 +48,7 @@ struct Light LHDTstruct mData; + unsigned int mRecordFlags; std::string mSound, mScript, mModel, mIcon, mName, mId; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadlock.cpp b/components/esm3/loadlock.cpp similarity index 76% rename from components/esm/loadlock.cpp rename to components/esm3/loadlock.cpp index b14353ec5a..4a2693ebf1 100644 --- a/components/esm/loadlock.cpp +++ b/components/esm3/loadlock.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Lockpick::sRecordId = REC_LOCK; - void Lockpick::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'L','K','D','T'>::value: - esm.getHT(mData, 16); + case fourCC("LKDT"): + esm.getHTSized<16>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -61,7 +59,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -75,6 +73,7 @@ namespace ESM void Lockpick::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mQuality = 0; diff --git a/components/esm/loadlock.hpp b/components/esm3/loadlock.hpp similarity index 77% rename from components/esm/loadlock.hpp rename to components/esm3/loadlock.hpp index 9db41af978..1ebc6c902c 100644 --- a/components/esm/loadlock.hpp +++ b/components/esm3/loadlock.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -11,9 +13,10 @@ class ESMWriter; struct Lockpick { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_LOCK; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Lockpick"; } + static std::string_view getRecordType() { return "Lockpick"; } struct Data { @@ -25,6 +28,7 @@ struct Lockpick }; // Size = 16 Data mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadltex.cpp b/components/esm3/loadltex.cpp similarity index 79% rename from components/esm/loadltex.cpp rename to components/esm3/loadltex.cpp index 3e150f9c0c..f7e75f079e 100644 --- a/components/esm/loadltex.cpp +++ b/components/esm3/loadltex.cpp @@ -2,12 +2,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - unsigned int LandTexture::sRecordId = REC_LTEX; - void LandTexture::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; @@ -17,20 +15,20 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'I','N','T','V'>::value: + case fourCC("INTV"): esm.getHT(mIndex); hasIndex = true; break; - case ESM::FourCC<'D','A','T','A'>::value: + case fourCC("DATA"): mTexture = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -53,7 +51,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); } } diff --git a/components/esm/loadltex.hpp b/components/esm3/loadltex.hpp similarity index 84% rename from components/esm/loadltex.hpp rename to components/esm3/loadltex.hpp index e3e2582462..c645cb1d4f 100644 --- a/components/esm/loadltex.hpp +++ b/components/esm3/loadltex.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -18,9 +20,10 @@ class ESMWriter; struct LandTexture { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_LTEX; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "LandTexture"; } + static std::string_view getRecordType() { return "LandTexture"; } // mId is merely a user friendly name for the texture in the editor. std::string mId, mTexture; diff --git a/components/esm/loadmgef.cpp b/components/esm3/loadmgef.cpp similarity index 91% rename from components/esm/loadmgef.cpp rename to components/esm3/loadmgef.cpp index 75a94f828a..28f9982ab7 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm3/loadmgef.cpp @@ -4,11 +4,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" +namespace ESM +{ namespace { - static const char *sIds[ESM::MagicEffect::Length] = + static const char *sIds[MagicEffect::Length] = { "WaterBreathing", "SwiftSwim", @@ -181,20 +183,20 @@ namespace 0x11c8, 0x1048, 0x1048, 0x1048, 0x1048, 0x1048, 0x1048 }; } +} namespace ESM { - unsigned int MagicEffect::sRecordId = REC_MGEF; - void MagicEffect::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // MagicEffect record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); esm.getHNT(mIndex, "INDX"); mId = indexToId (mIndex); - esm.getHNT(mData, "MEDT", 36); + esm.getHNTSized<36>(mData, "MEDT"); if (esm.getFormat() == 0) { // don't allow mods to change fixed flags in the legacy format @@ -208,39 +210,39 @@ void MagicEffect::load(ESMReader &esm, bool &isDeleted) while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'P','T','E','X'>::value: + case fourCC("PTEX"): mParticle = esm.getHString(); break; - case ESM::FourCC<'B','S','N','D'>::value: + case fourCC("BSND"): mBoltSound = esm.getHString(); break; - case ESM::FourCC<'C','S','N','D'>::value: + case fourCC("CSND"): mCastSound = esm.getHString(); break; - case ESM::FourCC<'H','S','N','D'>::value: + case fourCC("HSND"): mHitSound = esm.getHString(); break; - case ESM::FourCC<'A','S','N','D'>::value: + case fourCC("ASND"): mAreaSound = esm.getHString(); break; - case ESM::FourCC<'C','V','F','X'>::value: + case fourCC("CVFX"): mCasting = esm.getHString(); break; - case ESM::FourCC<'B','V','F','X'>::value: + case fourCC("BVFX"): mBolt = esm.getHString(); break; - case ESM::FourCC<'H','V','F','X'>::value: + case fourCC("HVFX"): mHit = esm.getHString(); break; - case ESM::FourCC<'A','V','F','X'>::value: + case fourCC("AVFX"): mArea = esm.getHString(); break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; default: @@ -280,14 +282,12 @@ short MagicEffect::getResistanceEffect(short effect) effects[DisintegrateArmor] = Sanctuary; effects[DisintegrateWeapon] = Sanctuary; - for (int i=0; i<5; ++i) - effects[DrainAttribute+i] = ResistMagicka; - for (int i=0; i<5; ++i) - effects[DamageAttribute+i] = ResistMagicka; - for (int i=0; i<5; ++i) - effects[AbsorbAttribute+i] = ResistMagicka; - for (int i=0; i<10; ++i) - effects[WeaknessToFire+i] = ResistMagicka; + for (int i = DrainAttribute; i <= DamageSkill; ++i) + effects[i] = ResistMagicka; + for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i) + effects[i] = ResistMagicka; + for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i) + effects[i] = ResistMagicka; effects[Burden] = ResistMagicka; effects[Charm] = ResistMagicka; @@ -325,14 +325,12 @@ short MagicEffect::getWeaknessEffect(short effect) static std::map effects; if (effects.empty()) { - for (int i=0; i<5; ++i) - effects[DrainAttribute+i] = WeaknessToMagicka; - for (int i=0; i<5; ++i) - effects[DamageAttribute+i] = WeaknessToMagicka; - for (int i=0; i<5; ++i) - effects[AbsorbAttribute+i] = WeaknessToMagicka; - for (int i=0; i<10; ++i) - effects[WeaknessToFire+i] = WeaknessToMagicka; + for (int i = DrainAttribute; i <= DamageSkill; ++i) + effects[i] = WeaknessToMagicka; + for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i) + effects[i] = WeaknessToMagicka; + for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i) + effects[i] = WeaknessToMagicka; effects[Burden] = WeaknessToMagicka; effects[Charm] = WeaknessToMagicka; @@ -533,10 +531,10 @@ const std::string &MagicEffect::effectIdToString(short effectID) } class FindSecond { - const std::string &mName; + std::string_view mName; public: - FindSecond(const std::string &name) : mName(name) { } + FindSecond(std::string_view name) : mName(name) { } bool operator()(const std::pair &item) const { @@ -546,13 +544,13 @@ public: } }; -short MagicEffect::effectStringToId(const std::string &effect) +short MagicEffect::effectStringToId(std::string_view effect) { std::map::const_iterator name; name = std::find_if(sNames.begin(), sNames.end(), FindSecond(effect)); if(name == sNames.end()) - throw std::runtime_error(std::string("Unimplemented effect ")+effect); + throw std::runtime_error("Unimplemented effect " + std::string(effect)); return name->first; } @@ -578,6 +576,7 @@ MagicEffect::MagnitudeDisplayType MagicEffect::getMagnitudeDisplayType() const { void MagicEffect::blank() { + mRecordFlags = 0; mData.mSchool = 0; mData.mBaseCost = 0; mData.mFlags = 0; diff --git a/components/esm/loadmgef.hpp b/components/esm3/loadmgef.hpp similarity index 95% rename from components/esm/loadmgef.hpp rename to components/esm3/loadmgef.hpp index d718aaccf5..6cd1ffad9b 100644 --- a/components/esm/loadmgef.hpp +++ b/components/esm3/loadmgef.hpp @@ -2,8 +2,11 @@ #define OPENMW_ESM_MGEF_H #include +#include #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -12,10 +15,12 @@ class ESMWriter; struct MagicEffect { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_MGEF; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "MagicEffect"; } + static std::string_view getRecordType() { return "MagicEffect"; } + unsigned int mRecordFlags; std::string mId; enum Flags @@ -69,7 +74,7 @@ struct MagicEffect static const std::map sNames; static const std::string &effectIdToString(short effectID); - static short effectStringToId(const std::string &effect); + static short effectStringToId(std::string_view effect); /// Returns the effect that provides resistance against \a effect (or -1 if there's none) static short getResistanceEffect(short effect); @@ -82,8 +87,8 @@ struct MagicEffect MEDTstruct mData; std::string mIcon, mParticle; // Textures - std::string mCasting, mHit, mArea; // ESM::Static - std::string mBolt; // ESM::Weapon + std::string mCasting, mHit, mArea; // Static + std::string mBolt; // Weapon std::string mCastSound, mBoltSound, mHitSound, mAreaSound; // Sounds std::string mDescription; diff --git a/components/esm/loadmisc.cpp b/components/esm3/loadmisc.cpp similarity index 76% rename from components/esm/loadmisc.cpp rename to components/esm3/loadmisc.cpp index 3ba6626505..48c66d08b6 100644 --- a/components/esm/loadmisc.cpp +++ b/components/esm3/loadmisc.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Miscellaneous::sRecordId = REC_MISC; - void Miscellaneous::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'M','C','D','T'>::value: - esm.getHT(mData, 12); + case fourCC("MCDT"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -61,7 +59,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -74,6 +72,7 @@ namespace ESM void Miscellaneous::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mIsKey = 0; diff --git a/components/esm/loadmisc.hpp b/components/esm3/loadmisc.hpp similarity index 82% rename from components/esm/loadmisc.hpp rename to components/esm3/loadmisc.hpp index e7a3239042..2af4784a3c 100644 --- a/components/esm/loadmisc.hpp +++ b/components/esm3/loadmisc.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -16,9 +18,10 @@ class ESMWriter; struct Miscellaneous { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_MISC; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Miscellaneous"; } + static std::string_view getRecordType() { return "Miscellaneous"; } struct MCDTstruct { @@ -30,6 +33,7 @@ struct Miscellaneous }; MCDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadnpc.cpp b/components/esm3/loadnpc.cpp similarity index 84% rename from components/esm/loadnpc.cpp rename to components/esm3/loadnpc.cpp index 2bb0811ac9..08677838a8 100644 --- a/components/esm/loadnpc.cpp +++ b/components/esm3/loadnpc.cpp @@ -2,17 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int NPC::sRecordId = REC_NPC_; - void NPC::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; - - mPersistent = (esm.getRecordFlags() & 0x0400) != 0; + mRecordFlags = esm.getRecordFlags(); mSpells.mList.clear(); mInventory.mList.clear(); @@ -27,37 +23,37 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'R','N','A','M'>::value: + case fourCC("RNAM"): mRace = esm.getHString(); break; - case ESM::FourCC<'C','N','A','M'>::value: + case fourCC("CNAM"): mClass = esm.getHString(); break; - case ESM::FourCC<'A','N','A','M'>::value: + case fourCC("ANAM"): mFaction = esm.getHString(); break; - case ESM::FourCC<'B','N','A','M'>::value: + case fourCC("BNAM"): mHead = esm.getHString(); break; - case ESM::FourCC<'K','N','A','M'>::value: + case fourCC("KNAM"): mHair = esm.getHString(); break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'N','P','D','T'>::value: + case fourCC("NPDT"): hasNpdt = true; esm.getSubHeader(); if (esm.getSubSize() == 52) @@ -84,24 +80,24 @@ namespace ESM else esm.fail("NPC_NPDT must be 12 or 52 bytes long"); break; - case ESM::FourCC<'F','L','A','G'>::value: + case fourCC("FLAG"): hasFlags = true; int flags; esm.getHT(flags); mFlags = flags & 0xFF; mBloodType = ((flags >> 8) & 0xFF) >> 2; break; - case ESM::FourCC<'N','P','C','S'>::value: + case fourCC("NPCS"): mSpells.add(esm); break; - case ESM::FourCC<'N','P','C','O'>::value: + case fourCC("NPCO"): mInventory.add(esm); break; - case ESM::FourCC<'A','I','D','T'>::value: + case fourCC("AIDT"): esm.getHExact(&mAiData, sizeof(mAiData)); break; - case ESM::FourCC<'D','O','D','T'>::value: - case ESM::FourCC<'D','N','A','M'>::value: + case fourCC("DODT"): + case fourCC("DNAM"): mTransport.add(esm); break; case AI_Wander: @@ -112,7 +108,7 @@ namespace ESM case AI_CNDT: mAiPackage.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -135,7 +131,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -159,6 +155,9 @@ namespace ESM npdt12.mDisposition = mNpdt.mDisposition; npdt12.mReputation = mNpdt.mReputation; npdt12.mRank = mNpdt.mRank; + npdt12.mUnknown1 = 0; + npdt12.mUnknown2 = 0; + npdt12.mUnknown3 = 0; npdt12.mGold = mNpdt.mGold; esm.writeHNT("NPDT", npdt12, 12); } @@ -187,6 +186,7 @@ namespace ESM void NPC::blank() { + mRecordFlags = 0; mNpdtType = NPC_DEFAULT; blankNpdt(); mBloodType = 0; diff --git a/components/esm/loadnpc.hpp b/components/esm3/loadnpc.hpp similarity index 94% rename from components/esm/loadnpc.hpp rename to components/esm3/loadnpc.hpp index 687afeaf64..b766a58394 100644 --- a/components/esm/loadnpc.hpp +++ b/components/esm3/loadnpc.hpp @@ -4,14 +4,15 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" #include "loadcont.hpp" #include "aipackage.hpp" #include "spelllist.hpp" #include "loadskil.hpp" #include "transport.hpp" -namespace ESM { +namespace ESM +{ class ESMReader; class ESMWriter; @@ -22,9 +23,10 @@ class ESMWriter; struct NPC { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_NPC_; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "NPC"; } + static std::string_view getRecordType() { return "NPC"; } // Services enum Services @@ -116,8 +118,6 @@ struct NPC int mBloodType; unsigned char mFlags; - bool mPersistent; - InventoryList mInventory; SpellList mSpells; @@ -129,6 +129,7 @@ struct NPC AIPackageList mAiPackage; + unsigned int mRecordFlags; std::string mId, mName, mModel, mRace, mClass, mFaction, mScript; // body parts diff --git a/components/esm/loadpgrd.cpp b/components/esm3/loadpgrd.cpp similarity index 92% rename from components/esm/loadpgrd.cpp rename to components/esm3/loadpgrd.cpp index 708685e727..461a4194ee 100644 --- a/components/esm/loadpgrd.cpp +++ b/components/esm3/loadpgrd.cpp @@ -2,12 +2,9 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Pathgrid::sRecordId = REC_PGRD; - Pathgrid::Point& Pathgrid::Point::operator=(const float rhs[3]) { mX = static_cast(rhs[0]); @@ -46,16 +43,16 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mCell = esm.getHString(); break; - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mData, 12); + case fourCC("DATA"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::FourCC<'P','G','R','P'>::value: + case fourCC("PGRP"): { esm.getSubHeader(); int size = esm.getSubSize(); @@ -76,7 +73,7 @@ namespace ESM } break; } - case ESM::FourCC<'P','G','R','C'>::value: + case fourCC("PGRC"): { esm.getSubHeader(); int size = esm.getSubSize(); @@ -100,6 +97,8 @@ namespace ESM for(PointList::const_iterator it = mPoints.begin(); it != mPoints.end(); ++it, ++pointIndex) { unsigned char connectionNum = (*it).mConnectionNum; + if (rawConnections.end() - rawIt < connectionNum) + esm.fail("Not enough connections"); for (int i = 0; i < connectionNum; ++i) { Edge edge; edge.mV0 = pointIndex; @@ -111,7 +110,7 @@ namespace ESM } break; } - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -154,7 +153,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } diff --git a/components/esm/loadpgrd.hpp b/components/esm3/loadpgrd.hpp similarity index 90% rename from components/esm/loadpgrd.hpp rename to components/esm3/loadpgrd.hpp index 4e74c9a24d..b9490ae221 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm3/loadpgrd.hpp @@ -4,6 +4,8 @@ #include #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -15,9 +17,10 @@ class ESMWriter; */ struct Pathgrid { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_PGRD; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Pathgrid"; } + static std::string_view getRecordType() { return "Pathgrid"; } struct DATAstruct { diff --git a/components/esm/loadprob.cpp b/components/esm3/loadprob.cpp similarity index 76% rename from components/esm/loadprob.cpp rename to components/esm3/loadprob.cpp index 6307df3298..419b716ebc 100644 --- a/components/esm/loadprob.cpp +++ b/components/esm3/loadprob.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Probe::sRecordId = REC_PROB; - void Probe::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'P','B','D','T'>::value: - esm.getHT(mData, 16); + case fourCC("PBDT"): + esm.getHTSized<16>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -61,7 +59,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -75,6 +73,7 @@ namespace ESM void Probe::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mQuality = 0; diff --git a/components/esm/loadprob.hpp b/components/esm3/loadprob.hpp similarity index 77% rename from components/esm/loadprob.hpp rename to components/esm3/loadprob.hpp index da203b456b..58e65d6c64 100644 --- a/components/esm/loadprob.hpp +++ b/components/esm3/loadprob.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -11,9 +13,10 @@ class ESMWriter; struct Probe { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_PROB; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Probe"; } + static std::string_view getRecordType() { return "Probe"; } struct Data { @@ -25,6 +28,7 @@ struct Probe }; // Size = 16 Data mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadrace.cpp b/components/esm3/loadrace.cpp similarity index 77% rename from components/esm/loadrace.cpp rename to components/esm3/loadrace.cpp index 88ce08c913..53174219df 100644 --- a/components/esm/loadrace.cpp +++ b/components/esm3/loadrace.cpp @@ -2,25 +2,23 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Race::sRecordId = REC_RACE; - int Race::MaleFemale::getValue (bool male) const { return male ? mMale : mFemale; } - int Race::MaleFemaleF::getValue (bool male) const + float Race::MaleFemaleF::getValue (bool male) const { - return static_cast(male ? mMale : mFemale); + return male ? mMale : mFemale; } void Race::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); @@ -29,26 +27,26 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'R','A','D','T'>::value: - esm.getHT(mData, 140); + case fourCC("RADT"): + esm.getHTSized<140>(mData); hasData = true; break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; - case ESM::FourCC<'N','P','C','S'>::value: + case fourCC("NPCS"): mPowers.add(esm); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -68,7 +66,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -80,6 +78,7 @@ namespace ESM void Race::blank() { + mRecordFlags = 0; mName.clear(); mDescription.clear(); diff --git a/components/esm/loadrace.hpp b/components/esm3/loadrace.hpp similarity index 85% rename from components/esm/loadrace.hpp rename to components/esm3/loadrace.hpp index bf0573075c..74459bab3d 100644 --- a/components/esm/loadrace.hpp +++ b/components/esm3/loadrace.hpp @@ -4,6 +4,7 @@ #include #include "spelllist.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -17,9 +18,10 @@ class ESMWriter; struct Race { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_RACE; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Race"; } + static std::string_view getRecordType() { return "Race"; } struct SkillBonus { @@ -38,7 +40,7 @@ struct Race { float mMale, mFemale; - int getValue (bool male) const; + float getValue (bool male) const; }; enum Flags @@ -65,6 +67,7 @@ struct Race RADTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mDescription; SpellList mPowers; diff --git a/components/esm/loadregn.cpp b/components/esm3/loadregn.cpp similarity index 66% rename from components/esm/loadregn.cpp rename to components/esm3/loadregn.cpp index 98edca48f0..9de89d1889 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm3/loadregn.cpp @@ -2,51 +2,40 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Region::sRecordId = REC_REGN; - void Region::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'W','E','A','T'>::value: + case fourCC("WEAT"): { esm.getSubHeader(); - if (esm.getVer() == VER_12) + // Cold weather not included before 1.3 + if (esm.getSubSize() == sizeof(mData)) { - mData.mA = 0; - mData.mB = 0; - esm.getExact(&mData, sizeof(mData) - 2); + esm.getT(mData); } - else if (esm.getVer() == VER_13) + else if (esm.getSubSize() == sizeof(mData) - 2) { - // May include the additional two bytes (but not necessarily) - if (esm.getSubSize() == sizeof(mData)) - { - esm.getExact(&mData, sizeof(mData)); - } - else - { - mData.mA = 0; - mData.mB = 0; - esm.getExact(&mData, sizeof(mData)-2); - } + mData.mSnow = 0; + mData.mBlizzard = 0; + esm.getExact(&mData, sizeof(mData) - 2); } else { @@ -54,13 +43,13 @@ namespace ESM } break; } - case ESM::FourCC<'B','N','A','M'>::value: + case fourCC("BNAM"): mSleepList = esm.getHString(); break; - case ESM::FourCC<'C','N','A','M'>::value: + case fourCC("CNAM"): esm.getHT(mMapColor); break; - case ESM::FourCC<'S','N','A','M'>::value: + case fourCC("SNAM"): { esm.getSubHeader(); SoundRef sr; @@ -69,7 +58,7 @@ namespace ESM mSoundList.push_back(sr); break; } - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -89,7 +78,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -108,14 +97,15 @@ namespace ESM esm.startSubRecord("SNAM"); esm.writeFixedSizeString(it->mSound, 32); esm.writeT(it->mChance); - esm.endRecord("NPCO"); + esm.endRecord("SNAM"); } } void Region::blank() { + mRecordFlags = 0; mData.mClear = mData.mCloudy = mData.mFoggy = mData.mOvercast = mData.mRain = - mData.mThunder = mData.mAsh, mData.mBlight = mData.mA = mData.mB = 0; + mData.mThunder = mData.mAsh = mData.mBlight = mData.mSnow = mData.mBlizzard = 0; mMapColor = 0; diff --git a/components/esm/loadregn.hpp b/components/esm3/loadregn.hpp similarity index 77% rename from components/esm/loadregn.hpp rename to components/esm3/loadregn.hpp index 6f39dc0bff..555b5e1b8d 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm3/loadregn.hpp @@ -4,7 +4,8 @@ #include #include -#include "esmcommon.hpp" +#include "components/esm/defs.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -18,20 +19,17 @@ class ESMWriter; struct Region { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_REGN; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Region"; } + static std::string_view getRecordType() { return "Region"; } #pragma pack(push) #pragma pack(1) struct WEATstruct { // These are probabilities that add up to 100 - unsigned char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, - // Unknown weather, probably snow and something. Only - // present in file version 1.3. - // the engine uses mA as "snow" and mB as "blizard" - mA, mB; + unsigned char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, mSnow, mBlizzard; }; // 10 bytes #pragma pack(pop) @@ -45,6 +43,7 @@ struct Region WEATstruct mData; int mMapColor; // RGBA + unsigned int mRecordFlags; // sleepList refers to a leveled list of creatures you can meet if // you sleep outside in this region. std::string mId, mName, mSleepList; diff --git a/components/esm/loadrepa.cpp b/components/esm3/loadrepa.cpp similarity index 76% rename from components/esm/loadrepa.cpp rename to components/esm3/loadrepa.cpp index c04691c12f..3580359873 100644 --- a/components/esm/loadrepa.cpp +++ b/components/esm3/loadrepa.cpp @@ -2,44 +2,42 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Repair::sRecordId = REC_REPA; - void Repair::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'R','I','D','T'>::value: - esm.getHT(mData, 16); + case fourCC("RIDT"): + esm.getHTSized<16>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -61,7 +59,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -75,6 +73,7 @@ namespace ESM void Repair::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mQuality = 0; diff --git a/components/esm/loadrepa.hpp b/components/esm3/loadrepa.hpp similarity index 77% rename from components/esm/loadrepa.hpp rename to components/esm3/loadrepa.hpp index 2537c53cb6..d4e94feaf4 100644 --- a/components/esm/loadrepa.hpp +++ b/components/esm3/loadrepa.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -11,9 +13,10 @@ class ESMWriter; struct Repair { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_REPA; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Repair"; } + static std::string_view getRecordType() { return "Repair"; } struct Data { @@ -25,6 +28,7 @@ struct Repair }; // Size = 16 Data mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mScript; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadscpt.cpp b/components/esm3/loadscpt.cpp similarity index 81% rename from components/esm/loadscpt.cpp rename to components/esm3/loadscpt.cpp index a7f348cb17..9f59dfbf23 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm3/loadscpt.cpp @@ -1,15 +1,14 @@ #include "loadscpt.hpp" +#include + #include #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Script::sRecordId = REC_SCPT; - void Script::loadSCVR(ESMReader &esm) { int s = mData.mStringTableSize; @@ -30,7 +29,7 @@ namespace ESM // The tmp buffer is a null-byte separated string list, we // just have to pick out one string at a time. char* str = tmp.data(); - if (!str) + if (tmp.empty()) { if (mVarNames.size() > 0) Log(Debug::Warning) << "SCVR with no variable names"; @@ -41,33 +40,38 @@ namespace ESM // Support '\r' terminated strings like vanilla. See Bug #1324. std::replace(tmp.begin(), tmp.end(), '\r', '\0'); // Avoid heap corruption - if (!tmp.empty() && tmp[tmp.size()-1] != '\0') + if (tmp.back() != '\0') { tmp.emplace_back('\0'); std::stringstream ss; ss << "Malformed string table"; ss << "\n File: " << esm.getName(); - ss << "\n Record: " << esm.getContext().recName.toString(); + ss << "\n Record: " << esm.getContext().recName.toStringView(); ss << "\n Subrecord: " << "SCVR"; ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); Log(Debug::Verbose) << ss.str(); + str = tmp.data(); } + const auto tmpEnd = tmp.data() + tmp.size(); for (size_t i = 0; i < mVarNames.size(); i++) { mVarNames[i] = std::string(str); str += mVarNames[i].size() + 1; - if (static_cast(str - tmp.data()) > tmp.size()) + if (str >= tmpEnd) { - // SCVR subrecord is unused and variable names are determined - // from the script source, so an overflow is not fatal. - std::stringstream ss; - ss << "String table overflow"; - ss << "\n File: " << esm.getName(); - ss << "\n Record: " << esm.getContext().recName.toString(); - ss << "\n Subrecord: " << "SCVR"; - ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); - Log(Debug::Verbose) << ss.str(); + if(str > tmpEnd) + { + // SCVR subrecord is unused and variable names are determined + // from the script source, so an overflow is not fatal. + std::stringstream ss; + ss << "String table overflow"; + ss << "\n File: " << esm.getName(); + ss << "\n Record: " << esm.getContext().recName.toStringView(); + ss << "\n Subrecord: " << "SCVR"; + ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); + Log(Debug::Verbose) << ss.str(); + } // Get rid of empty strings in the list. mVarNames.resize(i+1); break; @@ -78,6 +82,7 @@ namespace ESM void Script::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mVarNames.clear(); @@ -85,9 +90,9 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'S','C','H','D'>::value: + case fourCC("SCHD"): { esm.getSubHeader(); mId = esm.getString(32); @@ -96,11 +101,11 @@ namespace ESM hasHeader = true; break; } - case ESM::FourCC<'S','C','V','R'>::value: + case fourCC("SCVR"): // list of local variables loadSCVR(esm); break; - case ESM::FourCC<'S','C','D','T'>::value: + case fourCC("SCDT"): { // compiled script esm.getSubHeader(); @@ -119,10 +124,10 @@ namespace ESM esm.getExact(mScriptData.data(), mScriptData.size()); break; } - case ESM::FourCC<'S','C','T','X'>::value: + case fourCC("SCTX"): mScriptText = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -150,7 +155,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -173,6 +178,7 @@ namespace ESM void Script::blank() { + mRecordFlags = 0; mData.mNumShorts = mData.mNumLongs = mData.mNumFloats = 0; mData.mScriptDataSize = 0; mData.mStringTableSize = 0; diff --git a/components/esm/loadscpt.hpp b/components/esm3/loadscpt.hpp similarity index 86% rename from components/esm/loadscpt.hpp rename to components/esm3/loadscpt.hpp index e1ffe1b864..fdb31ad6a4 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm3/loadscpt.hpp @@ -4,7 +4,8 @@ #include #include -#include "esmcommon.hpp" +#include "components/esm/defs.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -19,9 +20,10 @@ class ESMWriter; class Script { public: - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SCPT; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Script"; } + static std::string_view getRecordType() { return "Script"; } struct SCHDstruct { @@ -35,6 +37,7 @@ public: Script::SCHDstruct mData; }; + unsigned int mRecordFlags; std::string mId; SCHDstruct mData; diff --git a/components/esm/loadskil.cpp b/components/esm3/loadskil.cpp similarity index 93% rename from components/esm/loadskil.cpp rename to components/esm3/loadskil.cpp index 61cca7d0d7..6ad0faaf75 100644 --- a/components/esm/loadskil.cpp +++ b/components/esm3/loadskil.cpp @@ -4,7 +4,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -125,28 +125,27 @@ namespace ESM HandToHand }}; - unsigned int Skill::sRecordId = REC_SKIL; - void Skill::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // Skill record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); bool hasIndex = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::FourCC<'I','N','D','X'>::value: + case fourCC("INDX"): esm.getHT(mIndex); hasIndex = true; break; - case ESM::FourCC<'S','K','D','T'>::value: - esm.getHT(mData, 24); + case fourCC("SKDT"): + esm.getHTSized<24>(mData); hasData = true; break; - case ESM::FourCC<'D','E','S','C'>::value: + case fourCC("DESC"): mDescription = esm.getHString(); break; default: @@ -172,6 +171,7 @@ namespace ESM void Skill::blank() { + mRecordFlags = 0; mData.mAttribute = 0; mData.mSpecialization = 0; mData.mUseValue[0] = mData.mUseValue[1] = mData.mUseValue[2] = mData.mUseValue[3] = 1.0; diff --git a/components/esm/loadskil.hpp b/components/esm3/loadskil.hpp similarity index 90% rename from components/esm/loadskil.hpp rename to components/esm3/loadskil.hpp index 099264fab7..3fed87f136 100644 --- a/components/esm/loadskil.hpp +++ b/components/esm3/loadskil.hpp @@ -4,9 +4,10 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" -namespace ESM { +namespace ESM +{ class ESMReader; class ESMWriter; @@ -18,10 +19,12 @@ class ESMWriter; struct Skill { - static unsigned int sRecordId; + + constexpr static RecNameInts sRecordId = REC_SKIL; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Skill"; } + static std::string_view getRecordType() { return "Skill"; } + unsigned int mRecordFlags; std::string mId; struct SKDTstruct diff --git a/components/esm/loadsndg.cpp b/components/esm3/loadsndg.cpp similarity index 76% rename from components/esm/loadsndg.cpp rename to components/esm3/loadsndg.cpp index 9bd806641b..c81ff87099 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm3/loadsndg.cpp @@ -2,38 +2,37 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - unsigned int SoundGenerator::sRecordId = REC_SNDG; - void SoundGenerator::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mType, 4); + case fourCC("DATA"): + esm.getHTSized<4>(mType); hasData = true; break; - case ESM::FourCC<'C','N','A','M'>::value: + case fourCC("CNAM"): mCreature = esm.getHString(); break; - case ESM::FourCC<'S','N','A','M'>::value: + case fourCC("SNAM"): mSound = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -54,7 +53,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -66,6 +65,7 @@ namespace ESM void SoundGenerator::blank() { + mRecordFlags = 0; mType = LeftFoot; mCreature.clear(); mSound.clear(); diff --git a/components/esm/loadsndg.hpp b/components/esm3/loadsndg.hpp similarity index 78% rename from components/esm/loadsndg.hpp rename to components/esm3/loadsndg.hpp index 70b221e98c..ad8c26f3ed 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm3/loadsndg.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -15,9 +17,10 @@ class ESMWriter; struct SoundGenerator { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SNDG; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "SoundGenerator"; } + static std::string_view getRecordType() { return "SoundGenerator"; } enum Type { @@ -34,6 +37,7 @@ struct SoundGenerator // Type int mType; + unsigned int mRecordFlags; std::string mId, mCreature, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadsoun.cpp b/components/esm3/loadsoun.cpp similarity index 78% rename from components/esm/loadsoun.cpp rename to components/esm3/loadsoun.cpp index 3b3dc1eacd..95be333e13 100644 --- a/components/esm/loadsoun.cpp +++ b/components/esm3/loadsoun.cpp @@ -2,35 +2,33 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Sound::sRecordId = REC_SOUN; - void Sound::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mSound = esm.getHString(); break; - case ESM::FourCC<'D','A','T','A'>::value: - esm.getHT(mData, 3); + case fourCC("DATA"): + esm.getHTSized<3>(mData); hasData = true; break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -52,7 +50,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -62,6 +60,7 @@ namespace ESM void Sound::blank() { + mRecordFlags = 0; mSound.clear(); mData.mVolume = 128; diff --git a/components/esm/loadsoun.hpp b/components/esm3/loadsoun.hpp similarity index 75% rename from components/esm/loadsoun.hpp rename to components/esm3/loadsoun.hpp index 937e22be88..5fea039f4f 100644 --- a/components/esm/loadsoun.hpp +++ b/components/esm3/loadsoun.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -16,11 +18,13 @@ struct SOUNstruct struct Sound { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SOUN; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Sound"; } + static std::string_view getRecordType() { return "Sound"; } SOUNstruct mData; + unsigned int mRecordFlags; std::string mId, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadspel.cpp b/components/esm3/loadspel.cpp similarity index 76% rename from components/esm/loadspel.cpp rename to components/esm3/loadspel.cpp index 947e6c9ec8..1415528f88 100644 --- a/components/esm/loadspel.cpp +++ b/components/esm3/loadspel.cpp @@ -2,15 +2,13 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Spell::sRecordId = REC_SPEL; - void Spell::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); @@ -19,25 +17,25 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'S','P','D','T'>::value: - esm.getHT(mData, 12); + case fourCC("SPDT"): + esm.getHTSized<12>(mData); hasData = true; break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): ENAMstruct s; - esm.getHT(s, 24); + esm.getHTSized<24>(s); mEffects.mList.push_back(s); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -59,7 +57,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -70,6 +68,7 @@ namespace ESM void Spell::blank() { + mRecordFlags = 0; mData.mType = 0; mData.mCost = 0; mData.mFlags = 0; diff --git a/components/esm/loadspel.hpp b/components/esm3/loadspel.hpp similarity index 86% rename from components/esm/loadspel.hpp rename to components/esm3/loadspel.hpp index 1763d0991c..59fbe7fd95 100644 --- a/components/esm/loadspel.hpp +++ b/components/esm3/loadspel.hpp @@ -4,6 +4,7 @@ #include #include "effectlist.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -13,9 +14,10 @@ class ESMWriter; struct Spell { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SPEL; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Spell"; } + static std::string_view getRecordType() { return "Spell"; } enum SpellType { @@ -42,6 +44,7 @@ struct Spell }; SPDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName; EffectList mEffects; diff --git a/components/esm/loadsscr.cpp b/components/esm3/loadsscr.cpp similarity index 80% rename from components/esm/loadsscr.cpp rename to components/esm3/loadsscr.cpp index f8854493b9..6f30980f04 100644 --- a/components/esm/loadsscr.cpp +++ b/components/esm3/loadsscr.cpp @@ -2,32 +2,30 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int StartScript::sRecordId = REC_SSCR; - void StartScript::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasData = false; bool hasName = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'D','A','T','A'>::value: + case fourCC("DATA"): mData = esm.getHString(); hasData = true; break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -47,7 +45,7 @@ namespace ESM esm.writeHNCString("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); } else { @@ -57,6 +55,7 @@ namespace ESM void StartScript::blank() { + mRecordFlags = 0; mData.clear(); } } diff --git a/components/esm/loadsscr.hpp b/components/esm3/loadsscr.hpp similarity index 80% rename from components/esm/loadsscr.hpp rename to components/esm3/loadsscr.hpp index ce2ff49e77..64c8e78cda 100644 --- a/components/esm/loadsscr.hpp +++ b/components/esm3/loadsscr.hpp @@ -3,6 +3,8 @@ #include +#include "components/esm/defs.hpp" + namespace ESM { @@ -19,11 +21,13 @@ class ESMWriter; struct StartScript { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SSCR; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "StartScript"; } + static std::string_view getRecordType() { return "StartScript"; } std::string mData; + unsigned int mRecordFlags; std::string mId; // Load a record and add it to the list diff --git a/components/esm/loadstat.cpp b/components/esm3/loadstat.cpp similarity index 73% rename from components/esm/loadstat.cpp rename to components/esm3/loadstat.cpp index 6c9de22bd1..fba92cd467 100644 --- a/components/esm/loadstat.cpp +++ b/components/esm3/loadstat.cpp @@ -2,30 +2,30 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" namespace ESM { - unsigned int Static::sRecordId = REC_STAT; - void Static::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); + //bool isBlocked = (mRecordFlags & FLAG_Blocked) != 0; + //bool isPersistent = (mRecordFlags & FLAG_Persistent) != 0; bool hasName = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -43,7 +43,7 @@ namespace ESM esm.writeHNCString("NAME", mId); if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); } else { @@ -53,6 +53,7 @@ namespace ESM void Static::blank() { + mRecordFlags = 0; mModel.clear(); } } diff --git a/components/esm/loadstat.hpp b/components/esm3/loadstat.hpp similarity index 82% rename from components/esm/loadstat.hpp rename to components/esm3/loadstat.hpp index 3d91440402..d107cc0ffd 100644 --- a/components/esm/loadstat.hpp +++ b/components/esm3/loadstat.hpp @@ -3,7 +3,10 @@ #include -namespace ESM { +#include "components/esm/defs.hpp" + +namespace ESM +{ class ESMReader; class ESMWriter; @@ -22,10 +25,12 @@ class ESMWriter; struct Static { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_STAT; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Static"; } + static std::string_view getRecordType() { return "Static"; } + unsigned int mRecordFlags; std::string mId, mModel; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadtes3.cpp b/components/esm3/loadtes3.cpp similarity index 73% rename from components/esm/loadtes3.cpp rename to components/esm3/loadtes3.cpp index d953f1dc23..0c3ccf232e 100644 --- a/components/esm/loadtes3.cpp +++ b/components/esm3/loadtes3.cpp @@ -1,13 +1,16 @@ #include "loadtes3.hpp" -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" -void ESM::Header::blank() +namespace ESM { - mData.version = ESM::VER_13; + +void Header::blank() +{ + mData.version = VER_13; mData.type = 0; mData.author.clear(); mData.desc.clear(); @@ -16,7 +19,7 @@ void ESM::Header::blank() mMaster.clear(); } -void ESM::Header::load (ESMReader &esm) +void Header::load (ESMReader &esm) { if (esm.isNextSub ("FORM")) { @@ -41,7 +44,7 @@ void ESM::Header::load (ESMReader &esm) { MasterData m; m.name = esm.getHString(); - m.size = esm.getHNLong ("DATA"); + esm.getHNT(m.size, "DATA"); mMaster.push_back (m); } @@ -54,18 +57,18 @@ void ESM::Header::load (ESMReader &esm) esm.getSubHeader(); mSCRD.resize(esm.getSubSize()); if (!mSCRD.empty()) - esm.getExact(&mSCRD[0], mSCRD.size()); + esm.getExact(mSCRD.data(), mSCRD.size()); } if (esm.isNextSub("SCRS")) { esm.getSubHeader(); mSCRS.resize(esm.getSubSize()); if (!mSCRS.empty()) - esm.getExact(&mSCRS[0], mSCRS.size()); + esm.getExact(mSCRS.data(), mSCRS.size()); } } -void ESM::Header::save (ESMWriter &esm) +void Header::save (ESMWriter &esm) { if (mFormat>0) esm.writeHNT ("FORM", mFormat); @@ -78,10 +81,11 @@ void ESM::Header::save (ESMWriter &esm) esm.writeT(mData.records); esm.endRecord("HEDR"); - for (std::vector::iterator iter = mMaster.begin(); - iter != mMaster.end(); ++iter) + for (const Header::MasterData& data : mMaster) { - esm.writeHNCString ("MAST", iter->name); - esm.writeHNT ("DATA", iter->size); + esm.writeHNCString ("MAST", data.name); + esm.writeHNT ("DATA", data.size); } } + +} diff --git a/components/esm/loadtes3.hpp b/components/esm3/loadtes3.hpp similarity index 89% rename from components/esm/loadtes3.hpp rename to components/esm3/loadtes3.hpp index 5b26ac7d2d..ea41ce9fb6 100644 --- a/components/esm/loadtes3.hpp +++ b/components/esm3/loadtes3.hpp @@ -3,7 +3,7 @@ #include -#include "esmcommon.hpp" +#include "components/esm/esmcommon.hpp" namespace ESM { @@ -42,14 +42,13 @@ namespace ESM /// \brief File header record struct Header { - static const int CurrentFormat = 0; // most recent known format + static constexpr int CurrentFormat = 1; // most recent known format // Defines another files (esm or esp) that this file depends upon. struct MasterData { std::string name; uint64_t size; - int index; // Position of the parent file in the global list of loaded files }; GMDT mGameData; // Used in .ess savegames only diff --git a/components/esm/loadweap.cpp b/components/esm3/loadweap.cpp similarity index 77% rename from components/esm/loadweap.cpp rename to components/esm3/loadweap.cpp index 4a77ff6a00..cdc81f2aa7 100644 --- a/components/esm/loadweap.cpp +++ b/components/esm3/loadweap.cpp @@ -2,47 +2,46 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { - unsigned int Weapon::sRecordId = REC_WEAP; - void Weapon::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { - case ESM::SREC_NAME: + case SREC_NAME: mId = esm.getHString(); hasName = true; break; - case ESM::FourCC<'M','O','D','L'>::value: + case fourCC("MODL"): mModel = esm.getHString(); break; - case ESM::FourCC<'F','N','A','M'>::value: + case fourCC("FNAM"): mName = esm.getHString(); break; - case ESM::FourCC<'W','P','D','T'>::value: - esm.getHT(mData, 32); + case fourCC("WPDT"): + esm.getHTSized<32>(mData); hasData = true; break; - case ESM::FourCC<'S','C','R','I'>::value: + case fourCC("SCRI"): mScript = esm.getHString(); break; - case ESM::FourCC<'I','T','E','X'>::value: + case fourCC("ITEX"): mIcon = esm.getHString(); break; - case ESM::FourCC<'E','N','A','M'>::value: + case fourCC("ENAM"): mEnchant = esm.getHString(); break; - case ESM::SREC_DELE: + case SREC_DELE: esm.skipHSub(); isDeleted = true; break; @@ -62,7 +61,7 @@ namespace ESM if (isDeleted) { - esm.writeHNCString("DELE", ""); + esm.writeHNString("DELE", "", 3); return; } @@ -76,6 +75,7 @@ namespace ESM void Weapon::blank() { + mRecordFlags = 0; mData.mWeight = 0; mData.mValue = 0; mData.mType = 0; diff --git a/components/esm/loadweap.hpp b/components/esm3/loadweap.hpp similarity index 92% rename from components/esm/loadweap.hpp rename to components/esm3/loadweap.hpp index 90431756d7..ee7fc845c4 100644 --- a/components/esm/loadweap.hpp +++ b/components/esm3/loadweap.hpp @@ -17,9 +17,10 @@ class ESMWriter; struct Weapon { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_WEAP; + /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Weapon"; } + static std::string_view getRecordType() { return "Weapon"; } enum Type { @@ -73,6 +74,7 @@ struct Weapon WPDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mModel, mIcon, mEnchant, mScript; void load(ESMReader &esm, bool &isDeleted); @@ -104,7 +106,7 @@ struct WeaponType std::string mSoundId; std::string mAttachBone; std::string mSheathingBone; - ESM::Skill::SkillEnum mSkill; + Skill::SkillEnum mSkill; Class mWeaponClass; int mAmmoType; int mFlags; diff --git a/components/esm/locals.cpp b/components/esm3/locals.cpp similarity index 85% rename from components/esm/locals.cpp rename to components/esm3/locals.cpp index 4149695fe9..7db0e544ec 100644 --- a/components/esm/locals.cpp +++ b/components/esm3/locals.cpp @@ -3,7 +3,10 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::Locals::load (ESMReader &esm) +namespace ESM +{ + +void Locals::load (ESMReader &esm) { while (esm.isNextSub ("LOCA")) { @@ -16,7 +19,7 @@ void ESM::Locals::load (ESMReader &esm) } } -void ESM::Locals::save (ESMWriter &esm) const +void Locals::save (ESMWriter &esm) const { for (std::vector >::const_iterator iter (mVariables.begin()); iter!=mVariables.end(); ++iter) @@ -25,3 +28,5 @@ void ESM::Locals::save (ESMWriter &esm) const iter->second.write (esm, Variant::Format_Local); } } + +} diff --git a/components/esm/locals.hpp b/components/esm3/locals.hpp similarity index 100% rename from components/esm/locals.hpp rename to components/esm3/locals.hpp diff --git a/components/esm3/magiceffects.cpp b/components/esm3/magiceffects.cpp new file mode 100644 index 0000000000..a1f943a93d --- /dev/null +++ b/components/esm3/magiceffects.cpp @@ -0,0 +1,35 @@ +#include "magiceffects.hpp" + +#include "esmwriter.hpp" +#include "esmreader.hpp" + +namespace ESM +{ + +void MagicEffects::save(ESMWriter &esm) const +{ + for (const auto& [key, params] : mEffects) + { + esm.writeHNT("EFID", key); + esm.writeHNT("BASE", params.first); + esm.writeHNT("MODI", params.second); + } +} + +void MagicEffects::load(ESMReader &esm) +{ + while (esm.isNextSub("EFID")) + { + int id; + std::pair params; + esm.getHT(id); + esm.getHNT(params.first, "BASE"); + if(esm.getFormat() < 17) + params.second = 0.f; + else + esm.getHNT(params.second, "MODI"); + mEffects.emplace(id, params); + } +} + +} diff --git a/components/esm/magiceffects.hpp b/components/esm3/magiceffects.hpp similarity index 85% rename from components/esm/magiceffects.hpp rename to components/esm3/magiceffects.hpp index a931c68fa5..4b54692c5f 100644 --- a/components/esm/magiceffects.hpp +++ b/components/esm3/magiceffects.hpp @@ -12,8 +12,8 @@ namespace ESM // format 0, saved games only struct MagicEffects { - // - std::map mEffects; + // + std::map> mEffects; void load (ESMReader &esm); void save (ESMWriter &esm) const; @@ -21,12 +21,9 @@ namespace ESM struct SummonKey { - SummonKey(int effectId, const std::string& sourceId, int index) - { - mEffectId = effectId; - mSourceId = sourceId; - mEffectIndex = index; - } + SummonKey(int effectId, const std::string& sourceId, int index): + mEffectId(effectId), mSourceId(sourceId), mEffectIndex(index) + {} bool operator==(const SummonKey &other) const { diff --git a/components/esm3/mappings.cpp b/components/esm3/mappings.cpp new file mode 100644 index 0000000000..4553c1396f --- /dev/null +++ b/components/esm3/mappings.cpp @@ -0,0 +1,134 @@ +#include "mappings.hpp" + +#include + +namespace ESM +{ + BodyPart::MeshPart getMeshPart(PartReferenceType type) + { + switch(type) + { + case PRT_Head: + return BodyPart::MP_Head; + case PRT_Hair: + return BodyPart::MP_Hair; + case PRT_Neck: + return BodyPart::MP_Neck; + case PRT_Cuirass: + return BodyPart::MP_Chest; + case PRT_Groin: + return BodyPart::MP_Groin; + case PRT_RHand: + return BodyPart::MP_Hand; + case PRT_LHand: + return BodyPart::MP_Hand; + case PRT_RWrist: + return BodyPart::MP_Wrist; + case PRT_LWrist: + return BodyPart::MP_Wrist; + case PRT_RForearm: + return BodyPart::MP_Forearm; + case PRT_LForearm: + return BodyPart::MP_Forearm; + case PRT_RUpperarm: + return BodyPart::MP_Upperarm; + case PRT_LUpperarm: + return BodyPart::MP_Upperarm; + case PRT_RFoot: + return BodyPart::MP_Foot; + case PRT_LFoot: + return BodyPart::MP_Foot; + case PRT_RAnkle: + return BodyPart::MP_Ankle; + case PRT_LAnkle: + return BodyPart::MP_Ankle; + case PRT_RKnee: + return BodyPart::MP_Knee; + case PRT_LKnee: + return BodyPart::MP_Knee; + case PRT_RLeg: + return BodyPart::MP_Upperleg; + case PRT_LLeg: + return BodyPart::MP_Upperleg; + case PRT_Tail: + return BodyPart::MP_Tail; + default: + throw std::runtime_error("PartReferenceType " + + std::to_string(type) + " not associated with a mesh part"); + } + } + + std::string getBoneName(PartReferenceType type) + { + switch(type) + { + case PRT_Head: + return "head"; + case PRT_Hair: + return "head"; // This is purposeful. + case PRT_Neck: + return "neck"; + case PRT_Cuirass: + return "chest"; + case PRT_Groin: + return "groin"; + case PRT_Skirt: + return "groin"; + case PRT_RHand: + return "right hand"; + case PRT_LHand: + return "left hand"; + case PRT_RWrist: + return "right wrist"; + case PRT_LWrist: + return "left wrist"; + case PRT_Shield: + return "shield bone"; + case PRT_RForearm: + return "right forearm"; + case PRT_LForearm: + return "left forearm"; + case PRT_RUpperarm: + return "right upper arm"; + case PRT_LUpperarm: + return "left upper arm"; + case PRT_RFoot: + return "right foot"; + case PRT_LFoot: + return "left foot"; + case PRT_RAnkle: + return "right ankle"; + case PRT_LAnkle: + return "left ankle"; + case PRT_RKnee: + return "right knee"; + case PRT_LKnee: + return "left knee"; + case PRT_RLeg: + return "right upper leg"; + case PRT_LLeg: + return "left upper leg"; + case PRT_RPauldron: + return "right clavicle"; + case PRT_LPauldron: + return "left clavicle"; + case PRT_Weapon: + return "weapon bone"; + case PRT_Tail: + return "tail"; + default: + throw std::runtime_error("unknown PartReferenceType"); + } + } + + std::string getMeshFilter(PartReferenceType type) + { + switch(type) + { + case PRT_Hair: + return "hair"; + default: + return getBoneName(type); + } + } +} diff --git a/components/esm3/mappings.hpp b/components/esm3/mappings.hpp new file mode 100644 index 0000000000..a6f0ec3048 --- /dev/null +++ b/components/esm3/mappings.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_ESM_MAPPINGS_H +#define OPENMW_ESM_MAPPINGS_H + +#include + +#include +#include + +namespace ESM +{ + BodyPart::MeshPart getMeshPart(PartReferenceType type); + std::string getBoneName(PartReferenceType type); + std::string getMeshFilter(PartReferenceType type); +} + +#endif diff --git a/components/esm/npcstate.cpp b/components/esm3/npcstate.cpp similarity index 77% rename from components/esm/npcstate.cpp rename to components/esm3/npcstate.cpp index 6c9988d50d..91c6ebbf09 100644 --- a/components/esm/npcstate.cpp +++ b/components/esm3/npcstate.cpp @@ -1,6 +1,9 @@ #include "npcstate.hpp" -void ESM::NpcState::load (ESMReader &esm) +namespace ESM +{ + +void NpcState::load (ESMReader &esm) { ObjectState::load (esm); @@ -14,7 +17,7 @@ void ESM::NpcState::load (ESMReader &esm) } } -void ESM::NpcState::save (ESMWriter &esm, bool inInventory) const +void NpcState::save (ESMWriter &esm, bool inInventory) const { ObjectState::save (esm, inInventory); @@ -28,10 +31,12 @@ void ESM::NpcState::save (ESMWriter &esm, bool inInventory) const } } -void ESM::NpcState::blank() +void NpcState::blank() { ObjectState::blank(); mNpcStats.blank(); mCreatureStats.blank(); mHasCustomState = true; } + +} diff --git a/components/esm/npcstate.hpp b/components/esm3/npcstate.hpp similarity index 100% rename from components/esm/npcstate.hpp rename to components/esm3/npcstate.hpp diff --git a/components/esm/npcstats.cpp b/components/esm3/npcstats.cpp similarity index 90% rename from components/esm/npcstats.cpp rename to components/esm3/npcstats.cpp index 277335e8ca..2896f6a922 100644 --- a/components/esm/npcstats.cpp +++ b/components/esm3/npcstats.cpp @@ -5,9 +5,12 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -ESM::NpcStats::Faction::Faction() : mExpelled (false), mRank (-1), mReputation (0) {} +namespace ESM +{ + +NpcStats::Faction::Faction() : mExpelled (false), mRank (-1), mReputation (0) {} -void ESM::NpcStats::load (ESMReader &esm) +void NpcStats::load (ESMReader &esm) { while (esm.isNextSub ("FACT")) { @@ -41,17 +44,17 @@ void ESM::NpcStats::load (ESMReader &esm) // we have deprecated werewolf skills, stored interleaved // Load into one big vector, then remove every 2nd value mWerewolfDeprecatedData = true; - std::vector > skills(mSkills, mSkills + sizeof(mSkills)/sizeof(mSkills[0])); + std::vector > skills(mSkills, mSkills + sizeof(mSkills)/sizeof(mSkills[0])); for (int i=0; i<27; ++i) { - ESM::StatState skill; + StatState skill; skill.load(esm, intFallback); skills.push_back(skill); } int i=0; - for (std::vector >::iterator it = skills.begin(); it != skills.end(); ++i) + for (std::vector >::iterator it = skills.begin(); it != skills.end(); ++i) { if (i%2 == 1) it = skills.erase(it); @@ -67,7 +70,7 @@ void ESM::NpcStats::load (ESMReader &esm) esm.getHNOT (hasWerewolfAttributes, "HWAT"); if (hasWerewolfAttributes) { - ESM::StatState dummy; + StatState dummy; for (int i=0; i<8; ++i) dummy.load(esm, intFallback); mWerewolfDeprecatedData = true; @@ -122,7 +125,7 @@ void ESM::NpcStats::load (ESMReader &esm) esm.getHNOT (mCrimeId, "CRID"); } -void ESM::NpcStats::save (ESMWriter &esm) const +void NpcStats::save (ESMWriter &esm) const { for (std::map::const_iterator iter (mFactions.begin()); iter!=mFactions.end(); ++iter) @@ -191,7 +194,7 @@ void ESM::NpcStats::save (ESMWriter &esm) const esm.writeHNT ("CRID", mCrimeId); } -void ESM::NpcStats::blank() +void NpcStats::blank() { mWerewolfDeprecatedData = false; mIsWerewolf = false; @@ -207,3 +210,5 @@ void ESM::NpcStats::blank() mTimeToStartDrowning = 20; mCrimeId = -1; } + +} diff --git a/components/esm/npcstats.hpp b/components/esm3/npcstats.hpp similarity index 100% rename from components/esm/npcstats.hpp rename to components/esm3/npcstats.hpp diff --git a/components/esm/objectstate.cpp b/components/esm3/objectstate.cpp similarity index 70% rename from components/esm/objectstate.cpp rename to components/esm3/objectstate.cpp index 9709bf4ff6..30f6c98f38 100644 --- a/components/esm/objectstate.cpp +++ b/components/esm3/objectstate.cpp @@ -3,11 +3,15 @@ #include #include #include +#include #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::ObjectState::load (ESMReader &esm) +namespace ESM +{ + +void ObjectState::load (ESMReader &esm) { mVersion = esm.getFormat(); @@ -20,14 +24,23 @@ void ESM::ObjectState::load (ESMReader &esm) if (mHasLocals) mLocals.load (esm); + mLuaScripts.load(esm); + mEnabled = 1; esm.getHNOT (mEnabled, "ENAB"); mCount = 1; esm.getHNOT (mCount, "COUN"); - mPosition = mRef.mPos; - esm.getHNOT (mPosition, "POS_", 24); + if(esm.isNextSub("POS_")) + { + std::array pos; + esm.getHT(pos); + memcpy(mPosition.pos, pos.data(), sizeof(float) * 3); + memcpy(mPosition.rot, pos.data() + 3, sizeof(float) * 3); + } + else + mPosition = mRef.mPos; if (esm.isNextSub("LROT")) esm.skipHSub(); // local rotation, no longer used @@ -46,7 +59,7 @@ void ESM::ObjectState::load (ESMReader &esm) esm.getHNOT (mHasCustomState, "HCUS"); } -void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const +void ObjectState::save (ESMWriter &esm, bool inInventory) const { mRef.save (esm, true, inInventory); @@ -56,6 +69,8 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const mLocals.save (esm); } + mLuaScripts.save(esm); + if (!mEnabled && !inInventory) esm.writeHNT ("ENAB", mEnabled); @@ -63,7 +78,12 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const esm.writeHNT ("COUN", mCount); if (!inInventory && mPosition != mRef.mPos) - esm.writeHNT ("POS_", mPosition, 24); + { + std::array pos; + memcpy(pos.data(), mPosition.pos, sizeof(float) * 3); + memcpy(pos.data() + 3, mPosition.rot, sizeof(float) * 3); + esm.writeHNT ("POS_", pos, 24); + } if (mFlags != 0) esm.writeHNT ("FLAG", mFlags); @@ -74,7 +94,7 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const esm.writeHNT ("HCUS", false); } -void ESM::ObjectState::blank() +void ObjectState::blank() { mRef.blank(); mHasLocals = 0; @@ -89,74 +109,76 @@ void ESM::ObjectState::blank() mHasCustomState = true; } -const ESM::NpcState& ESM::ObjectState::asNpcState() const +const NpcState& ObjectState::asNpcState() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to NpcState"; throw std::logic_error(error.str()); } -ESM::NpcState& ESM::ObjectState::asNpcState() +NpcState& ObjectState::asNpcState() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to NpcState"; throw std::logic_error(error.str()); } -const ESM::CreatureState& ESM::ObjectState::asCreatureState() const +const CreatureState& ObjectState::asCreatureState() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureState"; throw std::logic_error(error.str()); } -ESM::CreatureState& ESM::ObjectState::asCreatureState() +CreatureState& ObjectState::asCreatureState() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureState"; throw std::logic_error(error.str()); } -const ESM::ContainerState& ESM::ObjectState::asContainerState() const +const ContainerState& ObjectState::asContainerState() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to ContainerState"; throw std::logic_error(error.str()); } -ESM::ContainerState& ESM::ObjectState::asContainerState() +ContainerState& ObjectState::asContainerState() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to ContainerState"; throw std::logic_error(error.str()); } -const ESM::DoorState& ESM::ObjectState::asDoorState() const +const DoorState& ObjectState::asDoorState() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to DoorState"; throw std::logic_error(error.str()); } -ESM::DoorState& ESM::ObjectState::asDoorState() +DoorState& ObjectState::asDoorState() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to DoorState"; throw std::logic_error(error.str()); } -const ESM::CreatureLevListState& ESM::ObjectState::asCreatureLevListState() const +const CreatureLevListState& ObjectState::asCreatureLevListState() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureLevListState"; throw std::logic_error(error.str()); } -ESM::CreatureLevListState& ESM::ObjectState::asCreatureLevListState() +CreatureLevListState& ObjectState::asCreatureLevListState() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureLevListState"; throw std::logic_error(error.str()); } -ESM::ObjectState::~ObjectState() {} +ObjectState::~ObjectState() {} + +} diff --git a/components/esm/objectstate.hpp b/components/esm3/objectstate.hpp similarity index 92% rename from components/esm/objectstate.hpp rename to components/esm3/objectstate.hpp index 6b0fca5ea6..5a9e85b71d 100644 --- a/components/esm/objectstate.hpp +++ b/components/esm3/objectstate.hpp @@ -6,6 +6,7 @@ #include "cellref.hpp" #include "locals.hpp" +#include "components/esm/luascripts.hpp" #include "animationstate.hpp" namespace ESM @@ -27,9 +28,10 @@ namespace ESM unsigned char mHasLocals; Locals mLocals; + LuaScripts mLuaScripts; unsigned char mEnabled; int mCount; - ESM::Position mPosition; + Position mPosition; unsigned int mFlags; // Is there any class-specific state following the ObjectState @@ -37,7 +39,7 @@ namespace ESM unsigned int mVersion; - ESM::AnimationState mAnimationState; + AnimationState mAnimationState; ObjectState() : mHasLocals(0), mEnabled(0), mCount(0) diff --git a/components/esm3/player.cpp b/components/esm3/player.cpp new file mode 100644 index 0000000000..fbf6afec93 --- /dev/null +++ b/components/esm3/player.cpp @@ -0,0 +1,119 @@ +#include "player.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +namespace ESM +{ + +void Player::load (ESMReader &esm) +{ + mObject.mRef.loadId(esm, true); + mObject.load (esm); + + mCellId.load (esm); + + esm.getHNTSized<12>(mLastKnownExteriorPosition, "LKEP"); + + if (esm.isNextSub ("MARK")) + { + mHasMark = true; + esm.getHTSized<24>(mMarkedPosition); + mMarkedCell.load (esm); + } + else + mHasMark = false; + + // Automove, no longer used. + if (esm.isNextSub("AMOV")) + esm.skipHSub(); + + mBirthsign = esm.getHNString ("SIGN"); + + mCurrentCrimeId = -1; + esm.getHNOT (mCurrentCrimeId, "CURD"); + mPaidCrimeId = -1; + esm.getHNOT (mPaidCrimeId, "PAYD"); + + bool checkPrevItems = true; + while (checkPrevItems) + { + std::string boundItemId = esm.getHNOString("BOUN"); + std::string prevItemId = esm.getHNOString("PREV"); + + if (!boundItemId.empty()) + mPreviousItems[boundItemId] = prevItemId; + else + checkPrevItems = false; + } + + if(esm.getFormat() < 19) + { + bool intFallback = esm.getFormat() < 11; + bool clearModified = esm.getFormat() < 17 && !mObject.mNpcStats.mIsWerewolf; + if (esm.hasMoreSubs()) + { + for (int i=0; i attribute; + attribute.load(esm, intFallback); + if (clearModified) + attribute.mMod = 0.f; + mSaveAttributes[i] = attribute.mBase + attribute.mMod - attribute.mDamage; + if (mObject.mNpcStats.mIsWerewolf) + mObject.mCreatureStats.mAttributes[i] = attribute; + } + for (int i=0; i skill; + skill.load(esm, intFallback); + if (clearModified) + skill.mMod = 0.f; + mSaveSkills[i] = skill.mBase + skill.mMod - skill.mDamage; + if (mObject.mNpcStats.mIsWerewolf) + { + if(i == Skill::Acrobatics) + mSetWerewolfAcrobatics = mObject.mNpcStats.mSkills[i].mBase != skill.mBase; + mObject.mNpcStats.mSkills[i] = skill; + } + } + } + } + else + { + mSetWerewolfAcrobatics = false; + esm.getHNT(mSaveAttributes, "WWAT"); + esm.getHNT(mSaveSkills, "WWSK"); + } +} + +void Player::save (ESMWriter &esm) const +{ + mObject.save (esm); + + mCellId.save (esm); + + esm.writeHNT ("LKEP", mLastKnownExteriorPosition); + + if (mHasMark) + { + esm.writeHNT ("MARK", mMarkedPosition, 24); + mMarkedCell.save (esm); + } + + esm.writeHNString ("SIGN", mBirthsign); + + esm.writeHNT ("CURD", mCurrentCrimeId); + esm.writeHNT ("PAYD", mPaidCrimeId); + + for (PreviousItems::const_iterator it=mPreviousItems.begin(); it != mPreviousItems.end(); ++it) + { + esm.writeHNString ("BOUN", it->first); + esm.writeHNString ("PREV", it->second); + } + + esm.writeHNT("WWAT", mSaveAttributes); + esm.writeHNT("WWSK", mSaveSkills); +} + +} diff --git a/components/esm/player.hpp b/components/esm3/player.hpp similarity index 76% rename from components/esm/player.hpp rename to components/esm3/player.hpp index 78bd5ab6e7..6e914b0e9a 100644 --- a/components/esm/player.hpp +++ b/components/esm3/player.hpp @@ -5,10 +5,10 @@ #include "npcstate.hpp" #include "cellid.hpp" -#include "defs.hpp" +#include "components/esm/defs.hpp" #include "loadskil.hpp" -#include "attr.hpp" +#include "components/esm/attr.hpp" namespace ESM { @@ -23,15 +23,16 @@ namespace ESM CellId mCellId; float mLastKnownExteriorPosition[3]; unsigned char mHasMark; - ESM::Position mMarkedPosition; + bool mSetWerewolfAcrobatics; + Position mMarkedPosition; CellId mMarkedCell; std::string mBirthsign; int mCurrentCrimeId; int mPaidCrimeId; - StatState mSaveAttributes[ESM::Attribute::Length]; - StatState mSaveSkills[ESM::Skill::Length]; + float mSaveAttributes[Attribute::Length]; + float mSaveSkills[Skill::Length]; typedef std::map PreviousItems; // previous equipped items, needed for bound spells PreviousItems mPreviousItems; diff --git a/components/esm/projectilestate.cpp b/components/esm3/projectilestate.cpp similarity index 89% rename from components/esm/projectilestate.cpp rename to components/esm3/projectilestate.cpp index 8ade9d5b2e..6a2fcc6675 100644 --- a/components/esm/projectilestate.cpp +++ b/components/esm3/projectilestate.cpp @@ -28,6 +28,7 @@ namespace ESM esm.writeHNString ("SPEL", mSpellId); esm.writeHNT ("SPED", mSpeed); + esm.writeHNT ("SLOT", mSlot); } void MagicBoltState::load(ESMReader &esm) @@ -37,8 +38,12 @@ namespace ESM mSpellId = esm.getHNString("SPEL"); if (esm.isNextSub("SRCN")) // for backwards compatibility esm.skipHSub(); - ESM::EffectList().load(esm); // for backwards compatibility + EffectList().load(esm); // for backwards compatibility esm.getHNT (mSpeed, "SPED"); + if(esm.getFormat() < 17) + mSlot = 0; + else + esm.getHNT(mSlot, "SLOT"); if (esm.isNextSub("STCK")) // for backwards compatibility esm.skipHSub(); if (esm.isNextSub("SOUN")) // for backwards compatibility diff --git a/components/esm/projectilestate.hpp b/components/esm3/projectilestate.hpp similarity index 94% rename from components/esm/projectilestate.hpp rename to components/esm3/projectilestate.hpp index 67ec89bb6d..2b2f0d137b 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm3/projectilestate.hpp @@ -8,7 +8,7 @@ #include "effectlist.hpp" -#include "util.hpp" +#include "components/esm/util.hpp" namespace ESM { @@ -32,6 +32,7 @@ namespace ESM { std::string mSpellId; float mSpeed; + int mSlot; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/queststate.cpp b/components/esm3/queststate.cpp similarity index 74% rename from components/esm/queststate.cpp rename to components/esm3/queststate.cpp index 5408cd2ffd..af7ed81f9e 100644 --- a/components/esm/queststate.cpp +++ b/components/esm3/queststate.cpp @@ -3,16 +3,21 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -void ESM::QuestState::load (ESMReader &esm) +namespace ESM +{ + +void QuestState::load (ESMReader &esm) { mTopic = esm.getHNString ("YETO"); esm.getHNOT (mState, "QSTA"); esm.getHNOT (mFinished, "QFIN"); } -void ESM::QuestState::save (ESMWriter &esm) const +void QuestState::save (ESMWriter &esm) const { esm.writeHNString ("YETO", mTopic); esm.writeHNT ("QSTA", mState); esm.writeHNT ("QFIN", mFinished); } + +} diff --git a/components/esm/queststate.hpp b/components/esm3/queststate.hpp similarity index 100% rename from components/esm/queststate.hpp rename to components/esm3/queststate.hpp diff --git a/components/esm/quickkeys.cpp b/components/esm3/quickkeys.cpp similarity index 100% rename from components/esm/quickkeys.cpp rename to components/esm3/quickkeys.cpp diff --git a/components/esm/quickkeys.hpp b/components/esm3/quickkeys.hpp similarity index 100% rename from components/esm/quickkeys.hpp rename to components/esm3/quickkeys.hpp diff --git a/components/esm3/readerscache.cpp b/components/esm3/readerscache.cpp new file mode 100644 index 0000000000..c3b9d3d03c --- /dev/null +++ b/components/esm3/readerscache.cpp @@ -0,0 +1,87 @@ +#include "readerscache.hpp" + +#include + +namespace ESM +{ + ReadersCache::BusyItem::BusyItem(ReadersCache& owner, std::list::iterator item) noexcept + : mOwner(owner) + , mItem(item) + {} + + ReadersCache::BusyItem::~BusyItem() noexcept + { + mOwner.releaseItem(mItem); + } + + ReadersCache::ReadersCache(std::size_t capacity) + : mCapacity(capacity) + {} + + ReadersCache::BusyItem ReadersCache::get(std::size_t index) + { + const auto indexIt = mIndex.find(index); + std::list::iterator it; + if (indexIt == mIndex.end()) + { + closeExtraReaders(); + it = mBusyItems.emplace(mBusyItems.end()); + mIndex.emplace(index, it); + } + else + { + switch (indexIt->second->mState) + { + case State::Busy: + throw std::logic_error("ESMReader at index " + std::to_string(index) + " is busy"); + case State::Free: + it = indexIt->second; + mBusyItems.splice(mBusyItems.end(), mFreeItems, it); + break; + case State::Closed: + closeExtraReaders(); + it = indexIt->second; + if (it->mName.has_value()) + { + it->mReader.open(*it->mName); + it->mName.reset(); + } + mBusyItems.splice(mBusyItems.end(), mClosedItems, it); + break; + } + it->mState = State::Busy; + } + + return BusyItem(*this, it); + } + + void ReadersCache::closeExtraReaders() + { + while (!mFreeItems.empty() && mBusyItems.size() + mFreeItems.size() + 1 > mCapacity) + { + const auto it = mFreeItems.begin(); + if (it->mReader.isOpen()) + { + it->mName = it->mReader.getName(); + it->mReader.close(); + } + mClosedItems.splice(mClosedItems.end(), mFreeItems, it); + it->mState = State::Closed; + } + } + + void ReadersCache::releaseItem(std::list::iterator it) noexcept + { + assert(it->mState == State::Busy); + if (it->mReader.isOpen()) + { + mFreeItems.splice(mFreeItems.end(), mBusyItems, it); + it->mState = State::Free; + } + else + { + mClosedItems.splice(mClosedItems.end(), mBusyItems, it); + it->mState = State::Closed; + } + } +} diff --git a/components/esm3/readerscache.hpp b/components/esm3/readerscache.hpp new file mode 100644 index 0000000000..16511efdd7 --- /dev/null +++ b/components/esm3/readerscache.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_COMPONENTS_ESM3_READERSCACHE_H +#define OPENMW_COMPONENTS_ESM3_READERSCACHE_H + +#include "esmreader.hpp" + +#include +#include +#include +#include +#include + +namespace ESM +{ + class ReadersCache + { + private: + enum class State + { + Busy, + Free, + Closed, + }; + + struct Item + { + State mState = State::Busy; + ESMReader mReader; + std::optional mName; + + Item() = default; + }; + + public: + class BusyItem + { + public: + explicit BusyItem(ReadersCache& owner, std::list::iterator item) noexcept; + + BusyItem(const BusyItem& other) = delete; + + ~BusyItem() noexcept; + + BusyItem& operator=(const BusyItem& other) = delete; + + ESMReader& operator*() const noexcept { return mItem->mReader; } + + ESMReader* operator->() const noexcept { return &mItem->mReader; } + + private: + ReadersCache& mOwner; + std::list::iterator mItem; + }; + + explicit ReadersCache(std::size_t capacity = 100); + + BusyItem get(std::size_t index); + + private: + const std::size_t mCapacity; + std::map::iterator> mIndex; + std::list mBusyItems; + std::list mFreeItems; + std::list mClosedItems; + + inline void closeExtraReaders(); + + inline void releaseItem(std::list::iterator it) noexcept; + }; +} + +#endif diff --git a/components/esm/savedgame.cpp b/components/esm3/savedgame.cpp similarity index 81% rename from components/esm/savedgame.cpp rename to components/esm3/savedgame.cpp index 9edcb1a671..69da8d087f 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm3/savedgame.cpp @@ -3,10 +3,12 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 15; +namespace ESM +{ + +int SavedGame::sCurrentFormat = 21; -void ESM::SavedGame::load (ESMReader &esm) +void SavedGame::load (ESMReader &esm) { mPlayerName = esm.getHNString("PLNA"); esm.getHNOT (mPlayerLevel, "PLLE"); @@ -15,7 +17,7 @@ void ESM::SavedGame::load (ESMReader &esm) mPlayerClassName = esm.getHNOString("PLCN"); mPlayerCell = esm.getHNString("PLCE"); - esm.getHNT (mInGameTime, "TSTM", 16); + esm.getHNTSized<16>(mInGameTime, "TSTM"); esm.getHNT (mTimePlayed, "TIME"); mDescription = esm.getHNString ("DESC"); @@ -25,10 +27,10 @@ void ESM::SavedGame::load (ESMReader &esm) esm.getSubNameIs("SCRN"); esm.getSubHeader(); mScreenshot.resize(esm.getSubSize()); - esm.getExact(&mScreenshot[0], mScreenshot.size()); + esm.getExact(mScreenshot.data(), mScreenshot.size()); } -void ESM::SavedGame::save (ESMWriter &esm) const +void SavedGame::save (ESMWriter &esm) const { esm.writeHNString ("PLNA", mPlayerName); esm.writeHNT ("PLLE", mPlayerLevel); @@ -51,3 +53,5 @@ void ESM::SavedGame::save (ESMWriter &esm) const esm.write(&mScreenshot[0], mScreenshot.size()); esm.endRecord("SCRN"); } + +} diff --git a/components/esm/savedgame.hpp b/components/esm3/savedgame.hpp similarity index 90% rename from components/esm/savedgame.hpp rename to components/esm3/savedgame.hpp index 26efae824e..a51fd3e817 100644 --- a/components/esm/savedgame.hpp +++ b/components/esm3/savedgame.hpp @@ -4,7 +4,7 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -15,7 +15,7 @@ namespace ESM struct SavedGame { - static unsigned int sRecordId; + constexpr static RecNameInts sRecordId = REC_SAVE; static int sCurrentFormat; diff --git a/components/esm/spelllist.cpp b/components/esm3/spelllist.cpp similarity index 97% rename from components/esm/spelllist.cpp rename to components/esm3/spelllist.cpp index 71c7b340d2..168a8c7448 100644 --- a/components/esm/spelllist.cpp +++ b/components/esm3/spelllist.cpp @@ -3,7 +3,8 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM { +namespace ESM +{ void SpellList::add(ESMReader &esm) { diff --git a/components/esm/spelllist.hpp b/components/esm3/spelllist.hpp similarity index 100% rename from components/esm/spelllist.hpp rename to components/esm3/spelllist.hpp diff --git a/components/esm/spellstate.cpp b/components/esm3/spellstate.cpp similarity index 56% rename from components/esm/spellstate.cpp rename to components/esm3/spellstate.cpp index 2eb1e78679..b1ddb6523c 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm3/spellstate.cpp @@ -8,29 +8,38 @@ namespace ESM void SpellState::load(ESMReader &esm) { - while (esm.isNextSub("SPEL")) + if(esm.getFormat() < 17) { - std::string id = esm.getHString(); - - SpellParams state; - while (esm.isNextSub("INDX")) + while (esm.isNextSub("SPEL")) { - int index; - esm.getHT(index); + std::string id = esm.getHString(); - float magnitude; - esm.getHNT(magnitude, "RAND"); + SpellParams state; + while (esm.isNextSub("INDX")) + { + int index; + esm.getHT(index); - state.mEffectRands[index] = magnitude; - } + float magnitude; + esm.getHNT(magnitude, "RAND"); - while (esm.isNextSub("PURG")) { - int index; - esm.getHT(index); - state.mPurgedEffects.insert(index); - } + state.mEffectRands[index] = magnitude; + } - mSpells[id] = state; + while (esm.isNextSub("PURG")) { + int index; + esm.getHT(index); + state.mPurgedEffects.insert(index); + } + + mSpellParams[id] = state; + mSpells.emplace_back(id); + } + } + else + { + while (esm.isNextSub("SPEL")) + mSpells.emplace_back(esm.getHString()); } // Obsolete @@ -88,30 +97,8 @@ namespace ESM void SpellState::save(ESMWriter &esm) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - esm.writeHNString("SPEL", it->first); - - const std::map& random = it->second.mEffectRands; - for (std::map::const_iterator rIt = random.begin(); rIt != random.end(); ++rIt) - { - esm.writeHNT("INDX", rIt->first); - esm.writeHNT("RAND", rIt->second); - } - - const std::set& purges = it->second.mPurgedEffects; - for (std::set::const_iterator pIt = purges.begin(); pIt != purges.end(); ++pIt) - esm.writeHNT("PURG", *pIt); - } - - for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - esm.writeHNString("CORP", it->first); - - const CorprusStats & stats = it->second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } + for (const std::string& spell : mSpells) + esm.writeHNString("SPEL", spell); for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { diff --git a/components/esm/spellstate.hpp b/components/esm3/spellstate.hpp similarity index 76% rename from components/esm/spellstate.hpp rename to components/esm3/spellstate.hpp index 55c57611a2..bb909c506c 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm3/spellstate.hpp @@ -6,7 +6,7 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { @@ -31,13 +31,13 @@ namespace ESM struct SpellParams { - std::map mEffectRands; - std::set mPurgedEffects; + std::map mEffectRands; // + std::set mPurgedEffects; // indices of purged effects }; - typedef std::map TContainer; - TContainer mSpells; + std::vector mSpells; // FIXME: obsolete, used only for old saves + std::map mSpellParams; std::map > mPermanentSpellEffects; std::map mCorprusSpells; diff --git a/components/esm/statstate.cpp b/components/esm3/statstate.cpp similarity index 84% rename from components/esm/statstate.cpp rename to components/esm3/statstate.cpp index b9ddc3efd7..4e37df7d79 100644 --- a/components/esm/statstate.cpp +++ b/components/esm3/statstate.cpp @@ -16,17 +16,16 @@ namespace ESM { int base = 0; esm.getHNT(base, "STBA"); - mBase = static_cast(base); + mBase = static_cast(base); int mod = 0; esm.getHNOT(mod, "STMO"); - mMod = static_cast(mod); + mMod = static_cast(mod); int current = 0; esm.getHNOT(current, "STCU"); - mCurrent = static_cast(current); + mCurrent = static_cast(current); - // mDamage was changed to a float; ensure backwards compatibility int oldDamage = 0; esm.getHNOT(oldDamage, "STDA"); mDamage = static_cast(oldDamage); @@ -71,7 +70,7 @@ namespace ESM if (mProgress) esm.writeHNT("STPR", mProgress); } -} -template struct ESM::StatState; -template struct ESM::StatState; + template struct StatState; + template struct StatState; +} diff --git a/components/esm/statstate.hpp b/components/esm3/statstate.hpp similarity index 100% rename from components/esm/statstate.hpp rename to components/esm3/statstate.hpp diff --git a/components/esm/stolenitems.cpp b/components/esm3/stolenitems.cpp similarity index 77% rename from components/esm/stolenitems.cpp rename to components/esm3/stolenitems.cpp index c51b0b99b0..a43ae61a60 100644 --- a/components/esm/stolenitems.cpp +++ b/components/esm3/stolenitems.cpp @@ -1,7 +1,7 @@ #include "stolenitems.hpp" -#include -#include +#include +#include namespace ESM { @@ -32,15 +32,14 @@ namespace ESM std::map, int> ownerMap; while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) { - std::string subname = esm.retSubName().toString(); + const bool isFaction = (esm.retSubName() == "FNAM"); std::string owner = esm.getHString(); - bool isFaction = (subname == "FNAM"); int count; esm.getHNT(count, "COUN"); - ownerMap.insert(std::make_pair(std::make_pair(owner, isFaction), count)); + ownerMap.emplace(std::make_pair(std::move(owner), isFaction), count); } - mStolenItems[itemid] = ownerMap; + mStolenItems.insert_or_assign(std::move(itemid), std::move(ownerMap)); } } diff --git a/components/esm/stolenitems.hpp b/components/esm3/stolenitems.hpp similarity index 82% rename from components/esm/stolenitems.hpp rename to components/esm3/stolenitems.hpp index 928fbbf757..cd7fe5a6c8 100644 --- a/components/esm/stolenitems.hpp +++ b/components/esm3/stolenitems.hpp @@ -15,8 +15,8 @@ namespace ESM typedef std::map, int> > StolenItemsMap; StolenItemsMap mStolenItems; - void load(ESM::ESMReader& esm); - void write(ESM::ESMWriter& esm) const; + void load(ESMReader& esm); + void write(ESMWriter& esm) const; }; } diff --git a/components/esm/transport.cpp b/components/esm3/transport.cpp similarity index 78% rename from components/esm/transport.cpp rename to components/esm3/transport.cpp index 11676ea723..b79818a228 100644 --- a/components/esm/transport.cpp +++ b/components/esm3/transport.cpp @@ -2,21 +2,21 @@ #include -#include -#include +#include +#include namespace ESM { void Transport::add(ESMReader &esm) { - if (esm.retSubName().intval == ESM::FourCC<'D','O','D','T'>::value) + if (esm.retSubName().toInt() == fourCC("DODT")) { Dest dodt; esm.getHExact(&dodt.mPos, 24); mList.push_back(dodt); } - else if (esm.retSubName().intval == ESM::FourCC<'D','N','A','M'>::value) + else if (esm.retSubName().toInt() == fourCC("DNAM")) { const std::string name = esm.getHString(); if (mList.empty()) diff --git a/components/esm/transport.hpp b/components/esm3/transport.hpp similarity index 94% rename from components/esm/transport.hpp rename to components/esm3/transport.hpp index 10d4013f72..f1e5f2103b 100644 --- a/components/esm/transport.hpp +++ b/components/esm3/transport.hpp @@ -4,7 +4,7 @@ #include #include -#include "defs.hpp" +#include "components/esm/defs.hpp" namespace ESM { diff --git a/components/esm3/variant.cpp b/components/esm3/variant.cpp new file mode 100644 index 0000000000..b7dc293f53 --- /dev/null +++ b/components/esm3/variant.cpp @@ -0,0 +1,277 @@ +#include "variant.hpp" + +#include +#include + +#include "esmreader.hpp" +#include "variantimp.hpp" + +#include "components/esm/defs.hpp" + +namespace ESM +{ +namespace +{ + constexpr uint32_t STRV = fourCC("STRV"); + constexpr uint32_t INTV = fourCC("INTV"); + constexpr uint32_t FLTV = fourCC("FLTV"); + constexpr uint32_t STTV = fourCC("STTV"); + + template + struct GetValue + { + constexpr T operator()(int value) const { return static_cast(value); } + + constexpr T operator()(float value) const { return static_cast(value); } + + template + constexpr T operator()(const V&) const + { + if constexpr (orDefault) + return T {}; + else + throw std::runtime_error("cannot convert variant"); + } + }; + + template + struct SetValue + { + T mValue; + + explicit SetValue(T value) : mValue(value) {} + + void operator()(int& value) const { value = static_cast(mValue); } + + void operator()(float& value) const { value = static_cast(mValue); } + + template + void operator()(V&) const { throw std::runtime_error("cannot convert variant"); } + }; +} + +const std::string& Variant::getString() const +{ + return std::get(mData); +} + +int Variant::getInteger() const +{ + return std::visit(GetValue{}, mData); +} + +float Variant::getFloat() const +{ + return std::visit(GetValue{}, mData); +} + +void Variant::read (ESMReader& esm, Format format) +{ + // type + VarType type = VT_Unknown; + + if (format==Format_Global) + { + std::string typeId = esm.getHNString ("FNAM"); + + if (typeId == "s") + type = VT_Short; + else if (typeId == "l") + type = VT_Long; + else if (typeId == "f") + type = VT_Float; + else + esm.fail ("illegal global variable type " + typeId); + } + else if (format==Format_Gmst) + { + if (!esm.hasMoreSubs()) + { + type = VT_None; + } + else + { + esm.getSubName(); + NAME name = esm.retSubName(); + + + + if (name==STRV) + { + type = VT_String; + } + else if (name==INTV) + { + type = VT_Int; + } + else if (name==FLTV) + { + type = VT_Float; + } + else + esm.fail ("invalid subrecord: " + name.toString()); + } + } + else if (format == Format_Info) + { + esm.getSubName(); + NAME name = esm.retSubName(); + + if (name==INTV) + { + type = VT_Int; + } + else if (name==FLTV) + { + type = VT_Float; + } + else + esm.fail ("invalid subrecord: " + name.toString()); + } + else if (format == Format_Local) + { + esm.getSubName(); + NAME name = esm.retSubName(); + + if (name==INTV) + { + type = VT_Int; + } + else if (name==FLTV) + { + type = VT_Float; + } + else if (name==STTV) + { + type = VT_Short; + } + else + esm.fail ("invalid subrecord: " + name.toString()); + } + + setType (type); + + std::visit(ReadESMVariantValue {esm, format, mType}, mData); +} + +void Variant::write (ESMWriter& esm, Format format) const +{ + if (mType==VT_Unknown) + { + throw std::runtime_error ("can not serialise variant of unknown type"); + } + else if (mType==VT_None) + { + if (format==Format_Global) + throw std::runtime_error ("can not serialise variant of type none to global format"); + + if (format==Format_Info) + throw std::runtime_error ("can not serialise variant of type none to info format"); + + if (format==Format_Local) + throw std::runtime_error ("can not serialise variant of type none to local format"); + + // nothing to do here for GMST format + } + else + std::visit(WriteESMVariantValue {esm, format, mType}, mData); +} + +void Variant::write (std::ostream& stream) const +{ + switch (mType) + { + case VT_Unknown: + + stream << "variant unknown"; + break; + + case VT_None: + + stream << "variant none"; + break; + + case VT_Short: + + stream << "variant short: " << std::get(mData); + break; + + case VT_Int: + + stream << "variant int: " << std::get(mData); + break; + + case VT_Long: + + stream << "variant long: " << std::get(mData); + break; + + case VT_Float: + + stream << "variant float: " << std::get(mData); + break; + + case VT_String: + + stream << "variant string: \"" << std::get(mData) << "\""; + break; + } +} + +void Variant::setType (VarType type) +{ + if (type!=mType) + { + switch (type) + { + case VT_Unknown: + case VT_None: + mData = std::monostate {}; + break; + + case VT_Short: + case VT_Int: + case VT_Long: + mData = std::visit(GetValue{}, mData); + break; + + case VT_Float: + mData = std::visit(GetValue{}, mData); + break; + + case VT_String: + mData = std::string {}; + break; + } + + mType = type; + } +} + +void Variant::setString (const std::string& value) +{ + std::get(mData) = value; +} + +void Variant::setString (std::string&& value) +{ + std::get(mData) = std::move(value); +} + +void Variant::setInteger (int value) +{ + std::visit(SetValue(value), mData); +} + +void Variant::setFloat (float value) +{ + std::visit(SetValue(value), mData); +} + +std::ostream& operator<< (std::ostream& stream, const Variant& value) +{ + value.write (stream); + return stream; +} + +} diff --git a/components/esm/variant.hpp b/components/esm3/variant.hpp similarity index 61% rename from components/esm/variant.hpp rename to components/esm3/variant.hpp index 5f179a7bdc..3d706c163f 100644 --- a/components/esm/variant.hpp +++ b/components/esm3/variant.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include namespace ESM { @@ -20,12 +22,10 @@ namespace ESM VT_String }; - class VariantDataBase; - class Variant { VarType mType; - VariantDataBase *mData; + std::variant mData; public: @@ -37,21 +37,19 @@ namespace ESM Format_Local // local script variables in save game files }; - Variant(); + Variant() : mType (VT_None), mData (std::monostate{}) {} - Variant (const std::string& value); - Variant (int value); - Variant (float value); + explicit Variant(const std::string& value) : mType(VT_String), mData(value) {} - ~Variant(); + explicit Variant(std::string&& value) : mType(VT_String), mData(std::move(value)) {} - Variant& operator= (const Variant& variant); + explicit Variant(int value) : mType(VT_Long), mData(value) {} - Variant (const Variant& variant); + explicit Variant(float value) : mType(VT_Float), mData(value) {} - VarType getType() const; + VarType getType() const { return mType; } - std::string getString() const; + const std::string& getString() const; ///< Will throw an exception, if value can not be represented as a string. int getInteger() const; @@ -73,19 +71,27 @@ namespace ESM void setString (const std::string& value); ///< Will throw an exception, if type is not compatible with string. + void setString (std::string&& value); + ///< Will throw an exception, if type is not compatible with string. + void setInteger (int value); ///< Will throw an exception, if type is not compatible with integer. void setFloat (float value); ///< Will throw an exception, if type is not compatible with float. - bool isEqual (const Variant& value) const; + friend bool operator==(const Variant& left, const Variant& right) + { + return std::tie(left.mType, left.mData) == std::tie(right.mType, right.mData); + } + + friend bool operator!=(const Variant& left, const Variant& right) + { + return !(left == right); + } }; std::ostream& operator<<(std::ostream& stream, const Variant& value); - - bool operator== (const Variant& left, const Variant& right); - bool operator!= (const Variant& left, const Variant& right); } #endif diff --git a/components/esm3/variantimp.cpp b/components/esm3/variantimp.cpp new file mode 100644 index 0000000000..7116ce7a74 --- /dev/null +++ b/components/esm3/variantimp.cpp @@ -0,0 +1,170 @@ +#include "variantimp.hpp" + +#include +#include +#include + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +namespace ESM +{ + +void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, std::string& out) +{ + if (type!=VT_String) + throw std::logic_error ("not a string type"); + + if (format==Variant::Format_Global) + esm.fail ("global variables of type string not supported"); + + if (format==Variant::Format_Info) + esm.fail ("info variables of type string not supported"); + + if (format==Variant::Format_Local) + esm.fail ("local variables of type string not supported"); + + // GMST + out = esm.getHString(); +} + +void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, const std::string& in) +{ + if (type!=VT_String) + throw std::logic_error ("not a string type"); + + if (format==Variant::Format_Global) + throw std::runtime_error ("global variables of type string not supported"); + + if (format==Variant::Format_Info) + throw std::runtime_error ("info variables of type string not supported"); + + if (format==Variant::Format_Local) + throw std::runtime_error ("local variables of type string not supported"); + + // GMST + esm.writeHNString("STRV", in); +} + +void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, int& out) +{ + if (type!=VT_Short && type!=VT_Long && type!=VT_Int) + throw std::logic_error ("not an integer type"); + + if (format==Variant::Format_Global) + { + float value; + esm.getHNT (value, "FLTV"); + + if (type==VT_Short) + if (std::isnan(value)) + out = 0; + else + out = static_cast (value); + else if (type==VT_Long) + out = static_cast (value); + else + esm.fail ("unsupported global variable integer type"); + } + else if (format==Variant::Format_Gmst || format==Variant::Format_Info) + { + if (type!=VT_Int) + { + std::ostringstream stream; + stream + << "unsupported " <<(format==Variant::Format_Gmst ? "gmst" : "info") + << " variable integer type"; + esm.fail (stream.str()); + } + + esm.getHT(out); + } + else if (format==Variant::Format_Local) + { + if (type==VT_Short) + { + short value; + esm.getHT(value); + out = value; + } + else if (type==VT_Int) + { + esm.getHT(out); + } + else + esm.fail("unsupported local variable integer type"); + } +} + +void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, int in) +{ + if (type!=VT_Short && type!=VT_Long && type!=VT_Int) + throw std::logic_error ("not an integer type"); + + if (format==Variant::Format_Global) + { + if (type==VT_Short || type==VT_Long) + { + float value = static_cast(in); + esm.writeHNString ("FNAM", type==VT_Short ? "s" : "l"); + esm.writeHNT ("FLTV", value); + } + else + throw std::runtime_error ("unsupported global variable integer type"); + } + else if (format==Variant::Format_Gmst || format==Variant::Format_Info) + { + if (type!=VT_Int) + { + std::ostringstream stream; + stream + << "unsupported " <<(format==Variant::Format_Gmst ? "gmst" : "info") + << " variable integer type"; + throw std::runtime_error (stream.str()); + } + + esm.writeHNT("INTV", in); + } + else if (format==Variant::Format_Local) + { + if (type==VT_Short) + esm.writeHNT("STTV", static_cast(in)); + else if (type == VT_Int) + esm.writeHNT("INTV", in); + else + throw std::runtime_error("unsupported local variable integer type"); + } +} + +void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, float& out) +{ + if (type!=VT_Float) + throw std::logic_error ("not a float type"); + + if (format==Variant::Format_Global) + { + esm.getHNT(out, "FLTV"); + } + else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) + { + esm.getHT(out); + } +} + +void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, float in) +{ + if (type!=VT_Float) + throw std::logic_error ("not a float type"); + + if (format==Variant::Format_Global) + { + esm.writeHNString ("FNAM", "f"); + esm.writeHNT("FLTV", in); + } + else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) + { + esm.writeHNT("FLTV", in); + } +} + +} diff --git a/components/esm3/variantimp.hpp b/components/esm3/variantimp.hpp new file mode 100644 index 0000000000..9458728115 --- /dev/null +++ b/components/esm3/variantimp.hpp @@ -0,0 +1,60 @@ +#ifndef OPENMW_ESM_VARIANTIMP_H +#define OPENMW_ESM_VARIANTIMP_H + +#include +#include + +#include "variant.hpp" + +namespace ESM +{ + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, std::string& value); + + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, float& value); + + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int& value); + + void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, const std::string& value); + + void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, float value); + + void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int value); + + struct ReadESMVariantValue + { + std::reference_wrapper mReader; + Variant::Format mFormat; + VarType mType; + + ReadESMVariantValue(ESMReader& reader, Variant::Format format, VarType type) + : mReader(reader), mFormat(format), mType(type) {} + + void operator()(std::monostate) const {} + + template + void operator()(T& value) const + { + readESMVariantValue(mReader.get(), mFormat, mType, value); + } + }; + + struct WriteESMVariantValue + { + std::reference_wrapper mWriter; + Variant::Format mFormat; + VarType mType; + + WriteESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type) + : mWriter(writer), mFormat(format), mType(type) {} + + void operator()(std::monostate) const {} + + template + void operator()(const T& value) const + { + writeESMVariantValue(mWriter.get(), mFormat, mType, value); + } + }; +} + +#endif diff --git a/components/esm/weatherstate.cpp b/components/esm3/weatherstate.cpp similarity index 66% rename from components/esm/weatherstate.cpp rename to components/esm3/weatherstate.cpp index ff2528e58f..b149c2bfe0 100644 --- a/components/esm/weatherstate.cpp +++ b/components/esm3/weatherstate.cpp @@ -3,19 +3,22 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +namespace ESM +{ namespace { - const char* currentRegionRecord = "CREG"; - const char* timePassedRecord = "TMPS"; - const char* fastForwardRecord = "FAST"; - const char* weatherUpdateTimeRecord = "WUPD"; - const char* transitionFactorRecord = "TRFC"; - const char* currentWeatherRecord = "CWTH"; - const char* nextWeatherRecord = "NWTH"; - const char* queuedWeatherRecord = "QWTH"; - const char* regionNameRecord = "RGNN"; - const char* regionWeatherRecord = "RGNW"; - const char* regionChanceRecord = "RGNC"; + constexpr NAME currentRegionRecord = "CREG"; + constexpr NAME timePassedRecord = "TMPS"; + constexpr NAME fastForwardRecord = "FAST"; + constexpr NAME weatherUpdateTimeRecord = "WUPD"; + constexpr NAME transitionFactorRecord = "TRFC"; + constexpr NAME currentWeatherRecord = "CWTH"; + constexpr NAME nextWeatherRecord = "NWTH"; + constexpr NAME queuedWeatherRecord = "QWTH"; + constexpr NAME regionNameRecord = "RGNN"; + constexpr NAME regionWeatherRecord = "RGNW"; + constexpr NAME regionChanceRecord = "RGNC"; +} } namespace ESM @@ -31,15 +34,15 @@ namespace ESM esm.getHNT(mNextWeather, nextWeatherRecord); esm.getHNT(mQueuedWeather, queuedWeatherRecord); - while(esm.peekNextSub(regionNameRecord)) + while (esm.isNextSub(regionNameRecord)) { - std::string regionID = esm.getHNString(regionNameRecord); + std::string regionID = esm.getHString(); RegionWeatherState region; esm.getHNT(region.mWeather, regionWeatherRecord); - while(esm.peekNextSub(regionChanceRecord)) + while (esm.isNextSub(regionChanceRecord)) { char chance; - esm.getHNT(chance, regionChanceRecord); + esm.getHT(chance); region.mChances.push_back(chance); } @@ -49,7 +52,7 @@ namespace ESM void WeatherState::save(ESMWriter& esm) const { - esm.writeHNCString(currentRegionRecord, mCurrentRegion.c_str()); + esm.writeHNCString(currentRegionRecord, mCurrentRegion); esm.writeHNT(timePassedRecord, mTimePassed); esm.writeHNT(fastForwardRecord, mFastForward); esm.writeHNT(weatherUpdateTimeRecord, mWeatherUpdateTime); diff --git a/components/esm/weatherstate.hpp b/components/esm3/weatherstate.hpp similarity index 100% rename from components/esm/weatherstate.hpp rename to components/esm3/weatherstate.hpp diff --git a/components/esmterrain/storage.cpp b/components/esm3terrain/storage.cpp similarity index 98% rename from components/esmterrain/storage.cpp rename to components/esm3terrain/storage.cpp index cf27b7128d..4d3f3c3059 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esm3terrain/storage.cpp @@ -71,7 +71,7 @@ namespace ESMTerrain int endColumn = startColumn + size * (ESM::Land::LAND_SIZE-1) + 1; osg::ref_ptr land = getLand (cellX, cellY); - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0; + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; if (data) { min = std::numeric_limits::max(); @@ -119,7 +119,7 @@ namespace ESMTerrain } const LandObject* land = getLand(cellX, cellY, cache); - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VNML) : 0; + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VNML) : nullptr; if (data) { normal.x() = data->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; @@ -156,7 +156,7 @@ namespace ESMTerrain } const LandObject* land = getLand(cellX, cellY, cache); - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VCLR) : 0; + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VCLR) : nullptr; if (data) { color.r() = data->mColours[col*ESM::Land::LAND_SIZE*3+row*3]; @@ -207,9 +207,9 @@ namespace ESMTerrain for (int cellX = startCellX; cellX < startCellX + std::ceil(size); ++cellX) { const LandObject* land = getLand(cellX, cellY, cache); - const ESM::Land::LandData *heightData = 0; - const ESM::Land::LandData *normalData = 0; - const ESM::Land::LandData *colourData = 0; + const ESM::Land::LandData *heightData = nullptr; + const ESM::Land::LandData *normalData = nullptr; + const ESM::Land::LandData *colourData = nullptr; if (land) { heightData = land->getData(ESM::Land::DATA_VHGT); @@ -341,7 +341,7 @@ namespace ESMTerrain const LandObject* land = getLand(cellX, cellY, cache); - const ESM::Land::LandData *data = land ? land->getData(ESM::Land::DATA_VTEX) : 0; + const ESM::Land::LandData *data = land ? land->getData(ESM::Land::DATA_VTEX) : nullptr; if (data) { int tex = data->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; diff --git a/components/esmterrain/storage.hpp b/components/esm3terrain/storage.hpp similarity index 97% rename from components/esmterrain/storage.hpp rename to components/esm3terrain/storage.hpp index 68e71574ee..a2ac625c6c 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esm3terrain/storage.hpp @@ -6,8 +6,8 @@ #include -#include -#include +#include +#include namespace VFS { @@ -36,11 +36,7 @@ namespace ESMTerrain return nullptr; return &mData; } - - inline int getPlugin() const - { - return mLand->mPlugin; - } + inline int getPlugin() const { return mLand->getPlugin(); } private: const ESM::Land* mLand; diff --git a/components/esm4/actor.hpp b/components/esm4/actor.hpp new file mode 100644 index 0000000000..ae56a49564 --- /dev/null +++ b/components/esm4/actor.hpp @@ -0,0 +1,163 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ACTOR_H +#define ESM4_ACTOR_H + +#include + +#include "formid.hpp" + +namespace ESM4 +{ +#pragma pack(push, 1) + struct AIData // NPC_, CREA + { + std::uint8_t aggression; + std::uint8_t confidence; + std::uint8_t energyLevel; + std::uint8_t responsibility; + std::uint32_t aiFlags; + std::uint8_t trainSkill; + std::uint8_t trainLevel; + std::uint16_t unknown; + }; + + struct AttributeValues + { + std::uint8_t strength; + std::uint8_t intelligence; + std::uint8_t willpower; + std::uint8_t agility; + std::uint8_t speed; + std::uint8_t endurance; + std::uint8_t personality; + std::uint8_t luck; + }; + + struct ACBS_TES4 + { + std::uint32_t flags; + std::uint16_t baseSpell; + std::uint16_t fatigue; + std::uint16_t barterGold; + std::int16_t levelOrOffset; + std::uint16_t calcMin; + std::uint16_t calcMax; + std::uint32_t padding1; + std::uint32_t padding2; + }; + + struct ACBS_FO3 + { + std::uint32_t flags; + std::uint16_t fatigue; + std::uint16_t barterGold; + std::int16_t levelOrMult; + std::uint16_t calcMinlevel; + std::uint16_t calcMaxlevel; + std::uint16_t speedMultiplier; + float karma; + std::int16_t dispositionBase; + std::uint16_t templateFlags; + }; + + struct ACBS_TES5 + { + std::uint32_t flags; + std::uint16_t magickaOffset; + std::uint16_t staminaOffset; + std::uint16_t levelOrMult; // TODO: check if int16_t + std::uint16_t calcMinlevel; + std::uint16_t calcMaxlevel; + std::uint16_t speedMultiplier; + std::uint16_t dispositionBase; // TODO: check if int16_t + std::uint16_t templateFlags; + std::uint16_t healthOffset; + std::uint16_t bleedoutOverride; + }; + + union ActorBaseConfig + { + ACBS_TES4 tes4; + ACBS_FO3 fo3; + ACBS_TES5 tes5; + }; + + struct ActorFaction + { + FormId faction; + std::int8_t rank; + std::uint8_t unknown1; + std::uint8_t unknown2; + std::uint8_t unknown3; + }; +#pragma pack(pop) + + struct BodyTemplate // TES5 + { + // 0x00000001 - Head + // 0x00000002 - Hair + // 0x00000004 - Body + // 0x00000008 - Hands + // 0x00000010 - Forearms + // 0x00000020 - Amulet + // 0x00000040 - Ring + // 0x00000080 - Feet + // 0x00000100 - Calves + // 0x00000200 - Shield + // 0x00000400 - Tail + // 0x00000800 - Long Hair + // 0x00001000 - Circlet + // 0x00002000 - Ears + // 0x00004000 - Body AddOn 3 + // 0x00008000 - Body AddOn 4 + // 0x00010000 - Body AddOn 5 + // 0x00020000 - Body AddOn 6 + // 0x00040000 - Body AddOn 7 + // 0x00080000 - Body AddOn 8 + // 0x00100000 - Decapitate Head + // 0x00200000 - Decapitate + // 0x00400000 - Body AddOn 9 + // 0x00800000 - Body AddOn 10 + // 0x01000000 - Body AddOn 11 + // 0x02000000 - Body AddOn 12 + // 0x04000000 - Body AddOn 13 + // 0x08000000 - Body AddOn 14 + // 0x10000000 - Body AddOn 15 + // 0x20000000 - Body AddOn 16 + // 0x40000000 - Body AddOn 17 + // 0x80000000 - FX01 + std::uint32_t bodyPart; + std::uint8_t flags; + std::uint8_t unknown1; // probably padding + std::uint8_t unknown2; // probably padding + std::uint8_t unknown3; // probably padding + std::uint32_t type; // 0 = light, 1 = heavy, 2 = none (cloth?) + }; +} + +#endif // ESM4_ACTOR_H diff --git a/components/esm4/common.cpp b/components/esm4/common.cpp new file mode 100644 index 0000000000..752660fd85 --- /dev/null +++ b/components/esm4/common.cpp @@ -0,0 +1,100 @@ +/* + Copyright (C) 2015-2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "common.hpp" + +#include +#include +#include + +#include + +#include "formid.hpp" + +namespace ESM4 +{ + const char *sGroupType[] = + { + "Record Type", "World Child", "Interior Cell", "Interior Sub Cell", "Exterior Cell", + "Exterior Sub Cell", "Cell Child", "Topic Child", "Cell Persistent Child", + "Cell Temporary Child", "Cell Visible Dist Child", "Unknown" + }; + + std::string printLabel(const GroupLabel& label, const std::uint32_t type) + { + std::ostringstream ss; + ss << std::string(sGroupType[std::min(type, (uint32_t)11)]); // avoid out of range + + switch (type) + { + case ESM4::Grp_RecordType: + { + ss << ": " << std::string((char*)label.recordType, 4); + break; + } + case ESM4::Grp_ExteriorCell: + case ESM4::Grp_ExteriorSubCell: + { + //short x, y; + //y = label & 0xff; + //x = (label >> 16) & 0xff; + ss << ": grid (x, y) " << std::dec << label.grid[1] << ", " << label.grid[0]; + + break; + } + case ESM4::Grp_InteriorCell: + case ESM4::Grp_InteriorSubCell: + { + ss << ": block 0x" << std::hex << label.value; + break; + } + case ESM4::Grp_WorldChild: + case ESM4::Grp_CellChild: + case ESM4::Grp_TopicChild: + case ESM4::Grp_CellPersistentChild: + case ESM4::Grp_CellTemporaryChild: + case ESM4::Grp_CellVisibleDistChild: + { + ss << ": FormId 0x" << formIdToString(label.value); + break; + } + default: + break; + } + + return ss.str(); + } + + void gridToString(std::int16_t x, std::int16_t y, std::string& str) + { + char buf[6+6+2+1]; // longest signed 16 bit number is 6 characters (-32768) + int res = snprintf(buf, 6+6+2+1, "#%d %d", x, y); + if (res > 0 && res < 6+6+2+1) + str.assign(buf); + else + throw std::runtime_error("possible buffer overflow while converting grid"); + } +} diff --git a/components/esm4/common.hpp b/components/esm4/common.hpp new file mode 100644 index 0000000000..11286e4b03 --- /dev/null +++ b/components/esm4/common.hpp @@ -0,0 +1,935 @@ +/* + Copyright (C) 2015-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_COMMON_H +#define ESM4_COMMON_H + +#include +#include + +#include + +#include "formid.hpp" + +namespace ESM4 +{ + using ESM::fourCC; + + // Based on http://www.uesp.net/wiki/Tes5Mod:Mod_File_Format + enum RecordTypes + { + REC_AACT = fourCC("AACT"), // Action + REC_ACHR = fourCC("ACHR"), // Actor Reference + REC_ACTI = fourCC("ACTI"), // Activator + REC_ADDN = fourCC("ADDN"), // Addon Node + REC_ALCH = fourCC("ALCH"), // Potion + REC_AMMO = fourCC("AMMO"), // Ammo + REC_ANIO = fourCC("ANIO"), // Animated Object + REC_APPA = fourCC("APPA"), // Apparatus (probably unused) + REC_ARMA = fourCC("ARMA"), // Armature (Model) + REC_ARMO = fourCC("ARMO"), // Armor + REC_ARTO = fourCC("ARTO"), // Art Object + REC_ASPC = fourCC("ASPC"), // Acoustic Space + REC_ASTP = fourCC("ASTP"), // Association Type + REC_AVIF = fourCC("AVIF"), // Actor Values/Perk Tree Graphics + REC_BOOK = fourCC("BOOK"), // Book + REC_BPTD = fourCC("BPTD"), // Body Part Data + REC_CAMS = fourCC("CAMS"), // Camera Shot + REC_CELL = fourCC("CELL"), // Cell + REC_CLAS = fourCC("CLAS"), // Class + REC_CLFM = fourCC("CLFM"), // Color + REC_CLMT = fourCC("CLMT"), // Climate + REC_CLOT = fourCC("CLOT"), // Clothing + REC_COBJ = fourCC("COBJ"), // Constructible Object (recipes) + REC_COLL = fourCC("COLL"), // Collision Layer + REC_CONT = fourCC("CONT"), // Container + REC_CPTH = fourCC("CPTH"), // Camera Path + REC_CREA = fourCC("CREA"), // Creature + REC_CSTY = fourCC("CSTY"), // Combat Style + REC_DEBR = fourCC("DEBR"), // Debris + REC_DIAL = fourCC("DIAL"), // Dialog Topic + REC_DLBR = fourCC("DLBR"), // Dialog Branch + REC_DLVW = fourCC("DLVW"), // Dialog View + REC_DOBJ = fourCC("DOBJ"), // Default Object Manager + REC_DOOR = fourCC("DOOR"), // Door + REC_DUAL = fourCC("DUAL"), // Dual Cast Data (possibly unused) + REC_ECZN = fourCC("ECZN"), // Encounter Zone + REC_EFSH = fourCC("EFSH"), // Effect Shader + REC_ENCH = fourCC("ENCH"), // Enchantment + REC_EQUP = fourCC("EQUP"), // Equip Slot (flag-type values) + REC_EXPL = fourCC("EXPL"), // Explosion + REC_EYES = fourCC("EYES"), // Eyes + REC_FACT = fourCC("FACT"), // Faction + REC_FLOR = fourCC("FLOR"), // Flora + REC_FLST = fourCC("FLST"), // Form List (non-levelled list) + REC_FSTP = fourCC("FSTP"), // Footstep + REC_FSTS = fourCC("FSTS"), // Footstep Set + REC_FURN = fourCC("FURN"), // Furniture + REC_GLOB = fourCC("GLOB"), // Global Variable + REC_GMST = fourCC("GMST"), // Game Setting + REC_GRAS = fourCC("GRAS"), // Grass + REC_GRUP = fourCC("GRUP"), // Form Group + REC_HAIR = fourCC("HAIR"), // Hair + REC_HAZD = fourCC("HAZD"), // Hazard + REC_HDPT = fourCC("HDPT"), // Head Part + REC_IDLE = fourCC("IDLE"), // Idle Animation + REC_IDLM = fourCC("IDLM"), // Idle Marker + REC_IMAD = fourCC("IMAD"), // Image Space Modifier + REC_IMGS = fourCC("IMGS"), // Image Space + REC_INFO = fourCC("INFO"), // Dialog Topic Info + REC_INGR = fourCC("INGR"), // Ingredient + REC_IPCT = fourCC("IPCT"), // Impact Data + REC_IPDS = fourCC("IPDS"), // Impact Data Set + REC_KEYM = fourCC("KEYM"), // Key + REC_KYWD = fourCC("KYWD"), // Keyword + REC_LAND = fourCC("LAND"), // Land + REC_LCRT = fourCC("LCRT"), // Location Reference Type + REC_LCTN = fourCC("LCTN"), // Location + REC_LGTM = fourCC("LGTM"), // Lighting Template + REC_LIGH = fourCC("LIGH"), // Light + REC_LSCR = fourCC("LSCR"), // Load Screen + REC_LTEX = fourCC("LTEX"), // Land Texture + REC_LVLC = fourCC("LVLC"), // Leveled Creature + REC_LVLI = fourCC("LVLI"), // Leveled Item + REC_LVLN = fourCC("LVLN"), // Leveled Actor + REC_LVSP = fourCC("LVSP"), // Leveled Spell + REC_MATO = fourCC("MATO"), // Material Object + REC_MATT = fourCC("MATT"), // Material Type + REC_MESG = fourCC("MESG"), // Message + REC_MGEF = fourCC("MGEF"), // Magic Effect + REC_MISC = fourCC("MISC"), // Misc. Object + REC_MOVT = fourCC("MOVT"), // Movement Type + REC_MSTT = fourCC("MSTT"), // Movable Static + REC_MUSC = fourCC("MUSC"), // Music Type + REC_MUST = fourCC("MUST"), // Music Track + REC_NAVI = fourCC("NAVI"), // Navigation (master data) + REC_NAVM = fourCC("NAVM"), // Nav Mesh + REC_NOTE = fourCC("NOTE"), // Note + REC_NPC_ = fourCC("NPC_"), // Actor (NPC, Creature) + REC_OTFT = fourCC("OTFT"), // Outfit + REC_PACK = fourCC("PACK"), // AI Package + REC_PERK = fourCC("PERK"), // Perk + REC_PGRE = fourCC("PGRE"), // Placed grenade + REC_PHZD = fourCC("PHZD"), // Placed hazard + REC_PROJ = fourCC("PROJ"), // Projectile + REC_QUST = fourCC("QUST"), // Quest + REC_RACE = fourCC("RACE"), // Race / Creature type + REC_REFR = fourCC("REFR"), // Object Reference + REC_REGN = fourCC("REGN"), // Region (Audio/Weather) + REC_RELA = fourCC("RELA"), // Relationship + REC_REVB = fourCC("REVB"), // Reverb Parameters + REC_RFCT = fourCC("RFCT"), // Visual Effect + REC_SBSP = fourCC("SBSP"), // Subspace (TES4 only?) + REC_SCEN = fourCC("SCEN"), // Scene + REC_SCPT = fourCC("SCPT"), // Script + REC_SCRL = fourCC("SCRL"), // Scroll + REC_SGST = fourCC("SGST"), // Sigil Stone + REC_SHOU = fourCC("SHOU"), // Shout + REC_SLGM = fourCC("SLGM"), // Soul Gem + REC_SMBN = fourCC("SMBN"), // Story Manager Branch Node + REC_SMEN = fourCC("SMEN"), // Story Manager Event Node + REC_SMQN = fourCC("SMQN"), // Story Manager Quest Node + REC_SNCT = fourCC("SNCT"), // Sound Category + REC_SNDR = fourCC("SNDR"), // Sound Reference + REC_SOPM = fourCC("SOPM"), // Sound Output Model + REC_SOUN = fourCC("SOUN"), // Sound + REC_SPEL = fourCC("SPEL"), // Spell + REC_SPGD = fourCC("SPGD"), // Shader Particle Geometry + REC_STAT = fourCC("STAT"), // Static + REC_TACT = fourCC("TACT"), // Talking Activator + REC_TERM = fourCC("TERM"), // Terminal + REC_TES4 = fourCC("TES4"), // Plugin info + REC_TREE = fourCC("TREE"), // Tree + REC_TXST = fourCC("TXST"), // Texture Set + REC_VTYP = fourCC("VTYP"), // Voice Type + REC_WATR = fourCC("WATR"), // Water Type + REC_WEAP = fourCC("WEAP"), // Weapon + REC_WOOP = fourCC("WOOP"), // Word Of Power + REC_WRLD = fourCC("WRLD"), // World Space + REC_WTHR = fourCC("WTHR"), // Weather + REC_ACRE = fourCC("ACRE"), // Placed Creature (TES4 only?) + REC_PGRD = fourCC("PGRD"), // Pathgrid (TES4 only?) + REC_ROAD = fourCC("ROAD"), // Road (TES4 only?) + REC_IMOD = fourCC("IMOD"), // Item Mod + REC_PWAT = fourCC("PWAT"), // Placeable Water + REC_SCOL = fourCC("SCOL"), // Static Collection + REC_CCRD = fourCC("CCRD"), // Caravan Card + REC_CMNY = fourCC("CMNY"), // Caravan Money + REC_ALOC = fourCC("ALOC"), // Audio Location Controller + REC_MSET = fourCC("MSET") // Media Set + }; + + enum SubRecordTypes + { + SUB_HEDR = fourCC("HEDR"), + SUB_CNAM = fourCC("CNAM"), + SUB_SNAM = fourCC("SNAM"), // TES4 only? + SUB_MAST = fourCC("MAST"), + SUB_DATA = fourCC("DATA"), + SUB_ONAM = fourCC("ONAM"), + SUB_INTV = fourCC("INTV"), + SUB_INCC = fourCC("INCC"), + SUB_OFST = fourCC("OFST"), // TES4 only? + SUB_DELE = fourCC("DELE"), // TES4 only? + + SUB_DNAM = fourCC("DNAM"), + SUB_EDID = fourCC("EDID"), + SUB_FULL = fourCC("FULL"), + SUB_LTMP = fourCC("LTMP"), + SUB_MHDT = fourCC("MHDT"), + SUB_MNAM = fourCC("MNAM"), + SUB_MODL = fourCC("MODL"), + SUB_NAM0 = fourCC("NAM0"), + SUB_NAM2 = fourCC("NAM2"), + SUB_NAM3 = fourCC("NAM3"), + SUB_NAM4 = fourCC("NAM4"), + SUB_NAM9 = fourCC("NAM9"), + SUB_NAMA = fourCC("NAMA"), + SUB_PNAM = fourCC("PNAM"), + SUB_RNAM = fourCC("RNAM"), + SUB_TNAM = fourCC("TNAM"), + SUB_UNAM = fourCC("UNAM"), + SUB_WCTR = fourCC("WCTR"), + SUB_WNAM = fourCC("WNAM"), + SUB_XEZN = fourCC("XEZN"), + SUB_XLCN = fourCC("XLCN"), + SUB_XXXX = fourCC("XXXX"), + SUB_ZNAM = fourCC("ZNAM"), + SUB_MODT = fourCC("MODT"), + SUB_ICON = fourCC("ICON"), // TES4 only? + + SUB_NVER = fourCC("NVER"), + SUB_NVMI = fourCC("NVMI"), + SUB_NVPP = fourCC("NVPP"), + SUB_NVSI = fourCC("NVSI"), + + SUB_NVNM = fourCC("NVNM"), + SUB_NNAM = fourCC("NNAM"), + + SUB_XCLC = fourCC("XCLC"), + SUB_XCLL = fourCC("XCLL"), + SUB_TVDT = fourCC("TVDT"), + SUB_XCGD = fourCC("XCGD"), + SUB_LNAM = fourCC("LNAM"), + SUB_XCLW = fourCC("XCLW"), + SUB_XNAM = fourCC("XNAM"), + SUB_XCLR = fourCC("XCLR"), + SUB_XWCS = fourCC("XWCS"), + SUB_XWCN = fourCC("XWCN"), + SUB_XWCU = fourCC("XWCU"), + SUB_XCWT = fourCC("XCWT"), + SUB_XOWN = fourCC("XOWN"), + SUB_XILL = fourCC("XILL"), + SUB_XWEM = fourCC("XWEM"), + SUB_XCCM = fourCC("XCCM"), + SUB_XCAS = fourCC("XCAS"), + SUB_XCMO = fourCC("XCMO"), + SUB_XCIM = fourCC("XCIM"), + SUB_XCMT = fourCC("XCMT"), // TES4 only? + SUB_XRNK = fourCC("XRNK"), // TES4 only? + SUB_XGLB = fourCC("XGLB"), // TES4 only? + + SUB_VNML = fourCC("VNML"), + SUB_VHGT = fourCC("VHGT"), + SUB_VCLR = fourCC("VCLR"), + SUA_BTXT = fourCC("BTXT"), + SUB_ATXT = fourCC("ATXT"), + SUB_VTXT = fourCC("VTXT"), + SUB_VTEX = fourCC("VTEX"), + + SUB_HNAM = fourCC("HNAM"), + SUB_GNAM = fourCC("GNAM"), + + SUB_RCLR = fourCC("RCLR"), + SUB_RPLI = fourCC("RPLI"), + SUB_RPLD = fourCC("RPLD"), + SUB_RDAT = fourCC("RDAT"), + SUB_RDMD = fourCC("RDMD"), // TES4 only? + SUB_RDSD = fourCC("RDSD"), // TES4 only? + SUB_RDGS = fourCC("RDGS"), // TES4 only? + SUB_RDMO = fourCC("RDMO"), + SUB_RDSA = fourCC("RDSA"), + SUB_RDWT = fourCC("RDWT"), + SUB_RDOT = fourCC("RDOT"), + SUB_RDMP = fourCC("RDMP"), + + SUB_MODB = fourCC("MODB"), + SUB_OBND = fourCC("OBND"), + SUB_MODS = fourCC("MODS"), + + SUB_NAME = fourCC("NAME"), + SUB_XMRK = fourCC("XMRK"), + SUB_FNAM = fourCC("FNAM"), + SUB_XSCL = fourCC("XSCL"), + SUB_XTEL = fourCC("XTEL"), + SUB_XTRG = fourCC("XTRG"), + SUB_XSED = fourCC("XSED"), + SUB_XLOD = fourCC("XLOD"), + SUB_XPCI = fourCC("XPCI"), + SUB_XLOC = fourCC("XLOC"), + SUB_XESP = fourCC("XESP"), + SUB_XLCM = fourCC("XLCM"), + SUB_XRTM = fourCC("XRTM"), + SUB_XACT = fourCC("XACT"), + SUB_XCNT = fourCC("XCNT"), + SUB_VMAD = fourCC("VMAD"), + SUB_XPRM = fourCC("XPRM"), + SUB_XMBO = fourCC("XMBO"), + SUB_XPOD = fourCC("XPOD"), + SUB_XRMR = fourCC("XRMR"), + SUB_INAM = fourCC("INAM"), + SUB_SCHR = fourCC("SCHR"), + SUB_XLRM = fourCC("XLRM"), + SUB_XRGD = fourCC("XRGD"), + SUB_XRDS = fourCC("XRDS"), + SUB_XEMI = fourCC("XEMI"), + SUB_XLIG = fourCC("XLIG"), + SUB_XALP = fourCC("XALP"), + SUB_XNDP = fourCC("XNDP"), + SUB_XAPD = fourCC("XAPD"), + SUB_XAPR = fourCC("XAPR"), + SUB_XLIB = fourCC("XLIB"), + SUB_XLKR = fourCC("XLKR"), + SUB_XLRT = fourCC("XLRT"), + SUB_XCVL = fourCC("XCVL"), + SUB_XCVR = fourCC("XCVR"), + SUB_XCZA = fourCC("XCZA"), + SUB_XCZC = fourCC("XCZC"), + SUB_XFVC = fourCC("XFVC"), + SUB_XHTW = fourCC("XHTW"), + SUB_XIS2 = fourCC("XIS2"), + SUB_XMBR = fourCC("XMBR"), + SUB_XCCP = fourCC("XCCP"), + SUB_XPWR = fourCC("XPWR"), + SUB_XTRI = fourCC("XTRI"), + SUB_XATR = fourCC("XATR"), + SUB_XPRD = fourCC("XPRD"), + SUB_XPPA = fourCC("XPPA"), + SUB_PDTO = fourCC("PDTO"), + SUB_XLRL = fourCC("XLRL"), + + SUB_QNAM = fourCC("QNAM"), + SUB_COCT = fourCC("COCT"), + SUB_COED = fourCC("COED"), + SUB_CNTO = fourCC("CNTO"), + SUB_SCRI = fourCC("SCRI"), + + SUB_BNAM = fourCC("BNAM"), + + SUB_BMDT = fourCC("BMDT"), + SUB_MOD2 = fourCC("MOD2"), + SUB_MOD3 = fourCC("MOD3"), + SUB_MOD4 = fourCC("MOD4"), + SUB_MO2B = fourCC("MO2B"), + SUB_MO3B = fourCC("MO3B"), + SUB_MO4B = fourCC("MO4B"), + SUB_MO2T = fourCC("MO2T"), + SUB_MO3T = fourCC("MO3T"), + SUB_MO4T = fourCC("MO4T"), + SUB_ANAM = fourCC("ANAM"), + SUB_ENAM = fourCC("ENAM"), + SUB_ICO2 = fourCC("ICO2"), + + SUB_ACBS = fourCC("ACBS"), + SUB_SPLO = fourCC("SPLO"), + SUB_AIDT = fourCC("AIDT"), + SUB_PKID = fourCC("PKID"), + SUB_HCLR = fourCC("HCLR"), + SUB_FGGS = fourCC("FGGS"), + SUB_FGGA = fourCC("FGGA"), + SUB_FGTS = fourCC("FGTS"), + SUB_KFFZ = fourCC("KFFZ"), + + SUB_PFIG = fourCC("PFIG"), + SUB_PFPC = fourCC("PFPC"), + + SUB_XHRS = fourCC("XHRS"), + SUB_XMRC = fourCC("XMRC"), + + SUB_SNDD = fourCC("SNDD"), + SUB_SNDX = fourCC("SNDX"), + + SUB_DESC = fourCC("DESC"), + + SUB_ENIT = fourCC("ENIT"), + SUB_EFID = fourCC("EFID"), + SUB_EFIT = fourCC("EFIT"), + SUB_SCIT = fourCC("SCIT"), + + SUB_SOUL = fourCC("SOUL"), + SUB_SLCP = fourCC("SLCP"), + + SUB_CSCR = fourCC("CSCR"), + SUB_CSDI = fourCC("CSDI"), + SUB_CSDC = fourCC("CSDC"), + SUB_NIFZ = fourCC("NIFZ"), + SUB_CSDT = fourCC("CSDT"), + SUB_NAM1 = fourCC("NAM1"), + SUB_NIFT = fourCC("NIFT"), + + SUB_LVLD = fourCC("LVLD"), + SUB_LVLF = fourCC("LVLF"), + SUB_LVLO = fourCC("LVLO"), + + SUB_BODT = fourCC("BODT"), + SUB_YNAM = fourCC("YNAM"), + SUB_DEST = fourCC("DEST"), + SUB_DMDL = fourCC("DMDL"), + SUB_DMDS = fourCC("DMDS"), + SUB_DMDT = fourCC("DMDT"), + SUB_DSTD = fourCC("DSTD"), + SUB_DSTF = fourCC("DSTF"), + SUB_KNAM = fourCC("KNAM"), + SUB_KSIZ = fourCC("KSIZ"), + SUB_KWDA = fourCC("KWDA"), + SUB_VNAM = fourCC("VNAM"), + SUB_SDSC = fourCC("SDSC"), + SUB_MO2S = fourCC("MO2S"), + SUB_MO4S = fourCC("MO4S"), + SUB_BOD2 = fourCC("BOD2"), + SUB_BAMT = fourCC("BAMT"), + SUB_BIDS = fourCC("BIDS"), + SUB_ETYP = fourCC("ETYP"), + SUB_BMCT = fourCC("BMCT"), + SUB_MICO = fourCC("MICO"), + SUB_MIC2 = fourCC("MIC2"), + SUB_EAMT = fourCC("EAMT"), + SUB_EITM = fourCC("EITM"), + + SUB_SCTX = fourCC("SCTX"), + SUB_XLTW = fourCC("XLTW"), + SUB_XMBP = fourCC("XMBP"), + SUB_XOCP = fourCC("XOCP"), + SUB_XRGB = fourCC("XRGB"), + SUB_XSPC = fourCC("XSPC"), + SUB_XTNM = fourCC("XTNM"), + SUB_ATKR = fourCC("ATKR"), + SUB_CRIF = fourCC("CRIF"), + SUB_DOFT = fourCC("DOFT"), + SUB_DPLT = fourCC("DPLT"), + SUB_ECOR = fourCC("ECOR"), + SUB_ATKD = fourCC("ATKD"), + SUB_ATKE = fourCC("ATKE"), + SUB_FTST = fourCC("FTST"), + SUB_HCLF = fourCC("HCLF"), + SUB_NAM5 = fourCC("NAM5"), + SUB_NAM6 = fourCC("NAM6"), + SUB_NAM7 = fourCC("NAM7"), + SUB_NAM8 = fourCC("NAM8"), + SUB_PRKR = fourCC("PRKR"), + SUB_PRKZ = fourCC("PRKZ"), + SUB_SOFT = fourCC("SOFT"), + SUB_SPCT = fourCC("SPCT"), + SUB_TINC = fourCC("TINC"), + SUB_TIAS = fourCC("TIAS"), + SUB_TINI = fourCC("TINI"), + SUB_TINV = fourCC("TINV"), + SUB_TPLT = fourCC("TPLT"), + SUB_VTCK = fourCC("VTCK"), + SUB_SHRT = fourCC("SHRT"), + SUB_SPOR = fourCC("SPOR"), + SUB_XHOR = fourCC("XHOR"), + SUB_CTDA = fourCC("CTDA"), + SUB_CRDT = fourCC("CRDT"), + SUB_FNMK = fourCC("FNMK"), + SUB_FNPR = fourCC("FNPR"), + SUB_WBDT = fourCC("WBDT"), + SUB_QUAL = fourCC("QUAL"), + SUB_INDX = fourCC("INDX"), + SUB_ATTR = fourCC("ATTR"), + SUB_MTNM = fourCC("MTNM"), + SUB_UNES = fourCC("UNES"), + SUB_TIND = fourCC("TIND"), + SUB_TINL = fourCC("TINL"), + SUB_TINP = fourCC("TINP"), + SUB_TINT = fourCC("TINT"), + SUB_TIRS = fourCC("TIRS"), + SUB_PHWT = fourCC("PHWT"), + SUB_AHCF = fourCC("AHCF"), + SUB_AHCM = fourCC("AHCM"), + SUB_HEAD = fourCC("HEAD"), + SUB_MPAI = fourCC("MPAI"), + SUB_MPAV = fourCC("MPAV"), + SUB_DFTF = fourCC("DFTF"), + SUB_DFTM = fourCC("DFTM"), + SUB_FLMV = fourCC("FLMV"), + SUB_FTSF = fourCC("FTSF"), + SUB_FTSM = fourCC("FTSM"), + SUB_MTYP = fourCC("MTYP"), + SUB_PHTN = fourCC("PHTN"), + SUB_RNMV = fourCC("RNMV"), + SUB_RPRF = fourCC("RPRF"), + SUB_RPRM = fourCC("RPRM"), + SUB_SNMV = fourCC("SNMV"), + SUB_SPED = fourCC("SPED"), + SUB_SWMV = fourCC("SWMV"), + SUB_WKMV = fourCC("WKMV"), + SUB_LLCT = fourCC("LLCT"), + SUB_IDLF = fourCC("IDLF"), + SUB_IDLA = fourCC("IDLA"), + SUB_IDLC = fourCC("IDLC"), + SUB_IDLT = fourCC("IDLT"), + SUB_DODT = fourCC("DODT"), + SUB_TX00 = fourCC("TX00"), + SUB_TX01 = fourCC("TX01"), + SUB_TX02 = fourCC("TX02"), + SUB_TX03 = fourCC("TX03"), + SUB_TX04 = fourCC("TX04"), + SUB_TX05 = fourCC("TX05"), + SUB_TX06 = fourCC("TX06"), + SUB_TX07 = fourCC("TX07"), + SUB_BPND = fourCC("BPND"), + SUB_BPTN = fourCC("BPTN"), + SUB_BPNN = fourCC("BPNN"), + SUB_BPNT = fourCC("BPNT"), + SUB_BPNI = fourCC("BPNI"), + SUB_RAGA = fourCC("RAGA"), + + SUB_QSTI = fourCC("QSTI"), + SUB_QSTR = fourCC("QSTR"), + SUB_QSDT = fourCC("QSDT"), + SUB_SCDA = fourCC("SCDA"), + SUB_SCRO = fourCC("SCRO"), + SUB_QSTA = fourCC("QSTA"), + SUB_CTDT = fourCC("CTDT"), + SUB_SCHD = fourCC("SCHD"), + SUB_TCLF = fourCC("TCLF"), + SUB_TCLT = fourCC("TCLT"), + SUB_TRDT = fourCC("TRDT"), + SUB_TPIC = fourCC("TPIC"), + + SUB_PKDT = fourCC("PKDT"), + SUB_PSDT = fourCC("PSDT"), + SUB_PLDT = fourCC("PLDT"), + SUB_PTDT = fourCC("PTDT"), + SUB_PGRP = fourCC("PGRP"), + SUB_PGRR = fourCC("PGRR"), + SUB_PGRI = fourCC("PGRI"), + SUB_PGRL = fourCC("PGRL"), + SUB_PGAG = fourCC("PGAG"), + SUB_FLTV = fourCC("FLTV"), + + SUB_XHLT = fourCC("XHLT"), // Unofficial Oblivion Patch + SUB_XCHG = fourCC("XCHG"), // thievery.exp + + SUB_ITXT = fourCC("ITXT"), + SUB_MO5T = fourCC("MO5T"), + SUB_MOD5 = fourCC("MOD5"), + SUB_MDOB = fourCC("MDOB"), + SUB_SPIT = fourCC("SPIT"), + SUB_PTDA = fourCC("PTDA"), // TES5 + SUB_PFOR = fourCC("PFOR"), // TES5 + SUB_PFO2 = fourCC("PFO2"), // TES5 + SUB_PRCB = fourCC("PRCB"), // TES5 + SUB_PKCU = fourCC("PKCU"), // TES5 + SUB_PKC2 = fourCC("PKC2"), // TES5 + SUB_CITC = fourCC("CITC"), // TES5 + SUB_CIS1 = fourCC("CIS1"), // TES5 + SUB_CIS2 = fourCC("CIS2"), // TES5 + SUB_TIFC = fourCC("TIFC"), // TES5 + SUB_ALCA = fourCC("ALCA"), // TES5 + SUB_ALCL = fourCC("ALCL"), // TES5 + SUB_ALCO = fourCC("ALCO"), // TES5 + SUB_ALDN = fourCC("ALDN"), // TES5 + SUB_ALEA = fourCC("ALEA"), // TES5 + SUB_ALED = fourCC("ALED"), // TES5 + SUB_ALEQ = fourCC("ALEQ"), // TES5 + SUB_ALFA = fourCC("ALFA"), // TES5 + SUB_ALFC = fourCC("ALFC"), // TES5 + SUB_ALFD = fourCC("ALFD"), // TES5 + SUB_ALFE = fourCC("ALFE"), // TES5 + SUB_ALFI = fourCC("ALFI"), // TES5 + SUB_ALFL = fourCC("ALFL"), // TES5 + SUB_ALFR = fourCC("ALFR"), // TES5 + SUB_ALID = fourCC("ALID"), // TES5 + SUB_ALLS = fourCC("ALLS"), // TES5 + SUB_ALNA = fourCC("ALNA"), // TES5 + SUB_ALNT = fourCC("ALNT"), // TES5 + SUB_ALPC = fourCC("ALPC"), // TES5 + SUB_ALRT = fourCC("ALRT"), // TES5 + SUB_ALSP = fourCC("ALSP"), // TES5 + SUB_ALST = fourCC("ALST"), // TES5 + SUB_ALUA = fourCC("ALUA"), // TES5 + SUB_FLTR = fourCC("FLTR"), // TES5 + SUB_QTGL = fourCC("QTGL"), // TES5 + SUB_TWAT = fourCC("TWAT"), // TES5 + SUB_XIBS = fourCC("XIBS"), // FO3 + SUB_REPL = fourCC("REPL"), // FO3 + SUB_BIPL = fourCC("BIPL"), // FO3 + SUB_MODD = fourCC("MODD"), // FO3 + SUB_MOSD = fourCC("MOSD"), // FO3 + SUB_MO3S = fourCC("MO3S"), // FO3 + SUB_XCET = fourCC("XCET"), // FO3 + SUB_LVLG = fourCC("LVLG"), // FO3 + SUB_NVCI = fourCC("NVCI"), // FO3 + SUB_NVVX = fourCC("NVVX"), // FO3 + SUB_NVTR = fourCC("NVTR"), // FO3 + SUB_NVCA = fourCC("NVCA"), // FO3 + SUB_NVDP = fourCC("NVDP"), // FO3 + SUB_NVGD = fourCC("NVGD"), // FO3 + SUB_NVEX = fourCC("NVEX"), // FO3 + SUB_XHLP = fourCC("XHLP"), // FO3 + SUB_XRDO = fourCC("XRDO"), // FO3 + SUB_XAMT = fourCC("XAMT"), // FO3 + SUB_XAMC = fourCC("XAMC"), // FO3 + SUB_XRAD = fourCC("XRAD"), // FO3 + SUB_XORD = fourCC("XORD"), // FO3 + SUB_XCLP = fourCC("XCLP"), // FO3 + SUB_NEXT = fourCC("NEXT"), // FO3 + SUB_QOBJ = fourCC("QOBJ"), // FO3 + SUB_POBA = fourCC("POBA"), // FO3 + SUB_POCA = fourCC("POCA"), // FO3 + SUB_POEA = fourCC("POEA"), // FO3 + SUB_PKDD = fourCC("PKDD"), // FO3 + SUB_PKD2 = fourCC("PKD2"), // FO3 + SUB_PKPT = fourCC("PKPT"), // FO3 + SUB_PKED = fourCC("PKED"), // FO3 + SUB_PKE2 = fourCC("PKE2"), // FO3 + SUB_PKAM = fourCC("PKAM"), // FO3 + SUB_PUID = fourCC("PUID"), // FO3 + SUB_PKW3 = fourCC("PKW3"), // FO3 + SUB_PTD2 = fourCC("PTD2"), // FO3 + SUB_PLD2 = fourCC("PLD2"), // FO3 + SUB_PKFD = fourCC("PKFD"), // FO3 + SUB_IDLB = fourCC("IDLB"), // FO3 + SUB_XDCR = fourCC("XDCR"), // FO3 + SUB_DALC = fourCC("DALC"), // FO3 + SUB_IMPS = fourCC("IMPS"), // FO3 Anchorage + SUB_IMPF = fourCC("IMPF"), // FO3 Anchorage + + SUB_XATO = fourCC("XATO"), // FONV + SUB_INFC = fourCC("INFC"), // FONV + SUB_INFX = fourCC("INFX"), // FONV + SUB_TDUM = fourCC("TDUM"), // FONV + SUB_TCFU = fourCC("TCFU"), // FONV + SUB_DAT2 = fourCC("DAT2"), // FONV + SUB_RCIL = fourCC("RCIL"), // FONV + SUB_MMRK = fourCC("MMRK"), // FONV + SUB_SCRV = fourCC("SCRV"), // FONV + SUB_SCVR = fourCC("SCVR"), // FONV + SUB_SLSD = fourCC("SLSD"), // FONV + SUB_XSRF = fourCC("XSRF"), // FONV + SUB_XSRD = fourCC("XSRD"), // FONV + SUB_WMI1 = fourCC("WMI1"), // FONV + SUB_RDID = fourCC("RDID"), // FONV + SUB_RDSB = fourCC("RDSB"), // FONV + SUB_RDSI = fourCC("RDSI"), // FONV + SUB_BRUS = fourCC("BRUS"), // FONV + SUB_VATS = fourCC("VATS"), // FONV + SUB_VANM = fourCC("VANM"), // FONV + SUB_MWD1 = fourCC("MWD1"), // FONV + SUB_MWD2 = fourCC("MWD2"), // FONV + SUB_MWD3 = fourCC("MWD3"), // FONV + SUB_MWD4 = fourCC("MWD4"), // FONV + SUB_MWD5 = fourCC("MWD5"), // FONV + SUB_MWD6 = fourCC("MWD6"), // FONV + SUB_MWD7 = fourCC("MWD7"), // FONV + SUB_WMI2 = fourCC("WMI2"), // FONV + SUB_WMI3 = fourCC("WMI3"), // FONV + SUB_WMS1 = fourCC("WMS1"), // FONV + SUB_WMS2 = fourCC("WMS2"), // FONV + SUB_WNM1 = fourCC("WNM1"), // FONV + SUB_WNM2 = fourCC("WNM2"), // FONV + SUB_WNM3 = fourCC("WNM3"), // FONV + SUB_WNM4 = fourCC("WNM4"), // FONV + SUB_WNM5 = fourCC("WNM5"), // FONV + SUB_WNM6 = fourCC("WNM6"), // FONV + SUB_WNM7 = fourCC("WNM7"), // FONV + SUB_JNAM = fourCC("JNAM"), // FONV + SUB_EFSD = fourCC("EFSD"), // FONV DeadMoney + }; + + enum MagicEffectID + { + // Alteration + EFI_BRDN = fourCC("BRDN"), + EFI_FTHR = fourCC("FTHR"), + EFI_FISH = fourCC("FISH"), + EFI_FRSH = fourCC("FRSH"), + EFI_OPEN = fourCC("OPNN"), + EFI_SHLD = fourCC("SHLD"), + EFI_LISH = fourCC("LISH"), + EFI_WABR = fourCC("WABR"), + EFI_WAWA = fourCC("WAWA"), + + // Conjuration + EFI_BABO = fourCC("BABO"), // Bound Boots + EFI_BACU = fourCC("BACU"), // Bound Cuirass + EFI_BAGA = fourCC("BAGA"), // Bound Gauntlets + EFI_BAGR = fourCC("BAGR"), // Bound Greaves + EFI_BAHE = fourCC("BAHE"), // Bound Helmet + EFI_BASH = fourCC("BASH"), // Bound Shield + EFI_BWAX = fourCC("BWAX"), // Bound Axe + EFI_BWBO = fourCC("BWBO"), // Bound Bow + EFI_BWDA = fourCC("BWDA"), // Bound Dagger + EFI_BWMA = fourCC("BWMA"), // Bound Mace + EFI_BWSW = fourCC("BWSW"), // Bound Sword + EFI_Z001 = fourCC("Z001"), // Summon Rufio's Ghost + EFI_Z002 = fourCC("Z002"), // Summon Ancestor Guardian + EFI_Z003 = fourCC("Z003"), // Summon Spiderling + EFI_Z005 = fourCC("Z005"), // Summon Bear + EFI_ZCLA = fourCC("ZCLA"), // Summon Clannfear + EFI_ZDAE = fourCC("ZDAE"), // Summon Daedroth + EFI_ZDRE = fourCC("ZDRE"), // Summon Dremora + EFI_ZDRL = fourCC("ZDRL"), // Summon Dremora Lord + EFI_ZFIA = fourCC("ZFIA"), // Summon Flame Atronach + EFI_ZFRA = fourCC("ZFRA"), // Summon Frost Atronach + EFI_ZGHO = fourCC("ZGHO"), // Summon Ghost + EFI_ZHDZ = fourCC("ZHDZ"), // Summon Headless Zombie + EFI_ZLIC = fourCC("ZLIC"), // Summon Lich + EFI_ZSCA = fourCC("ZSCA"), // Summon Scamp + EFI_ZSKE = fourCC("ZSKE"), // Summon Skeleton + EFI_ZSKA = fourCC("ZSKA"), // Summon Skeleton Guardian + EFI_ZSKH = fourCC("ZSKH"), // Summon Skeleton Hero + EFI_ZSKC = fourCC("ZSKC"), // Summon Skeleton Champion + EFI_ZSPD = fourCC("ZSPD"), // Summon Spider Daedra + EFI_ZSTA = fourCC("ZSTA"), // Summon Storm Atronach + EFI_ZWRA = fourCC("ZWRA"), // Summon Faded Wraith + EFI_ZWRL = fourCC("ZWRL"), // Summon Gloom Wraith + EFI_ZXIV = fourCC("ZXIV"), // Summon Xivilai + EFI_ZZOM = fourCC("ZZOM"), // Summon Zombie + EFI_TURN = fourCC("TURN"), // Turn Undead + + // Destruction + EFI_DGAT = fourCC("DGAT"), // Damage Attribute + EFI_DGFA = fourCC("DGFA"), // Damage Fatigue + EFI_DGHE = fourCC("DGHE"), // Damage Health + EFI_DGSP = fourCC("DGSP"), // Damage Magicka + EFI_DIAR = fourCC("DIAR"), // Disintegrate Armor + EFI_DIWE = fourCC("DIWE"), // Disintegrate Weapon + EFI_DRAT = fourCC("DRAT"), // Drain Attribute + EFI_DRFA = fourCC("DRFA"), // Drain Fatigue + EFI_DRHE = fourCC("DRHE"), // Drain Health + EFI_DRSP = fourCC("DRSP"), // Drain Magicka + EFI_DRSK = fourCC("DRSK"), // Drain Skill + EFI_FIDG = fourCC("FIDG"), // Fire Damage + EFI_FRDG = fourCC("FRDG"), // Frost Damage + EFI_SHDG = fourCC("SHDG"), // Shock Damage + EFI_WKDI = fourCC("WKDI"), // Weakness to Disease + EFI_WKFI = fourCC("WKFI"), // Weakness to Fire + EFI_WKFR = fourCC("WKFR"), // Weakness to Frost + EFI_WKMA = fourCC("WKMA"), // Weakness to Magic + EFI_WKNW = fourCC("WKNW"), // Weakness to Normal Weapons + EFI_WKPO = fourCC("WKPO"), // Weakness to Poison + EFI_WKSH = fourCC("WKSH"), // Weakness to Shock + + // Illusion + EFI_CALM = fourCC("CALM"), // Calm + EFI_CHML = fourCC("CHML"), // Chameleon + EFI_CHRM = fourCC("CHRM"), // Charm + EFI_COCR = fourCC("COCR"), // Command Creature + EFI_COHU = fourCC("COHU"), // Command Humanoid + EFI_DEMO = fourCC("DEMO"), // Demoralize + EFI_FRNZ = fourCC("FRNZ"), // Frenzy + EFI_INVI = fourCC("INVI"), // Invisibility + EFI_LGHT = fourCC("LGHT"), // Light + EFI_NEYE = fourCC("NEYE"), // Night-Eye + EFI_PARA = fourCC("PARA"), // Paralyze + EFI_RALY = fourCC("RALY"), // Rally + EFI_SLNC = fourCC("SLNC"), // Silence + + // Mysticism + EFI_DTCT = fourCC("DTCT"), // Detect Life + EFI_DSPL = fourCC("DSPL"), // Dispel + EFI_REDG = fourCC("REDG"), // Reflect Damage + EFI_RFLC = fourCC("RFLC"), // Reflect Spell + EFI_STRP = fourCC("STRP"), // Soul Trap + EFI_SABS = fourCC("SABS"), // Spell Absorption + EFI_TELE = fourCC("TELE"), // Telekinesis + + // Restoration + EFI_ABAT = fourCC("ABAT"), // Absorb Attribute + EFI_ABFA = fourCC("ABFA"), // Absorb Fatigue + EFI_ABHe = fourCC("ABHe"), // Absorb Health + EFI_ABSP = fourCC("ABSP"), // Absorb Magicka + EFI_ABSK = fourCC("ABSK"), // Absorb Skill + EFI_1400 = fourCC("1400"), // Cure Disease + EFI_CUPA = fourCC("CUPA"), // Cure Paralysis + EFI_CUPO = fourCC("CUPO"), // Cure Poison + EFI_FOAT = fourCC("FOAT"), // Fortify Attribute + EFI_FOFA = fourCC("FOFA"), // Fortify Fatigue + EFI_FOHE = fourCC("FOHE"), // Fortify Health + EFI_FOSP = fourCC("FOSP"), // Fortify Magicka + EFI_FOSK = fourCC("FOSK"), // Fortify Skill + EFI_RSDI = fourCC("RSDI"), // Resist Disease + EFI_RSFI = fourCC("RSFI"), // Resist Fire + EFI_RSFR = fourCC("RSFR"), // Resist Frost + EFI_RSMA = fourCC("RSMA"), // Resist Magic + EFI_RSNW = fourCC("RSNW"), // Resist Normal Weapons + EFI_RSPA = fourCC("RSPA"), // Resist Paralysis + EFI_RSPO = fourCC("RSPO"), // Resist Poison + EFI_RSSH = fourCC("RSSH"), // Resist Shock + EFI_REAT = fourCC("REAT"), // Restore Attribute + EFI_REFA = fourCC("REFA"), // Restore Fatigue + EFI_REHE = fourCC("REHE"), // Restore Health + EFI_RESP = fourCC("RESP"), // Restore Magicka + + // Effects + EFI_LOCK = fourCC("LOCK"), // Lock Lock + EFI_SEFF = fourCC("SEFF"), // Script Effect + EFI_Z020 = fourCC("Z020"), // Summon 20 Extra + EFI_MYHL = fourCC("MYHL"), // Summon Mythic Dawn Helmet + EFI_MYTH = fourCC("MYTH"), // Summon Mythic Dawn Armor + EFI_REAN = fourCC("REAN"), // Reanimate + EFI_DISE = fourCC("DISE"), // Disease Info + EFI_POSN = fourCC("POSN"), // Poison Info + EFI_DUMY = fourCC("DUMY"), // Mehrunes Dagon Custom Effect + EFI_STMA = fourCC("STMA"), // Stunted Magicka + EFI_SUDG = fourCC("SUDG"), // Sun Damage + EFI_VAMP = fourCC("VAMP"), // Vampirism + EFI_DARK = fourCC("DARK"), // Darkness + EFI_RSWD = fourCC("RSWD") // Resist Water Damage + }; + + // Based on http://www.uesp.net/wiki/Tes5Mod:Mod_File_Format#Groups + enum GroupType + { + Grp_RecordType = 0, + Grp_WorldChild = 1, + Grp_InteriorCell = 2, + Grp_InteriorSubCell = 3, + Grp_ExteriorCell = 4, + Grp_ExteriorSubCell = 5, + Grp_CellChild = 6, + Grp_TopicChild = 7, + Grp_CellPersistentChild = 8, + Grp_CellTemporaryChild = 9, + Grp_CellVisibleDistChild = 10 + }; + + // Based on http://www.uesp.net/wiki/Tes5Mod:Mod_File_Format#Records + enum RecordFlag + { + Rec_ESM = 0x00000001, // (TES4 record only) Master (ESM) file. + Rec_Deleted = 0x00000020, // Deleted + Rec_Constant = 0x00000040, // Constant + Rec_HiddenLMap = 0x00000040, // (REFR) Hidden From Local Map (Needs Confirmation: Related to shields) + Rec_Localized = 0x00000080, // (TES4 record only) Is localized. This will make Skyrim load the + // .STRINGS, .DLSTRINGS, and .ILSTRINGS files associated with the mod. + // If this flag is not set, lstrings are treated as zstrings. + Rec_FireOff = 0x00000080, // (PHZD) Turn off fire + Rec_UpdateAnim = 0x00000100, // Must Update Anims + Rec_NoAccess = 0x00000100, // (REFR) Inaccessible + Rec_Hidden = 0x00000200, // (REFR) Hidden from local map + Rec_StartDead = 0x00000200, // (ACHR) Starts dead /(REFR) MotionBlurCastsShadows + Rec_Persistent = 0x00000400, // Quest item / Persistent reference + Rec_DispMenu = 0x00000400, // (LSCR) Displays in Main Menu + Rec_Disabled = 0x00000800, // Initially disabled + Rec_Ignored = 0x00001000, // Ignored + Rec_VisDistant = 0x00008000, // Visible when distant + Rec_RandAnim = 0x00010000, // (ACTI) Random Animation Start + Rec_Danger = 0x00020000, // (ACTI) Dangerous / Off limits (Interior cell) + // Dangerous Can't be set withough Ignore Object Interaction + Rec_Compressed = 0x00040000, // Data is compressed + Rec_CanNotWait = 0x00080000, // Can't wait + Rec_IgnoreObj = 0x00100000, // (ACTI) Ignore Object Interaction + // Ignore Object Interaction Sets Dangerous Automatically + Rec_Marker = 0x00800000, // Is Marker + Rec_Obstacle = 0x02000000, // (ACTI) Obstacle / (REFR) No AI Acquire + Rec_NavMFilter = 0x04000000, // NavMesh Gen - Filter + Rec_NavMBBox = 0x08000000, // NavMesh Gen - Bounding Box + Rec_ExitToTalk = 0x10000000, // (FURN) Must Exit to Talk + Rec_Refected = 0x10000000, // (REFR) Reflected By Auto Water + Rec_ChildUse = 0x20000000, // (FURN/IDLM) Child Can Use + Rec_NoHavok = 0x20000000, // (REFR) Don't Havok Settle + Rec_NavMGround = 0x40000000, // NavMesh Gen - Ground + Rec_NoRespawn = 0x40000000, // (REFR) NoRespawn + Rec_MultiBound = 0x80000000 // (REFR) MultiBound + }; + +#pragma pack(push, 1) + // NOTE: the label field of a group is not reliable (http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format) + union GroupLabel + { + std::uint32_t value; // formId, blockNo or raw int representation of type + char recordType[4]; // record type in ascii + std::int16_t grid[2]; // grid y, x (note the reverse order) + }; + + struct GroupTypeHeader + { + std::uint32_t typeId; + std::uint32_t groupSize; // includes the 24 bytes (20 for TES4) of header (i.e. this struct) + GroupLabel label; // format based on type + std::int32_t type; + std::uint16_t stamp; // & 0xff for day, & 0xff00 for months since Dec 2002 (i.e. 1 = Jan 2003) + std::uint16_t unknown; + std::uint16_t version; // not in TES4 + std::uint16_t unknown2; // not in TES4 + }; + + struct RecordTypeHeader + { + std::uint32_t typeId; + std::uint32_t dataSize; // does *not* include 24 bytes (20 for TES4) of header + std::uint32_t flags; + FormId id; + std::uint32_t revision; + std::uint16_t version; // not in TES4 + std::uint16_t unknown; // not in TES4 + }; + + union RecordHeader + { + struct GroupTypeHeader group; + struct RecordTypeHeader record; + }; + + struct SubRecordHeader + { + std::uint32_t typeId; + std::uint16_t dataSize; + }; + + // Grid, CellGrid and Vertex are shared by NVMI(NAVI) and NVNM(NAVM) + + struct Grid + { + std::int16_t x; + std::int16_t y; + }; + + union CellGrid + { + FormId cellId; + Grid grid; + }; + + struct Vertex + { + float x; + float y; + float z; + }; +#pragma pack(pop) + + // For pretty printing GroupHeader labels + std::string printLabel(const GroupLabel& label, const std::uint32_t type); + + void gridToString(std::int16_t x, std::int16_t y, std::string& str); +} + +#endif // ESM4_COMMON_H diff --git a/components/esm4/dialogue.hpp b/components/esm4/dialogue.hpp new file mode 100644 index 0000000000..e77d192901 --- /dev/null +++ b/components/esm4/dialogue.hpp @@ -0,0 +1,46 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_DIALOGUE_H +#define ESM4_DIALOGUE_H + +namespace ESM4 +{ + enum DialType + { + DTYP_Topic = 0, + DTYP_Conversation = 1, + DTYP_Combat = 2, + DTYP_Persuation = 3, + DTYP_Detection = 4, + DTYP_Service = 5, + DTYP_Miscellaneous = 6, + // below FO3/FONV + DTYP_Radio = 7 + }; +} + +#endif // ESM4_DIALOGUE_H diff --git a/components/esm4/effect.hpp b/components/esm4/effect.hpp new file mode 100644 index 0000000000..8580a478dc --- /dev/null +++ b/components/esm4/effect.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_EFFECT_H +#define ESM4_EFFECT_H + +#include + +#include "formid.hpp" + +namespace ESM4 +{ +#pragma pack(push, 1) + union EFI_Label + { + std::uint32_t value; + char effect[4]; + }; + + struct ScriptEffect + { + FormId formId; // Script effect (Magic effect must be SEFF) + std::int32_t school; // Magic school. See Magic schools for more information. + EFI_Label visualEffect; // Visual effect name or 0x00000000 if None + std::uint8_t flags; // 0x01 = Hostile + std::uint8_t unknown1; + std::uint8_t unknown2; + std::uint8_t unknown3; + }; +#pragma pack(pop) +} + +#endif // ESM4_EFFECT_H diff --git a/components/esm4/formid.cpp b/components/esm4/formid.cpp new file mode 100644 index 0000000000..f5493fd0ca --- /dev/null +++ b/components/esm4/formid.cpp @@ -0,0 +1,78 @@ +/* + Copyright (C) 2016, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + +*/ +#include "formid.hpp" + +#include +#include +#include +#include // strtol +#include // LONG_MIN, LONG_MAX for gcc + +#include + +namespace ESM4 +{ + void formIdToString(FormId formId, std::string& str) + { + char buf[8+1]; + int res = snprintf(buf, 8+1, "%08X", formId); + if (res > 0 && res < 8+1) + str.assign(buf); + else + throw std::runtime_error("Possible buffer overflow while converting formId"); + } + + std::string formIdToString(FormId formId) + { + std::string str; + formIdToString(formId, str); + return str; + } + + bool isFormId(const std::string& str, FormId *id) + { + if (str.size() != 8) + return false; + + char *tmp; + errno = 0; + unsigned long val = strtol(str.c_str(), &tmp, 16); + + if (tmp == str.c_str() || *tmp != '\0' + || ((val == (unsigned long)LONG_MIN || val == (unsigned long)LONG_MAX) && errno == ERANGE)) + return false; + + if (id != nullptr) + *id = static_cast(val); + + return true; + } + + FormId stringToFormId(const std::string& str) + { + if (str.size() != 8) + throw std::out_of_range("StringToFormId: incorrect string size"); + + return static_cast(std::stoul(str, nullptr, 16)); + } +} diff --git a/components/esm4/formid.hpp b/components/esm4/formid.hpp new file mode 100644 index 0000000000..6bb2c475e3 --- /dev/null +++ b/components/esm4/formid.hpp @@ -0,0 +1,42 @@ +/* + Copyright (C) 2016 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + +*/ +#ifndef ESM4_FORMID_H +#define ESM4_FORMID_H + +#include +#include + +namespace ESM4 +{ + typedef std::uint32_t FormId; + + void formIdToString(FormId formId, std::string& str); + + std::string formIdToString(FormId formId); + + bool isFormId(const std::string& str, FormId *id = nullptr); + + FormId stringToFormId(const std::string& str); +} + +#endif // ESM4_FORMID_H diff --git a/components/esm4/inventory.hpp b/components/esm4/inventory.hpp new file mode 100644 index 0000000000..31181ce33c --- /dev/null +++ b/components/esm4/inventory.hpp @@ -0,0 +1,55 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_INVENTORY_H +#define ESM4_INVENTORY_H + +#include + +#include "formid.hpp" + +namespace ESM4 +{ +#pragma pack(push, 1) + // LVLC, LVLI + struct LVLO + { + std::int16_t level; + std::uint16_t unknown; // sometimes missing + FormId item; + std::int16_t count; + std::uint16_t unknown2; // sometimes missing + }; + + struct InventoryItem // NPC_, CREA, CONT + { + FormId item; + std::uint32_t count; + }; +#pragma pack(pop) +} + +#endif // ESM4_INVENTORY_H diff --git a/components/esm4/lighting.hpp b/components/esm4/lighting.hpp new file mode 100644 index 0000000000..a139ae630e --- /dev/null +++ b/components/esm4/lighting.hpp @@ -0,0 +1,80 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LIGHTING_H +#define ESM4_LIGHTING_H + +#include +#include + +namespace ESM4 +{ +#pragma pack(push, 1) + // guesses only for TES4 + struct Lighting + { // | Aichan Prison values + std::uint32_t ambient; // | 16 17 19 00 (RGBA) + std::uint32_t directional; // | 00 00 00 00 (RGBA) + std::uint32_t fogColor; // | 1D 1B 16 00 (RGBA) + float fogNear; // Fog Near | 00 00 00 00 = 0.f + float fogFar; // Fog Far | 00 80 3B 45 = 3000.f + std::int32_t rotationXY; // rotation xy | 00 00 00 00 = 0 + std::int32_t rotationZ; // rotation z | 00 00 00 00 = 0 + float fogDirFade; // Fog dir fade | 00 00 80 3F = 1.f + float fogClipDist; // Fog clip dist | 00 80 3B 45 = 3000.f + float fogPower = std::numeric_limits::max(); + }; + + struct Lighting_TES5 + { + std::uint32_t ambient; + std::uint32_t directional; + std::uint32_t fogColor; + float fogNear; + float fogFar; + std::int32_t rotationXY; + std::int32_t rotationZ; + float fogDirFade; + float fogClipDist; + float fogPower; + std::uint32_t unknown1; + std::uint32_t unknown2; + std::uint32_t unknown3; + std::uint32_t unknown4; + std::uint32_t unknown5; + std::uint32_t unknown6; + std::uint32_t unknown7; + std::uint32_t unknown8; + std::uint32_t fogColorFar; + float fogMax; + float LightFadeStart; + float LightFadeEnd; + std::uint32_t padding; + }; +#pragma pack(pop) +} + +#endif // ESM4_LIGHTING_H diff --git a/components/esm4/loadachr.cpp b/components/esm4/loadachr.cpp new file mode 100644 index 0000000000..7a62b5cdbd --- /dev/null +++ b/components/esm4/loadachr.cpp @@ -0,0 +1,110 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadachr.hpp" + +#include +//#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::ActorCharacter::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + mParent = reader.currCell(); // NOTE: only for persistent achr? (aren't they all persistent?) + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_NAME: reader.getFormId(mBaseObj); break; + case ESM4::SUB_DATA: reader.get(mPlacement); break; + case ESM4::SUB_XSCL: reader.get(mScale); break; + case ESM4::SUB_XOWN: reader.get(mOwner); break; + case ESM4::SUB_XESP: + { + reader.get(mEsp); + reader.adjustFormId(mEsp.parent); + break; + } + case ESM4::SUB_XRGD: // ragdoll + case ESM4::SUB_XRGB: // ragdoll biped + { + //std::cout << "ACHR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XHRS: // horse formId + case ESM4::SUB_XMRC: // merchant container formId + // TES5 + case ESM4::SUB_XAPD: // activation parent + case ESM4::SUB_XAPR: // active parent + case ESM4::SUB_XEZN: // encounter zone + case ESM4::SUB_XHOR: + case ESM4::SUB_XLCM: // levelled creature + case ESM4::SUB_XLCN: // location + case ESM4::SUB_XLKR: // location route? + case ESM4::SUB_XLRT: // location type + // + case ESM4::SUB_XPRD: + case ESM4::SUB_XPPA: + case ESM4::SUB_INAM: + case ESM4::SUB_PDTO: + // + case ESM4::SUB_XIS2: + case ESM4::SUB_XPCI: // formId + case ESM4::SUB_XLOD: + case ESM4::SUB_VMAD: + case ESM4::SUB_XLRL: // Unofficial Skyrim Patch + case ESM4::SUB_XRDS: // FO3 + case ESM4::SUB_XIBS: // FO3 + case ESM4::SUB_SCHR: // FO3 + case ESM4::SUB_TNAM: // FO3 + case ESM4::SUB_XATO: // FONV + { + //std::cout << "ACHR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ACHR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::ActorCharacter::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::ActorCharacter::blank() +//{ +//} diff --git a/components/esm4/loadachr.hpp b/components/esm4/loadachr.hpp new file mode 100644 index 0000000000..89b1ad7c86 --- /dev/null +++ b/components/esm4/loadachr.hpp @@ -0,0 +1,67 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ACHR_H +#define ESM4_ACHR_H + +#include + +#include "reference.hpp" // FormId, Placement, EnableParent + +namespace ESM4 +{ + class Reader; + class Writer; + + struct ActorCharacter + { + FormId mParent; // cell formId, from the loading sequence + // NOTE: for exterior cells it will be the dummy cell FormId + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + FormId mBaseObj; + + Placement mPlacement; + float mScale = 1.0f; + FormId mOwner; + FormId mGlobal; + + bool mInitiallyDisabled; // TODO may need to check mFlags & 0x800 (initially disabled) + + EnableParent mEsp; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ACHR_H diff --git a/components/esm4/loadacre.cpp b/components/esm4/loadacre.cpp new file mode 100644 index 0000000000..33d402eb81 --- /dev/null +++ b/components/esm4/loadacre.cpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadacre.hpp" + +#include +//#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::ActorCreature::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_NAME: reader.getFormId(mBaseObj); break; + case ESM4::SUB_DATA: reader.get(mPlacement); break; + case ESM4::SUB_XSCL: reader.get(mScale); break; + case ESM4::SUB_XESP: + { + reader.get(mEsp); + reader.adjustFormId(mEsp.parent); + break; + } + case ESM4::SUB_XOWN: reader.getFormId(mOwner); break; + case ESM4::SUB_XGLB: reader.get(mGlobal); break; // FIXME: formId? + case ESM4::SUB_XRNK: reader.get(mFactionRank); break; + case ESM4::SUB_XRGD: // ragdoll + case ESM4::SUB_XRGB: // ragdoll biped + { + // seems to occur only for dead bodies, e.g. DeadMuffy, DeadDogVicious + //std::cout << "ACRE " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XLKR: // FO3 + case ESM4::SUB_XLCM: // FO3 + case ESM4::SUB_XEZN: // FO3 + case ESM4::SUB_XMRC: // FO3 + case ESM4::SUB_XAPD: // FO3 + case ESM4::SUB_XAPR: // FO3 + case ESM4::SUB_XRDS: // FO3 + case ESM4::SUB_XPRD: // FO3 + case ESM4::SUB_XATO: // FONV + { + //std::cout << "ACRE " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ACRE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::ActorCreature::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::ActorCreature::blank() +//{ +//} diff --git a/components/esm4/loadacre.hpp b/components/esm4/loadacre.hpp new file mode 100644 index 0000000000..4be76a5a8a --- /dev/null +++ b/components/esm4/loadacre.hpp @@ -0,0 +1,64 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ACRE_H +#define ESM4_ACRE_H + +#include + +#include "reference.hpp" // FormId, Placement, EnableParent + +namespace ESM4 +{ + class Reader; + class Writer; + + struct ActorCreature + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + FormId mBaseObj; + + Placement mPlacement; + float mScale = 1.0f; + FormId mOwner; + FormId mGlobal; + std::uint32_t mFactionRank; + + bool mInitiallyDisabled; // TODO may need to check mFlags & 0x800 (initially disabled) + + EnableParent mEsp; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ACRE_H diff --git a/components/esm4/loadacti.cpp b/components/esm4/loadacti.cpp new file mode 100644 index 0000000000..8c64e0477c --- /dev/null +++ b/components/esm4/loadacti.cpp @@ -0,0 +1,89 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadacti.hpp" + +#include +#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Activator::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_SNAM: reader.getFormId(mLoopingSound); break; + case ESM4::SUB_VNAM: reader.getFormId(mActivationSound); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_INAM: reader.getFormId(mRadioTemplate); break; // FONV + case ESM4::SUB_RNAM: reader.getFormId(mRadioStation); break; + case ESM4::SUB_XATO: reader.getZString(mActivationPrompt); break; // FONV + case ESM4::SUB_MODT: + case ESM4::SUB_MODS: + case ESM4::SUB_DEST: + case ESM4::SUB_DMDL: + case ESM4::SUB_DMDS: + case ESM4::SUB_DMDT: + case ESM4::SUB_DSTD: + case ESM4::SUB_DSTF: + case ESM4::SUB_FNAM: + case ESM4::SUB_KNAM: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_OBND: + case ESM4::SUB_PNAM: + case ESM4::SUB_VMAD: + case ESM4::SUB_WNAM: + { + //std::cout << "ACTI " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ACTI::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Activator::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Activator::blank() +//{ +//} diff --git a/components/esm4/loadacti.hpp b/components/esm4/loadacti.hpp new file mode 100644 index 0000000000..7d5798c033 --- /dev/null +++ b/components/esm4/loadacti.hpp @@ -0,0 +1,67 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ACTI_H +#define ESM4_ACTI_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Activator + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + FormId mScriptId; + FormId mLoopingSound; // SOUN + FormId mActivationSound; // SOUN + + float mBoundRadius; + + FormId mRadioTemplate; // SOUN + FormId mRadioStation; // TACT + + std::string mActivationPrompt; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ACTI_H diff --git a/components/esm4/loadalch.cpp b/components/esm4/loadalch.cpp new file mode 100644 index 0000000000..579bf309f0 --- /dev/null +++ b/components/esm4/loadalch.cpp @@ -0,0 +1,104 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadalch.hpp" + +#include +#include +//#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Potion::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIcon); break; // FO3 + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_SCIT: + { + reader.get(mEffect); + reader.adjustFormId(mEffect.formId); + break; + } + case ESM4::SUB_ENIT: + { + if (subHdr.dataSize == 8) // TES4 + { + reader.get(&mItem, 8); + mItem.withdrawl = 0; + mItem.sound = 0; + break; + } + + reader.get(mItem); + reader.adjustFormId(mItem.withdrawl); + reader.adjustFormId(mItem.sound); + break; + } + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_EFID: + case ESM4::SUB_EFIT: + case ESM4::SUB_CTDA: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_MODS: + case ESM4::SUB_OBND: + case ESM4::SUB_ETYP: // FO3 + { + //std::cout << "ALCH " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ALCH::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Potion::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Potion::blank() +//{ +//} diff --git a/components/esm4/loadalch.hpp b/components/esm4/loadalch.hpp new file mode 100644 index 0000000000..dbc8b80f82 --- /dev/null +++ b/components/esm4/loadalch.hpp @@ -0,0 +1,85 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ALCH_H +#define ESM4_ALCH_H + +#include +#include + +#include "effect.hpp" // FormId, ScriptEffect + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Potion + { +#pragma pack(push, 1) + struct Data + { + float weight; + }; + + struct EnchantedItem + { + std::int32_t value; + std::uint32_t flags; + FormId withdrawl; + float chanceAddition; + FormId sound; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + std::string mMiniIcon; // inventory + + FormId mPickUpSound; + FormId mDropSound; + + FormId mScriptId; + ScriptEffect mEffect; + + float mBoundRadius; + + Data mData; + EnchantedItem mItem; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ALCH_H diff --git a/components/esm4/loadaloc.cpp b/components/esm4/loadaloc.cpp new file mode 100644 index 0000000000..b14341f682 --- /dev/null +++ b/components/esm4/loadaloc.cpp @@ -0,0 +1,159 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadaloc.hpp" + +#include +#include +//#include // FIXME: for debugging only +//#include // FIXME: for debugging only + +//#include // FIXME + +//#include "formid.hpp" // FIXME: + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::MediaLocationController::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_GNAM: + { + FormId id; + reader.getFormId(id); + mBattleSets.push_back(id); + + break; + } + case ESM4::SUB_LNAM: + { + FormId id; + reader.getFormId(id); + mLocationSets.push_back(id); + + break; + } + case ESM4::SUB_YNAM: + { + FormId id; + reader.getFormId(id); + mEnemySets.push_back(id); + + break; + } + case ESM4::SUB_HNAM: + { + FormId id; + reader.getFormId(id); + mNeutralSets.push_back(id); + + break; + } + case ESM4::SUB_XNAM: + { + FormId id; + reader.getFormId(id); + mFriendSets.push_back(id); + + break; + } + case ESM4::SUB_ZNAM: + { + FormId id; + reader.getFormId(id); + mAllySets.push_back(id); + + break; + } + case ESM4::SUB_RNAM: reader.getFormId(mConditionalFaction); break; + case ESM4::SUB_NAM1: + { + reader.get(mMediaFlags); + std::uint8_t flags = mMediaFlags.loopingOptions; + mMediaFlags.loopingOptions = (flags & 0xF0) >> 4; + mMediaFlags.factionNotFound = flags & 0x0F; // WARN: overwriting data + break; + } + case ESM4::SUB_NAM4: reader.get(mLocationDelay); break; + case ESM4::SUB_NAM7: reader.get(mRetriggerDelay); break; + case ESM4::SUB_NAM5: reader.get(mDayStart); break; + case ESM4::SUB_NAM6: reader.get(mNightStart); break; + case ESM4::SUB_NAM2: // always 0? 4 bytes + case ESM4::SUB_NAM3: // always 0? 4 bytes + case ESM4::SUB_FNAM: // always 0? 4 bytes + { +#if 0 + boost::scoped_array mDataBuf(new unsigned char[subHdr.dataSize]); + reader.get(&mDataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + ss << mEditorId << " " << ESM::printName(subHdr.typeId) << ":size " << subHdr.dataSize << "\n"; + for (std::size_t i = 0; i < subHdr.dataSize; ++i) + { + //if (mDataBuf[i] > 64 && mDataBuf[i] < 91) // looks like printable ascii char + //ss << (char)(mDataBuf[i]) << " "; + //else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]); + if ((i & 0x000f) == 0xf) // wrap around + ss << "\n"; + else if (i < subHdr.dataSize-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + //std::cout << "ALOC " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); +#endif + break; + } + default: + //std::cout << "ALOC " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + throw std::runtime_error("ESM4::ALOC::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::MediaLocationController::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::MediaLocationController::blank() +//{ +//} diff --git a/components/esm4/loadaloc.hpp b/components/esm4/loadaloc.hpp new file mode 100644 index 0000000000..3c5ae39f55 --- /dev/null +++ b/components/esm4/loadaloc.hpp @@ -0,0 +1,85 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ALOC_H +#define ESM4_ALOC_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + +#pragma pack(push, 1) + struct MLC_Flags + { + // use day/night transition: 0 = loop, 1 = random, 2 = retrigger, 3 = none + // use defaults (6:00/23:54): 4 = loop, 5 = random, 6 = retrigger, 7 = none + std::uint8_t loopingOptions; + // 0 = neutral, 1 = enemy, 2 = ally, 3 = friend, 4 = location, 5 = none + std::uint8_t factionNotFound; // WARN: overwriting whatever is in this + std::uint16_t unknown; // padding? + }; +#pragma pack(pop) + + struct MediaLocationController + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + + std::vector mBattleSets; + std::vector mLocationSets; + std::vector mEnemySets; + std::vector mNeutralSets; + std::vector mFriendSets; + std::vector mAllySets; + + MLC_Flags mMediaFlags; + + FormId mConditionalFaction; + + float mLocationDelay; + float mRetriggerDelay; + + std::uint32_t mDayStart; + std::uint32_t mNightStart; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ALOC_H diff --git a/components/esm4/loadammo.cpp b/components/esm4/loadammo.cpp new file mode 100644 index 0000000000..16cd7cab1c --- /dev/null +++ b/components/esm4/loadammo.cpp @@ -0,0 +1,119 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadammo.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Ammunition::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 16) // FO3 has 13 bytes even though VER_094 + { + FormId projectile; + reader.get(projectile); // FIXME: add to mData + reader.get(mData.flags); + reader.get(mData.weight); + float damageInFloat; + reader.get(damageInFloat); // FIXME: add to mData + } + else if (isFONV || subHdr.dataSize == 13) + { + reader.get(mData.speed); + std::uint8_t flags; + reader.get(flags); + mData.flags = flags; + static std::uint8_t dummy; + reader.get(dummy); + reader.get(dummy); + reader.get(dummy); + reader.get(mData.value); + reader.get(mData.clipRounds); + } + else // TES4 + { + reader.get(mData.speed); + reader.get(mData.flags); + reader.get(mData.value); + reader.get(mData.weight); + reader.get(mData.damage); + } + break; + } + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIcon); break; // FO3 + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ANAM: reader.get(mEnchantmentPoints); break; + case ESM4::SUB_ENAM: reader.getFormId(mEnchantment); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_OBND: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_ONAM: // FO3 + case ESM4::SUB_DAT2: // FONV + case ESM4::SUB_QNAM: // FONV + case ESM4::SUB_RCIL: // FONV + case ESM4::SUB_SCRI: // FONV + { + //std::cout << "AMMO " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::AMMO::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Ammunition::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Ammunition::blank() +//{ +//} diff --git a/components/esm4/loadammo.hpp b/components/esm4/loadammo.hpp new file mode 100644 index 0000000000..b399245a1b --- /dev/null +++ b/components/esm4/loadammo.hpp @@ -0,0 +1,81 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_AMMO_H +#define ESM4_AMMO_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Ammunition + { + struct Data // FIXME: TES5 projectile, damage (float) + { + float speed; + std::uint32_t flags; + std::uint32_t value; // gold + float weight; + std::uint16_t damage; + std::uint8_t clipRounds; // only in FO3/FONV + + Data() : speed(0.f), flags(0), value(0), weight(0.f), damage(0), clipRounds(0) {} + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mText; + std::string mIcon; // inventory + std::string mMiniIcon; // inventory + + FormId mPickUpSound; + FormId mDropSound; + + float mBoundRadius; + + std::uint16_t mEnchantmentPoints; + FormId mEnchantment; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_AMMO_H diff --git a/components/esm4/loadanio.cpp b/components/esm4/loadanio.cpp new file mode 100644 index 0000000000..86441e36ee --- /dev/null +++ b/components/esm4/loadanio.cpp @@ -0,0 +1,69 @@ +/* + Copyright (C) 2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadanio.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::AnimObject::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_BNAM: reader.getZString(mUnloadEvent); break; + case ESM4::SUB_DATA: reader.getFormId(mIdleAnim); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: // TES5 only + case ESM4::SUB_MODS: // TES5 only + { + //std::cout << "ANIO " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ANIO::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::AnimObject::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::AnimObject::blank() +//{ +//} diff --git a/components/esm4/loadanio.hpp b/components/esm4/loadanio.hpp new file mode 100644 index 0000000000..5e40b5f9d8 --- /dev/null +++ b/components/esm4/loadanio.hpp @@ -0,0 +1,60 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ANIO_H +#define ESM4_ANIO_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct AnimObject + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + float mBoundRadius; + + FormId mIdleAnim; // only in TES4 + std::string mUnloadEvent; // only in TES5 + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ANIO_H diff --git a/components/esm4/loadappa.cpp b/components/esm4/loadappa.cpp new file mode 100644 index 0000000000..a7814d99c4 --- /dev/null +++ b/components/esm4/loadappa.cpp @@ -0,0 +1,88 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadappa.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Apparatus::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + { + reader.get(mData.value); + reader.get(mData.weight); + } + else + { + reader.get(mData.type); + reader.get(mData.value); + reader.get(mData.weight); + reader.get(mData.quality); + } + break; + } + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_MODT: + case ESM4::SUB_OBND: + case ESM4::SUB_QUAL: + { + //std::cout << "APPA " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::APPA::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Apparatus::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Apparatus::blank() +//{ +//} diff --git a/components/esm4/loadappa.hpp b/components/esm4/loadappa.hpp new file mode 100644 index 0000000000..7e4e48ce5a --- /dev/null +++ b/components/esm4/loadappa.hpp @@ -0,0 +1,72 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_APPA_H +#define ESM4_APPA_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Apparatus + { + struct Data + { + std::uint8_t type; // 0 = Mortar and Pestle, 1 = Alembic, 2 = Calcinator, 3 = Retort + std::uint32_t value; // gold + float weight; + float quality; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mText; + std::string mIcon; // inventory + + float mBoundRadius; + + FormId mScriptId; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_APPA_H diff --git a/components/esm4/loadarma.cpp b/components/esm4/loadarma.cpp new file mode 100644 index 0000000000..389a5b122c --- /dev/null +++ b/components/esm4/loadarma.cpp @@ -0,0 +1,138 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadarma.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::ArmorAddon::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + std::uint32_t esmVer = reader.esmVersion(); + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MOD2: reader.getZString(mModelMale); break; + case ESM4::SUB_MOD3: reader.getZString(mModelFemale); break; + case ESM4::SUB_MOD4: + case ESM4::SUB_MOD5: + { + std::string model; + reader.getZString(model); + + //std::cout << mEditorId << " " << ESM::printName(subHdr.typeId) << " " << model << std::endl; + + break; + } + case ESM4::SUB_NAM0: reader.getFormId(mTextureMale); break; + case ESM4::SUB_NAM1: reader.getFormId(mTextureFemale); break; + case ESM4::SUB_RNAM: reader.getFormId(mRacePrimary); break; + case ESM4::SUB_MODL: + { + if ((esmVer == ESM::VER_094 || esmVer == ESM::VER_170) && subHdr.dataSize == 4) // TES5 + { + FormId formId; + reader.getFormId(formId); + mRaces.push_back(formId); + } + else + reader.skipSubRecordData(); // FIXME: this should be mModelMale for FO3/FONV + + break; + } + case ESM4::SUB_BODT: // body template + { + reader.get(mBodyTemplate.bodyPart); + reader.get(mBodyTemplate.flags); + reader.get(mBodyTemplate.unknown1); // probably padding + reader.get(mBodyTemplate.unknown2); // probably padding + reader.get(mBodyTemplate.unknown3); // probably padding + reader.get(mBodyTemplate.type); + + break; + } + case ESM4::SUB_BOD2: // TES5 + { + reader.get(mBodyTemplate.bodyPart); + mBodyTemplate.flags = 0; + mBodyTemplate.unknown1 = 0; // probably padding + mBodyTemplate.unknown2 = 0; // probably padding + mBodyTemplate.unknown3 = 0; // probably padding + reader.get(mBodyTemplate.type); + + break; + } + case ESM4::SUB_DNAM: + case ESM4::SUB_MO2T: // FIXME: should group with MOD2 + case ESM4::SUB_MO2S: // FIXME: should group with MOD2 + case ESM4::SUB_MO3T: // FIXME: should group with MOD3 + case ESM4::SUB_MO3S: // FIXME: should group with MOD3 + case ESM4::SUB_MOSD: // FO3 // FIXME: should group with MOD3 + case ESM4::SUB_MO4T: // FIXME: should group with MOD4 + case ESM4::SUB_MO4S: // FIXME: should group with MOD4 + case ESM4::SUB_MO5T: + case ESM4::SUB_NAM2: // txst formid male + case ESM4::SUB_NAM3: // txst formid female + case ESM4::SUB_SNDD: // footset sound formid + case ESM4::SUB_BMDT: // FO3 + case ESM4::SUB_DATA: // FO3 + case ESM4::SUB_ETYP: // FO3 + case ESM4::SUB_FULL: // FO3 + case ESM4::SUB_ICO2: // FO3 // female + case ESM4::SUB_ICON: // FO3 // male + case ESM4::SUB_MODT: // FO3 // FIXME: should group with MODL + case ESM4::SUB_MODS: // FO3 // FIXME: should group with MODL + case ESM4::SUB_MODD: // FO3 // FIXME: should group with MODL + case ESM4::SUB_OBND: // FO3 + { + //std::cout << "ARMA " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ARMA::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::ArmorAddon::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::ArmorAddon::blank() +//{ +//} diff --git a/components/esm4/loadarma.hpp b/components/esm4/loadarma.hpp new file mode 100644 index 0000000000..8bee9d0396 --- /dev/null +++ b/components/esm4/loadarma.hpp @@ -0,0 +1,67 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ARMA_H +#define ESM4_ARMA_H + +#include +#include +#include + +#include "formid.hpp" +#include "actor.hpp" // BodyTemplate + +namespace ESM4 +{ + class Reader; + class Writer; + + struct ArmorAddon + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::string mModelMale; + std::string mModelFemale; + + FormId mTextureMale; + FormId mTextureFemale; + + FormId mRacePrimary; + std::vector mRaces; // TES5 only + + BodyTemplate mBodyTemplate; // TES5 + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ARMA_H diff --git a/components/esm4/loadarmo.cpp b/components/esm4/loadarmo.cpp new file mode 100644 index 0000000000..b2b43e1111 --- /dev/null +++ b/components/esm4/loadarmo.cpp @@ -0,0 +1,192 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadarmo.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Armor::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + mIsFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 8) // FO3 has 12 bytes even though VER_094 + { + reader.get(mData.value); + reader.get(mData.weight); + mIsFO3 = true; + } + else if (mIsFONV || subHdr.dataSize == 12) + { + reader.get(mData.value); + reader.get(mData.health); + reader.get(mData.weight); + } + else + { + reader.get(mData); // TES4 + mIsTES4 = true; + } + + break; + } + case ESM4::SUB_MODL: // seems only for Dawnguard/Dragonborn? + { + //if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + if (subHdr.dataSize == 4) // FO3 has zstring even though VER_094 + { + FormId formId; + reader.getFormId(formId); + + mAddOns.push_back(formId); + } + else + { + if (!reader.getZString(mModelMale)) + throw std::runtime_error ("ARMO MODL data read error"); + } + + break; + } + case ESM4::SUB_MOD2: reader.getZString(mModelMaleWorld);break; + case ESM4::SUB_MOD3: reader.getZString(mModelFemale); break; + case ESM4::SUB_MOD4: reader.getZString(mModelFemaleWorld); break; + case ESM4::SUB_ICON: reader.getZString(mIconMale); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIconMale); break; + case ESM4::SUB_ICO2: reader.getZString(mIconFemale); break; + case ESM4::SUB_MIC2: reader.getZString(mMiniIconFemale); break; + case ESM4::SUB_BMDT: + { + if (subHdr.dataSize == 8) // FO3 + { + reader.get(mArmorFlags); + reader.get(mGeneralFlags); + mGeneralFlags &= 0x000000ff; + mGeneralFlags |= TYPE_FO3; + } + else // TES4 + { + reader.get(mArmorFlags); + mGeneralFlags = (mArmorFlags & 0x00ff0000) >> 16; + mGeneralFlags |= TYPE_TES4; + } + break; + } + case ESM4::SUB_BODT: + { + reader.get(mArmorFlags); + uint32_t flags = 0; + if (subHdr.dataSize == 12) + reader.get(flags); + reader.get(mGeneralFlags); // skill + mGeneralFlags &= 0x0000000f; // 0 (light), 1 (heavy) or 2 (none) + if (subHdr.dataSize == 12) + mGeneralFlags |= (flags & 0x0000000f) << 3; + mGeneralFlags |= TYPE_TES5; + break; + } + case ESM4::SUB_BOD2: + { + reader.get(mArmorFlags); + reader.get(mGeneralFlags); + mGeneralFlags &= 0x0000000f; // 0 (light), 1 (heavy) or 2 (none) + mGeneralFlags |= TYPE_TES5; + break; + } + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_ANAM: reader.get(mEnchantmentPoints); break; + case ESM4::SUB_ENAM: reader.getFormId(mEnchantment); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_MO2B: + case ESM4::SUB_MO3B: + case ESM4::SUB_MO4B: + case ESM4::SUB_MO2T: + case ESM4::SUB_MO2S: + case ESM4::SUB_MO3T: + case ESM4::SUB_MO4T: + case ESM4::SUB_MO4S: + case ESM4::SUB_OBND: + case ESM4::SUB_RNAM: // race formid + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_TNAM: + case ESM4::SUB_DNAM: + case ESM4::SUB_BAMT: + case ESM4::SUB_BIDS: + case ESM4::SUB_ETYP: + case ESM4::SUB_BMCT: + case ESM4::SUB_EAMT: + case ESM4::SUB_EITM: + case ESM4::SUB_VMAD: + case ESM4::SUB_REPL: // FO3 + case ESM4::SUB_BIPL: // FO3 + case ESM4::SUB_MODD: // FO3 + case ESM4::SUB_MOSD: // FO3 + case ESM4::SUB_MODS: // FO3 + case ESM4::SUB_MO3S: // FO3 + case ESM4::SUB_BNAM: // FONV + case ESM4::SUB_SNAM: // FONV + { + //std::cout << "ARMO " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ARMO::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + //if ((mArmorFlags&0xffff) == 0x02) // only hair + //std::cout << "only hair " << mEditorId << std::endl; +} + +//void ESM4::Armor::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Armor::blank() +//{ +//} diff --git a/components/esm4/loadarmo.hpp b/components/esm4/loadarmo.hpp new file mode 100644 index 0000000000..e7f6fa5a7c --- /dev/null +++ b/components/esm4/loadarmo.hpp @@ -0,0 +1,193 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ARMO_H +#define ESM4_ARMO_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Armor + { + // WARN: TES4 Armorflags still has the general flags high bits + enum ArmorFlags + { + TES4_Head = 0x00000001, + TES4_Hair = 0x00000002, + TES4_UpperBody = 0x00000004, + TES4_LowerBody = 0x00000008, + TES4_Hands = 0x00000010, + TES4_Feet = 0x00000020, + TES4_RightRing = 0x00000040, + TES4_LeftRing = 0x00000080, + TES4_Amulet = 0x00000100, + TES4_Weapon = 0x00000200, + TES4_BackWeapon = 0x00000400, + TES4_SideWeapon = 0x00000800, + TES4_Quiver = 0x00001000, + TES4_Shield = 0x00002000, + TES4_Torch = 0x00004000, + TES4_Tail = 0x00008000, + // + FO3_Head = 0x00000001, + FO3_Hair = 0x00000002, + FO3_UpperBody = 0x00000004, + FO3_LeftHand = 0x00000008, + FO3_RightHand = 0x00000010, + FO3_Weapon = 0x00000020, + FO3_PipBoy = 0x00000040, + FO3_Backpack = 0x00000080, + FO3_Necklace = 0x00000100, + FO3_Headband = 0x00000200, + FO3_Hat = 0x00000400, + FO3_EyeGlasses = 0x00000800, + FO3_NoseRing = 0x00001000, + FO3_Earrings = 0x00002000, + FO3_Mask = 0x00004000, + FO3_Choker = 0x00008000, + FO3_MouthObject = 0x00010000, + FO3_BodyAddOn1 = 0x00020000, + FO3_BodyAddOn2 = 0x00040000, + FO3_BodyAddOn3 = 0x00080000, + // + TES5_Head = 0x00000001, + TES5_Hair = 0x00000002, + TES5_Body = 0x00000004, + TES5_Hands = 0x00000008, + TES5_Forearms = 0x00000010, + TES5_Amulet = 0x00000020, + TES5_Ring = 0x00000040, + TES5_Feet = 0x00000080, + TES5_Calves = 0x00000100, + TES5_Shield = 0x00000200, + TES5_Tail = 0x00000400, + TES5_LongHair = 0x00000800, + TES5_Circlet = 0x00001000, + TES5_Ears = 0x00002000, + TES5_BodyAddOn3 = 0x00004000, + TES5_BodyAddOn4 = 0x00008000, + TES5_BodyAddOn5 = 0x00010000, + TES5_BodyAddOn6 = 0x00020000, + TES5_BodyAddOn7 = 0x00040000, + TES5_BodyAddOn8 = 0x00080000, + TES5_DecapHead = 0x00100000, + TES5_Decapitate = 0x00200000, + TES5_BodyAddOn9 = 0x00400000, + TES5_BodyAddOn10 = 0x00800000, + TES5_BodyAddOn11 = 0x01000000, + TES5_BodyAddOn12 = 0x02000000, + TES5_BodyAddOn13 = 0x04000000, + TES5_BodyAddOn14 = 0x08000000, + TES5_BodyAddOn15 = 0x10000000, + TES5_BodyAddOn16 = 0x20000000, + TES5_BodyAddOn17 = 0x40000000, + TES5_FX01 = 0x80000000 + }; + + enum GeneralFlags + { + TYPE_TES4 = 0x1000, + TYPE_FO3 = 0x2000, + TYPE_TES5 = 0x3000, + TYPE_FONV = 0x4000, + // + TES4_HideRings = 0x0001, + TES4_HideAmulet = 0x0002, + TES4_NonPlayable = 0x0040, + TES4_HeavyArmor = 0x0080, + // + FO3_PowerArmor = 0x0020, + FO3_NonPlayable = 0x0040, + FO3_HeavyArmor = 0x0080, + // + TES5_LightArmor = 0x0000, + TES5_HeavyArmor = 0x0001, + TES5_None = 0x0002, + TES5_ModVoice = 0x0004, // note bit shift + TES5_NonPlayable = 0x0040 // note bit shift + }; + +#pragma pack(push, 1) + struct Data + { + std::uint16_t armor; // only in TES4? + std::uint32_t value; + std::uint32_t health; // not in TES5? + float weight; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + bool mIsTES4; // TODO: check that these match the general flags + bool mIsFO3; + bool mIsFONV; + + std::string mEditorId; + std::string mFullName; + std::string mModelMale; + std::string mModelMaleWorld; + std::string mModelFemale; + std::string mModelFemaleWorld; + std::string mText; + std::string mIconMale; + std::string mMiniIconMale; + std::string mIconFemale; + std::string mMiniIconFemale; + + FormId mPickUpSound; + FormId mDropSound; + + std::string mModel; // FIXME: for OpenCS + + float mBoundRadius; + + std::uint32_t mArmorFlags; + std::uint32_t mGeneralFlags; + FormId mScriptId; + std::uint16_t mEnchantmentPoints; + FormId mEnchantment; + + std::vector mAddOns; // TES5 ARMA + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ARMO_H diff --git a/components/esm4/loadaspc.cpp b/components/esm4/loadaspc.cpp new file mode 100644 index 0000000000..b1e856056a --- /dev/null +++ b/components/esm4/loadaspc.cpp @@ -0,0 +1,84 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadaspc.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::AcousticSpace::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_ANAM: reader.get(mEnvironmentType); break; + case ESM4::SUB_SNAM: + { + FormId id; + reader.getFormId(id); + mAmbientLoopSounds.push_back(id); + break; + } + case ESM4::SUB_RDAT: reader.getFormId(mSoundRegion); break; + case ESM4::SUB_INAM: reader.get(mIsInterior); break; + case ESM4::SUB_WNAM: // usually 0 for FONV (maybe # of close Actors for Walla to trigger) + { + std::uint32_t dummy; + reader.get(dummy); + //std::cout << "WNAM " << mEditorId << " " << dummy << std::endl; + break; + } + case ESM4::SUB_BNAM: // TES5 reverb formid + case ESM4::SUB_OBND: + { + //std::cout << "ASPC " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::ASPC::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::AcousticSpace::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::AcousticSpace::blank() +//{ +//} diff --git a/components/esm4/loadaspc.hpp b/components/esm4/loadaspc.hpp new file mode 100644 index 0000000000..9fd17ea27c --- /dev/null +++ b/components/esm4/loadaspc.hpp @@ -0,0 +1,63 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ASPC_H +#define ESM4_ASPC_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct AcousticSpace + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::uint32_t mEnvironmentType; + + // 0 Dawn (5:00 start), 1 Afternoon (8:00), 2 Dusk (18:00), 3 Night (20:00) + std::vector mAmbientLoopSounds; + FormId mSoundRegion; + + std::uint32_t mIsInterior; // if true only use mAmbientLoopSounds[0] + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ASPC_H diff --git a/components/esm4/loadbook.cpp b/components/esm4/loadbook.cpp new file mode 100644 index 0000000000..c17ef102be --- /dev/null +++ b/components/esm4/loadbook.cpp @@ -0,0 +1,101 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadbook.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Book::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + //std::uint32_t esmVer = reader.esmVersion(); // currently unused + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_DATA: + { + reader.get(mData.flags); + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 16) // FO3 has 10 bytes even though VER_094 + { + static std::uint8_t dummy; + reader.get(mData.type); + reader.get(dummy); + reader.get(dummy); + reader.get(mData.teaches); + } + else + { + reader.get(mData.bookSkill); + } + reader.get(mData.value); + reader.get(mData.weight); + break; + } + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_ANAM: reader.get(mEnchantmentPoints); break; + case ESM4::SUB_ENAM: reader.getFormId(mEnchantment); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; // TODO: does this exist? + case ESM4::SUB_MODT: + case ESM4::SUB_OBND: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_CNAM: + case ESM4::SUB_INAM: + case ESM4::SUB_VMAD: + { + //std::cout << "BOOK " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::BOOK::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Book::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Book::blank() +//{ +//} diff --git a/components/esm4/loadbook.hpp b/components/esm4/loadbook.hpp new file mode 100644 index 0000000000..a1176a3320 --- /dev/null +++ b/components/esm4/loadbook.hpp @@ -0,0 +1,111 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_BOOK_H +#define ESM4_BOOK_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Book + { + enum Flags + { + Flag_Scroll = 0x0001, + Flag_NoTake = 0x0002 + }; + + enum BookSkill // for TES4 only + { + BookSkill_None = -1, + BookSkill_Armorer = 0, + BookSkill_Athletics = 1, + BookSkill_Blade = 2, + BookSkill_Block = 3, + BookSkill_Blunt = 4, + BookSkill_HandToHand = 5, + BookSkill_HeavyArmor = 6, + BookSkill_Alchemy = 7, + BookSkill_Alteration = 8, + BookSkill_Conjuration = 9, + BookSkill_Destruction = 10, + BookSkill_Illusion = 11, + BookSkill_Mysticism = 12, + BookSkill_Restoration = 13, + BookSkill_Acrobatics = 14, + BookSkill_LightArmor = 15, + BookSkill_Marksman = 16, + BookSkill_Mercantile = 17, + BookSkill_Security = 18, + BookSkill_Sneak = 19, + BookSkill_Speechcraft = 20 + }; + + struct Data + { + std::uint8_t flags; + std::uint8_t type; // TES5 only + std::uint32_t teaches; // TES5 only + std::int8_t bookSkill; // not in TES5 + std::uint32_t value; + float weight; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + float mBoundRadius; + + std::string mText; + FormId mScriptId; + std::string mIcon; + std::uint16_t mEnchantmentPoints; + FormId mEnchantment; + + Data mData; + + FormId mPickUpSound; + FormId mDropSound; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_BOOK_H diff --git a/components/esm4/loadbptd.cpp b/components/esm4/loadbptd.cpp new file mode 100644 index 0000000000..33229edd07 --- /dev/null +++ b/components/esm4/loadbptd.cpp @@ -0,0 +1,102 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadbptd.hpp" + +#include +#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::BodyPartData::BodyPart::clear() +{ + mPartName.clear(); + mNodeName.clear(); + mVATSTarget.clear(); + mIKStartNode.clear(); + std::memset(&mData, 0, sizeof(BPND)); + mLimbReplacementModel.clear(); + mGoreEffectsTarget.clear(); +} + +void ESM4::BodyPartData::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + BodyPart bodyPart; + bodyPart.clear(); + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_BPTN: reader.getLocalizedString(bodyPart.mPartName); break; + case ESM4::SUB_BPNN: reader.getZString(bodyPart.mNodeName); break; + case ESM4::SUB_BPNT: reader.getZString(bodyPart.mVATSTarget); break; + case ESM4::SUB_BPNI: reader.getZString(bodyPart.mIKStartNode); break; + case ESM4::SUB_BPND: reader.get(bodyPart.mData); break; + case ESM4::SUB_NAM1: reader.getZString(bodyPart.mLimbReplacementModel); break; + case ESM4::SUB_NAM4: // FIXME: assumed occurs last + { + reader.getZString(bodyPart.mGoreEffectsTarget); // target bone + + mBodyParts.push_back(bodyPart); // FIXME: possible without copying? + + bodyPart.clear(); + break; + } + case ESM4::SUB_NAM5: + case ESM4::SUB_RAGA: // ragdoll + case ESM4::SUB_MODS: + case ESM4::SUB_MODT: + { + //std::cout << "BPTD " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::BPTD::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + + //if (mEditorId == "DefaultBodyPartData") + //std::cout << "BPTD: " << mEditorId << std::endl; // FIXME: testing only +} + +//void ESM4::BodyPartData::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::BodyPartData::blank() +//{ +//} diff --git a/components/esm4/loadbptd.hpp b/components/esm4/loadbptd.hpp new file mode 100644 index 0000000000..f831a53653 --- /dev/null +++ b/components/esm4/loadbptd.hpp @@ -0,0 +1,125 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_BPTD_H +#define ESM4_BPTD_H + +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct BodyPartData + { +#pragma pack(push, 1) + struct BPND + { + float damageMult; + + // Severable + // IK Data + // IK Data - Biped Data + // Explodable + // IK Data - Is Head + // IK Data - Headtracking + // To Hit Chance - Absolute + std::uint8_t flags; + + // Torso + // Head + // Eye + // LookAt + // Fly Grab + // Saddle + std::uint8_t partType; + + std::uint8_t healthPercent; + std::int8_t actorValue; //(Actor Values) + std::uint8_t toHitChance; + + std::uint8_t explExplosionChance; // % + std::uint16_t explDebrisCount; + FormId explDebris; + FormId explExplosion; + float trackingMaxAngle; + float explDebrisScale; + + std::int32_t sevDebrisCount; + FormId sevDebris; + FormId sevExplosion; + float sevDebrisScale; + + //Struct - Gore Effects Positioning + float transX; + float transY; + float transZ; + float rotX; + float rotY; + float rotZ; + + FormId sevImpactDataSet; + FormId explImpactDataSet; + uint8_t sevDecalCount; + uint8_t explDecalCount; + uint16_t Unknown; + float limbReplacementScale; + }; +#pragma pack(pop) + + struct BodyPart + { + std::string mPartName; + std::string mNodeName; + std::string mVATSTarget; + std::string mIKStartNode; + BPND mData; + std::string mLimbReplacementModel; + std::string mGoreEffectsTarget; + + void clear(); + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + std::vector mBodyParts; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_BPTD_H diff --git a/components/esm4/loadcell.cpp b/components/esm4/loadcell.cpp new file mode 100644 index 0000000000..6da827499d --- /dev/null +++ b/components/esm4/loadcell.cpp @@ -0,0 +1,227 @@ +/* + Copyright (C) 2015-2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadcell.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include +#include // FLT_MAX for gcc + +#include // FIXME: debug only + +#include "reader.hpp" +//#include "writer.hpp" + +// TODO: Try loading only EDID and XCLC (along with mFormId, mFlags and mParent) +// +// But, for external cells we may be scanning the whole record since we don't know if there is +// going to be an EDID subrecord. And the vast majority of cells are these kinds. +// +// So perhaps some testing needs to be done to see if scanning and skipping takes +// longer/shorter/same as loading the subrecords. +void ESM4::Cell::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + mParent = reader.currWorld(); + + reader.clearCellGrid(); // clear until XCLC FIXME: somehow do this automatically? + + // Sometimes cell 0,0 does not have an XCLC sub record (e.g. ToddLand 000009BF) + // To workaround this issue put a default value if group is "exterior sub cell" and its + // grid from label is "0 0". Note the reversed X/Y order (no matter since they're both 0 + // anyway). + if (reader.grp().type == ESM4::Grp_ExteriorSubCell + && reader.grp().label.grid[1] == 0 && reader.grp().label.grid[0] == 0) + { + ESM4::CellGrid currCellGrid; + currCellGrid.grid.x = 0; + currCellGrid.grid.y = 0; + reader.setCurrCellGrid(currCellGrid); // side effect: sets mCellGridValid true + } + + // WARN: we need to call setCurrCell (and maybe setCurrCellGrid?) again before loading + // cell child groups if we are loading them after restoring the context + // (may be easier to update the context before saving?) + reader.setCurrCell(mFormId); // save for LAND (and other children) to access later + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: + { + if (!reader.getZString(mEditorId)) + throw std::runtime_error ("CELL EDID data read error"); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "CELL Editor ID: " << mEditorId << std::endl; +#endif + break; + } + case ESM4::SUB_XCLC: + { + //(X, Y) grid location of the cell followed by flags. Always in + //exterior cells and never in interior cells. + // + // int32 - X + // int32 - Y + // uint32 - flags (high bits look random) + // + // 0x1 - Force Hide Land Quad 1 + // 0x2 - Force Hide Land Quad 2 + // 0x4 - Force Hide Land Quad 3 + // 0x8 - Force Hide Land Quad 4 + uint32_t flags; + reader.get(mX); + reader.get(mY); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "CELL group " << ESM4::printLabel(reader.grp().label, reader.grp().type) << std::endl; + std::cout << padding << "CELL formId " << std::hex << reader.hdr().record.id << std::endl; + std::cout << padding << "CELL X " << std::dec << mX << ", Y " << mY << std::endl; +#endif + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + if (subHdr.dataSize == 12) + reader.get(flags); // not in Obvlivion, nor FO3/FONV + + // Remember cell grid for later (loading LAND, NAVM which should be CELL temporary children) + // Note that grids only apply for external cells. For interior cells use the cell's formid. + ESM4::CellGrid currCell; + currCell.grid.x = (int16_t)mX; + currCell.grid.y = (int16_t)mY; + reader.setCurrCellGrid(currCell); + + break; + } + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + if (subHdr.dataSize == 2) + reader.get(mCellFlags); + else + { + assert(subHdr.dataSize == 1 && "CELL unexpected DATA flag size"); + reader.get(&mCellFlags, sizeof(std::uint8_t)); + } + else + { + reader.get((std::uint8_t&)mCellFlags); // 8 bits in Obvlivion + } +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "flags: " << std::hex << mCellFlags << std::endl; +#endif + break; + } + case ESM4::SUB_XCLR: // for exterior cells + { + mRegions.resize(subHdr.dataSize/sizeof(FormId)); + for (std::vector::iterator it = mRegions.begin(); it != mRegions.end(); ++it) + { + reader.getFormId(*it); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "region: " << std::hex << *it << std::endl; +#endif + } + break; + } + case ESM4::SUB_XOWN: reader.getFormId(mOwner); break; + case ESM4::SUB_XGLB: reader.getFormId(mGlobal); break; // Oblivion only? + case ESM4::SUB_XCCM: reader.getFormId(mClimate); break; + case ESM4::SUB_XCWT: reader.getFormId(mWater); break; + case ESM4::SUB_XCLW: reader.get(mWaterHeight); break; + case ESM4::SUB_XCLL: + { + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + { + if (subHdr.dataSize == 40) // FO3/FONV + reader.get(mLighting); + else if (subHdr.dataSize == 92) // TES5 + { + reader.get(mLighting); + reader.skipSubRecordData(52); // FIXME + } + else + reader.skipSubRecordData(); + } + else + reader.get(&mLighting, 36); // TES4 + + break; + } + case ESM4::SUB_XCMT: reader.get(mMusicType); break; // Oblivion only? + case ESM4::SUB_LTMP: reader.getFormId(mLightingTemplate); break; + case ESM4::SUB_LNAM: reader.get(mLightingTemplateFlags); break; // seems to always follow LTMP + case ESM4::SUB_XCMO: reader.getFormId(mMusic); break; + case ESM4::SUB_XCAS: reader.getFormId(mAcousticSpace); break; + case ESM4::SUB_TVDT: + case ESM4::SUB_MHDT: + case ESM4::SUB_XCGD: + case ESM4::SUB_XNAM: + case ESM4::SUB_XLCN: + case ESM4::SUB_XWCS: + case ESM4::SUB_XWCU: + case ESM4::SUB_XWCN: + case ESM4::SUB_XCIM: + case ESM4::SUB_XEZN: + case ESM4::SUB_XWEM: + case ESM4::SUB_XILL: + case ESM4::SUB_XRNK: // Oblivion only? + case ESM4::SUB_XCET: // FO3 + case ESM4::SUB_IMPF: // FO3 Zeta + { + //std::cout << "CELL " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::CELL::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Cell::save(ESM4::Writer& writer) const +//{ +//} + +void ESM4::Cell::blank() +{ +} diff --git a/components/esm4/loadcell.hpp b/components/esm4/loadcell.hpp new file mode 100644 index 0000000000..ff51c64f00 --- /dev/null +++ b/components/esm4/loadcell.hpp @@ -0,0 +1,101 @@ +/* + Copyright (C) 2015-2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CELL_H +#define ESM4_CELL_H + +#include +#include +#include + +#include "formid.hpp" +#include "lighting.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + struct ReaderContext; + struct CellGroup; + typedef std::uint32_t FormId; + + enum CellFlags // TES4 TES5 + { // ----------------------- ------------------------------------ + CELL_Interior = 0x0001, // Can't travel from here Interior + CELL_HasWater = 0x0002, // Has water (Int) Has Water (Int) + CELL_NoTravel = 0x0004, // not Can't Travel From Here(Int only) + CELL_HideLand = 0x0008, // Force hide land (Ext) No LOD Water + // Oblivion interior (Int) + CELL_Public = 0x0020, // Public place Public Area + CELL_HandChgd = 0x0040, // Hand changed Hand Changed + CELL_QuasiExt = 0x0080, // Behave like exterior Show Sky + CELL_SkyLight = 0x0100 // Use Sky Lighting + }; + + // Unlike TES3, multiple cells can have the same exterior co-ordinates. + // The cells need to be organised under world spaces. + struct Cell + { + FormId mParent; // world formId (for grouping cells), from the loading sequence + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::uint16_t mCellFlags; // TES5 can also be 8 bits + + std::int32_t mX; + std::int32_t mY; + + FormId mOwner; + FormId mGlobal; + FormId mClimate; + FormId mWater; + float mWaterHeight; + + std::vector mRegions; + Lighting mLighting; + + FormId mLightingTemplate; // FO3/FONV + std::uint32_t mLightingTemplateFlags; // FO3/FONV + + FormId mMusic; // FO3/FONV + FormId mAcousticSpace; // FO3/FONV + // TES4: 0 = default, 1 = public, 2 = dungeon + // FO3/FONV have more types (not sure how they are used, however) + std::uint8_t mMusicType; + + CellGroup *mCellGroup; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + void blank(); + }; +} + +#endif // ESM4_CELL_H diff --git a/components/esm4/loadclas.cpp b/components/esm4/loadclas.cpp new file mode 100644 index 0000000000..eca978276b --- /dev/null +++ b/components/esm4/loadclas.cpp @@ -0,0 +1,72 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadclas.hpp" + +//#ifdef NDEBUG // FIXME: debugging only +//#undef NDEBUG +//#endif + +//#include +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Class::load(ESM4::Reader& reader) +{ + //mFormId = reader.adjustFormId(reader.hdr().record.id); // FIXME: use master adjusted? + mFormId = reader.hdr().record.id; + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mDesc); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_DATA: + { + //std::cout << "CLAS " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::CLAS::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Class::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Class::blank() +//{ +//} diff --git a/components/esm4/loadclas.hpp b/components/esm4/loadclas.hpp new file mode 100644 index 0000000000..bf091fa7f4 --- /dev/null +++ b/components/esm4/loadclas.hpp @@ -0,0 +1,63 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CLAS_H +#define ESM4_CLAS_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Class + { + struct Data + { + std::uint32_t attr; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mDesc; + std::string mIcon; + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& reader) const; + + //void blank(); + }; +} + +#endif // ESM4_CLAS_H diff --git a/components/esm4/loadclfm.cpp b/components/esm4/loadclfm.cpp new file mode 100644 index 0000000000..efaf1461e4 --- /dev/null +++ b/components/esm4/loadclfm.cpp @@ -0,0 +1,78 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadclfm.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Colour::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_CNAM: + { + reader.get(mColour.red); + reader.get(mColour.green); + reader.get(mColour.blue); + reader.get(mColour.custom); + + break; + } + case ESM4::SUB_FNAM: + { + reader.get(mPlayable); + + break; + } + default: + //std::cout << "CLFM " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + throw std::runtime_error("ESM4::CLFM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Colour::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Colour::blank() +//{ +//} diff --git a/components/esm4/loadclfm.hpp b/components/esm4/loadclfm.hpp new file mode 100644 index 0000000000..4fb81c6e00 --- /dev/null +++ b/components/esm4/loadclfm.hpp @@ -0,0 +1,67 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CLFM_H +#define ESM4_CLFM_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + // FIXME: duplicate with Npc + struct ColourRGB + { + std::uint8_t red; + std::uint8_t green; + std::uint8_t blue; + std::uint8_t custom; // alpha? + }; + + struct Colour + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + + ColourRGB mColour; + std::uint32_t mPlayable; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_CLFM_H diff --git a/components/esm4/loadclot.cpp b/components/esm4/loadclot.cpp new file mode 100644 index 0000000000..cdcc692caa --- /dev/null +++ b/components/esm4/loadclot.cpp @@ -0,0 +1,86 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadclot.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Clothing::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_BMDT: reader.get(mClothingFlags); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_ENAM: reader.getFormId(mEnchantment); break; + case ESM4::SUB_ANAM: reader.get(mEnchantmentPoints); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODL: reader.getZString(mModelMale); break; + case ESM4::SUB_MOD2: reader.getZString(mModelMaleWorld); break; + case ESM4::SUB_MOD3: reader.getZString(mModelFemale); break; + case ESM4::SUB_MOD4: reader.getZString(mModelFemaleWorld); break; + case ESM4::SUB_ICON: reader.getZString(mIconMale); break; + case ESM4::SUB_ICO2: reader.getZString(mIconFemale); break; + case ESM4::SUB_MODT: + case ESM4::SUB_MO2B: + case ESM4::SUB_MO3B: + case ESM4::SUB_MO4B: + case ESM4::SUB_MO2T: + case ESM4::SUB_MO3T: + case ESM4::SUB_MO4T: + { + //std::cout << "CLOT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::CLOT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + //if ((mClothingFlags&0xffff) == 0x02) // only hair + //std::cout << "only hair " << mEditorId << std::endl; +} + +//void ESM4::Clothing::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Clothing::blank() +//{ +//} diff --git a/components/esm4/loadclot.hpp b/components/esm4/loadclot.hpp new file mode 100644 index 0000000000..16c8a145a0 --- /dev/null +++ b/components/esm4/loadclot.hpp @@ -0,0 +1,80 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CLOT_H +#define ESM4_CLOT_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Clothing + { +#pragma pack(push, 1) + struct Data + { + std::uint32_t value; // gold + float weight; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModelMale; + std::string mModelMaleWorld; + std::string mModelFemale; + std::string mModelFemaleWorld; + std::string mIconMale; // texture + std::string mIconFemale; // texture + + std::string mModel; // FIXME: for OpenCS + + float mBoundRadius; + + std::uint32_t mClothingFlags; // see Armor::ArmorFlags for the values + FormId mScriptId; + std::uint16_t mEnchantmentPoints; + FormId mEnchantment; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_CLOT_H diff --git a/components/esm4/loadcont.cpp b/components/esm4/loadcont.cpp new file mode 100644 index 0000000000..f0c7309967 --- /dev/null +++ b/components/esm4/loadcont.cpp @@ -0,0 +1,95 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadcont.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Container::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + reader.get(mDataFlags); + reader.get(mWeight); + break; + } + case ESM4::SUB_CNTO: + { + static InventoryItem inv; // FIXME: use unique_ptr here? + reader.get(inv); + reader.adjustFormId(inv.item); + mInventory.push_back(inv); + break; + } + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SNAM: reader.getFormId(mOpenSound); break; + case ESM4::SUB_QNAM: reader.getFormId(mCloseSound); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_MODS: // TES5 only + case ESM4::SUB_VMAD: // TES5 only + case ESM4::SUB_OBND: // TES5 only + case ESM4::SUB_COCT: // TES5 only + case ESM4::SUB_COED: // TES5 only + case ESM4::SUB_DEST: // FONV + case ESM4::SUB_DSTD: // FONV + case ESM4::SUB_DSTF: // FONV + case ESM4::SUB_DMDL: // FONV + case ESM4::SUB_DMDT: // FONV + case ESM4::SUB_RNAM: // FONV + { + //std::cout << "CONT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::CONT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Container::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Container::blank() +//{ +//} diff --git a/components/esm4/loadcont.hpp b/components/esm4/loadcont.hpp new file mode 100644 index 0000000000..e7b47e8944 --- /dev/null +++ b/components/esm4/loadcont.hpp @@ -0,0 +1,68 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CONT_H +#define ESM4_CONT_H + +#include +#include +#include + +#include "formid.hpp" +#include "inventory.hpp" // InventoryItem + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Container + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + float mBoundRadius; + unsigned char mDataFlags; + float mWeight; + + FormId mOpenSound; + FormId mCloseSound; + FormId mScriptId; // TES4 only + + std::vector mInventory; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_CONT_H diff --git a/components/esm4/loadcrea.cpp b/components/esm4/loadcrea.cpp new file mode 100644 index 0000000000..3da8a0c114 --- /dev/null +++ b/components/esm4/loadcrea.cpp @@ -0,0 +1,206 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadcrea.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include +#include +#include +#include +#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Creature::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_CNTO: + { + static InventoryItem inv; // FIXME: use unique_ptr here? + reader.get(inv); + reader.adjustFormId(inv.item); + mInventory.push_back(inv); + break; + } + case ESM4::SUB_SPLO: + { + FormId id; + reader.getFormId(id); + mSpell.push_back(id); + break; + } + case ESM4::SUB_PKID: + { + FormId id; + reader.getFormId(id); + mAIPackages.push_back(id); + break; + } + case ESM4::SUB_SNAM: + { + reader.get(mFaction); + reader.adjustFormId(mFaction.faction); + break; + } + case ESM4::SUB_INAM: reader.getFormId(mDeathItem); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_AIDT: + { + if (subHdr.dataSize == 20) // FO3 + reader.skipSubRecordData(); + else + reader.get(mAIData); // 12 bytes + break; + } + case ESM4::SUB_ACBS: + { + //if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || mIsFONV) + if (subHdr.dataSize == 24) + reader.get(mBaseConfig); + else + reader.get(&mBaseConfig, 16); // TES4 + break; + } + case ESM4::SUB_DATA: + { + if (subHdr.dataSize == 17) // FO3 + reader.skipSubRecordData(); + else + reader.get(mData); + break; + } + case ESM4::SUB_ZNAM: reader.getFormId(mCombatStyle); break; + case ESM4::SUB_CSCR: reader.getFormId(mSoundBase); break; + case ESM4::SUB_CSDI: reader.getFormId(mSound); break; + case ESM4::SUB_CSDC: reader.get(mSoundChance); break; + case ESM4::SUB_BNAM: reader.get(mBaseScale); break; + case ESM4::SUB_TNAM: reader.get(mTurningSpeed); break; + case ESM4::SUB_WNAM: reader.get(mFootWeight); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_NAM0: reader.getZString(mBloodSpray); break; + case ESM4::SUB_NAM1: reader.getZString(mBloodDecal); break; + case ESM4::SUB_NIFZ: + { + std::string str; + if (!reader.getZString(str)) + throw std::runtime_error ("CREA NIFZ data read error"); + + std::stringstream ss(str); + std::string file; + while (std::getline(ss, file, '\0')) // split the strings + mNif.push_back(file); + + break; + } + case ESM4::SUB_NIFT: + { + if (subHdr.dataSize != 4) // FIXME: FO3 + { + reader.skipSubRecordData(); + break; + } + + assert(subHdr.dataSize == 4 && "CREA NIFT datasize error"); + std::uint32_t nift; + reader.get(nift); + if (nift) + std::cout << "CREA NIFT " << mFormId << ", non-zero " << nift << std::endl; + break; + } + case ESM4::SUB_KFFZ: + { + std::string str; + if (!reader.getZString(str)) + throw std::runtime_error ("CREA KFFZ data read error"); + + std::stringstream ss(str); + std::string file; + while (std::getline(ss, file, '\0')) // split the strings + mKf.push_back(file); + + break; + } + case ESM4::SUB_TPLT: reader.get(mBaseTemplate); break; // FO3 + case ESM4::SUB_PNAM: // FO3/FONV/TES5 + { + FormId bodyPart; + reader.get(bodyPart); + mBodyParts.push_back(bodyPart); + + break; + } + case ESM4::SUB_MODT: + case ESM4::SUB_RNAM: + case ESM4::SUB_CSDT: + case ESM4::SUB_OBND: // FO3 + case ESM4::SUB_EAMT: // FO3 + case ESM4::SUB_VTCK: // FO3 + case ESM4::SUB_NAM4: // FO3 + case ESM4::SUB_NAM5: // FO3 + case ESM4::SUB_CNAM: // FO3 + case ESM4::SUB_LNAM: // FO3 + case ESM4::SUB_EITM: // FO3 + case ESM4::SUB_DEST: // FO3 + case ESM4::SUB_DSTD: // FO3 + case ESM4::SUB_DSTF: // FO3 + case ESM4::SUB_DMDL: // FO3 + case ESM4::SUB_DMDT: // FO3 + case ESM4::SUB_COED: // FO3 + { + //std::cout << "CREA " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::CREA::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Creature::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Creature::blank() +//{ +//} diff --git a/components/esm4/loadcrea.hpp b/components/esm4/loadcrea.hpp new file mode 100644 index 0000000000..05e6840312 --- /dev/null +++ b/components/esm4/loadcrea.hpp @@ -0,0 +1,148 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_CREA_H +#define ESM4_CREA_H + +#include +#include +#include + +#include "actor.hpp" +#include "inventory.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Creature + { + enum ACBS_TES4 + { + TES4_Essential = 0x000002, + TES4_WeapAndShield = 0x000004, + TES4_Respawn = 0x000008, + TES4_PCLevelOffset = 0x000080, + TES4_NoLowLevelProc = 0x000200, + TES4_NoHead = 0x008000, // different meaning to npc_ + TES4_NoRightArm = 0x010000, + TES4_NoLeftArm = 0x020000, + TES4_NoCombatWater = 0x040000, + TES4_NoShadow = 0x080000, + TES4_NoCorpseCheck = 0x100000 // opposite of npc_ + }; + + enum ACBS_FO3 + { + FO3_Biped = 0x00000001, + FO3_Essential = 0x00000002, + FO3_Weap_Shield = 0x00000004, + FO3_Respawn = 0x00000008, + FO3_CanSwim = 0x00000010, + FO3_CanFly = 0x00000020, + FO3_CanWalk = 0x00000040, + FO3_PCLevelMult = 0x00000080, + FO3_NoLowLevelProc = 0x00000200, + FO3_NoBloodSpray = 0x00000800, + FO3_NoBloodDecal = 0x00001000, + FO3_NoHead = 0x00008000, + FO3_NoRightArm = 0x00010000, + FO3_NoLeftArm = 0x00020000, + FO3_NoWaterCombat = 0x00040000, + FO3_NoShadow = 0x00080000, + FO3_NoVATSMelee = 0x00100000, + FO3_AllowPCDialog = 0x00200000, + FO3_NoOpenDoors = 0x00400000, + FO3_Immobile = 0x00800000, + FO3_TiltFrontBack = 0x01000000, + FO3_TiltLeftRight = 0x02000000, + FO3_NoKnockdown = 0x04000000, + FO3_NotPushable = 0x08000000, + FO3_AllowPickpoket = 0x10000000, + FO3_IsGhost = 0x20000000, + FO3_NoRotateHead = 0x40000000, + FO3_Invulnerable = 0x80000000 + }; + +#pragma pack(push, 1) + struct Data + { + std::uint8_t unknown; + std::uint8_t combat; + std::uint8_t magic; + std::uint8_t stealth; + std::uint16_t soul; + std::uint16_t health; + std::uint16_t unknown2; + std::uint16_t damage; + AttributeValues attribs; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + FormId mDeathItem; + std::vector mSpell; + FormId mScriptId; + + AIData mAIData; + std::vector mAIPackages; + ActorBaseConfig mBaseConfig; + ActorFaction mFaction; + Data mData; + FormId mCombatStyle; + FormId mSoundBase; + FormId mSound; + std::uint8_t mSoundChance; + float mBaseScale; + float mTurningSpeed; + float mFootWeight; + std::string mBloodSpray; + std::string mBloodDecal; + + float mBoundRadius; + std::vector mNif; // NIF filenames, get directory from mModel + std::vector mKf; + + std::vector mInventory; + + FormId mBaseTemplate; // FO3/FONV + std::vector mBodyParts; // FO3/FONV + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_CREA_H diff --git a/components/esm4/loaddial.cpp b/components/esm4/loaddial.cpp new file mode 100644 index 0000000000..342828a9d6 --- /dev/null +++ b/components/esm4/loaddial.cpp @@ -0,0 +1,112 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loaddial.hpp" + +#include +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Dialogue::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mTopicName); break; + case ESM4::SUB_QSTI: + { + FormId questId; + reader.getFormId(questId); + mQuests.push_back(questId); + + break; + } + case ESM4::SUB_QSTR: // Seems never used in TES4 + { + FormId questRem; + reader.getFormId(questRem); + mQuestsRemoved.push_back(questRem); + + break; + } + case ESM4::SUB_DATA: + { + if (subHdr.dataSize == 4) // TES5 + { + std::uint8_t dummy; + reader.get(dummy); + if (dummy != 0) + mDoAllBeforeRepeat = true; + } + + reader.get(mDialType); // TES4/FO3/FONV/TES5 + + if (subHdr.dataSize >= 2) // FO3/FONV/TES5 + reader.get(mDialFlags); + + if (subHdr.dataSize >= 3) // TES5 + reader.skipSubRecordData(1); // unknown + + break; + } + case ESM4::SUB_PNAM: reader.get(mPriority); break; // FO3/FONV + case ESM4::SUB_TDUM: reader.getZString(mTextDumb); break; // FONV + case ESM4::SUB_SCRI: + case ESM4::SUB_INFC: // FONV info connection + case ESM4::SUB_INFX: // FONV info index + case ESM4::SUB_QNAM: // TES5 + case ESM4::SUB_BNAM: // TES5 + case ESM4::SUB_SNAM: // TES5 + case ESM4::SUB_TIFC: // TES5 + { + //std::cout << "DIAL " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::DIAL::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Dialogue::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Dialogue::blank() +//{ +//} diff --git a/components/esm4/loaddial.hpp b/components/esm4/loaddial.hpp new file mode 100644 index 0000000000..8097c94baf --- /dev/null +++ b/components/esm4/loaddial.hpp @@ -0,0 +1,67 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_DIAL_H +#define ESM4_DIAL_H + +#include +#include +#include + +#include "formid.hpp" +#include "dialogue.hpp" // DialType + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Dialogue + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::vector mQuests; + std::vector mQuestsRemoved; // FONV only? + std::string mTopicName; + + std::string mTextDumb; // FIXME: temp name + + bool mDoAllBeforeRepeat; // TES5 only + std::uint8_t mDialType; // DialType + std::uint8_t mDialFlags; // FO3/FONV: 0x1 rumours, 0x2 top-level + + float mPriority; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_DIAL_H diff --git a/components/esm4/loaddobj.cpp b/components/esm4/loaddobj.cpp new file mode 100644 index 0000000000..6d791ff044 --- /dev/null +++ b/components/esm4/loaddobj.cpp @@ -0,0 +1,117 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loaddobj.hpp" + +#include +#include +//#include // FIXME: for debugging only + +//#include "formid.hpp" + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::DefaultObj::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; // "DefaultObjectManager" + case ESM4::SUB_DATA: + { + reader.getFormId(mData.stimpack); + reader.getFormId(mData.superStimpack); + reader.getFormId(mData.radX); + reader.getFormId(mData.radAway); + reader.getFormId(mData.morphine); + reader.getFormId(mData.perkParalysis); + reader.getFormId(mData.playerFaction); + reader.getFormId(mData.mysteriousStrangerNPC); + reader.getFormId(mData.mysteriousStrangerFaction); + reader.getFormId(mData.defaultMusic); + reader.getFormId(mData.battleMusic); + reader.getFormId(mData.deathMusic); + reader.getFormId(mData.successMusic); + reader.getFormId(mData.levelUpMusic); + reader.getFormId(mData.playerVoiceMale); + reader.getFormId(mData.playerVoiceMaleChild); + reader.getFormId(mData.playerVoiceFemale); + reader.getFormId(mData.playerVoiceFemaleChild); + reader.getFormId(mData.eatPackageDefaultFood); + reader.getFormId(mData.everyActorAbility); + reader.getFormId(mData.drugWearsOffImageSpace); + // below FONV only + if (subHdr.dataSize == 136) // FONV 136/4 = 34 formid + { + reader.getFormId(mData.doctorsBag); + reader.getFormId(mData.missFortuneNPC); + reader.getFormId(mData.missFortuneFaction); + reader.getFormId(mData.meltdownExplosion); + reader.getFormId(mData.unarmedForwardPA); + reader.getFormId(mData.unarmedBackwardPA); + reader.getFormId(mData.unarmedLeftPA); + reader.getFormId(mData.unarmedRightPA); + reader.getFormId(mData.unarmedCrouchPA); + reader.getFormId(mData.unarmedCounterPA); + reader.getFormId(mData.spotterEffect); + reader.getFormId(mData.itemDetectedEfect); + reader.getFormId(mData.cateyeMobileEffectNYI); + } + + break; + } + case ESM4::SUB_DNAM: + { + //std::cout << "DOBJ " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + //std::cout << "DOBJ " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + throw std::runtime_error("ESM4::DOBJ::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::DefaultObj::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::DefaultObj::blank() +//{ +//} diff --git a/components/esm4/loaddobj.hpp b/components/esm4/loaddobj.hpp new file mode 100644 index 0000000000..c959a62e50 --- /dev/null +++ b/components/esm4/loaddobj.hpp @@ -0,0 +1,97 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_DOBJ_H +#define ESM4_DOBJ_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Defaults + { + FormId stimpack; + FormId superStimpack; + FormId radX; + FormId radAway; + FormId morphine; + FormId perkParalysis; + FormId playerFaction; + FormId mysteriousStrangerNPC; + FormId mysteriousStrangerFaction; + FormId defaultMusic; + FormId battleMusic; + FormId deathMusic; + FormId successMusic; + FormId levelUpMusic; + FormId playerVoiceMale; + FormId playerVoiceMaleChild; + FormId playerVoiceFemale; + FormId playerVoiceFemaleChild; + FormId eatPackageDefaultFood; + FormId everyActorAbility; + FormId drugWearsOffImageSpace; + // below FONV only + FormId doctorsBag; + FormId missFortuneNPC; + FormId missFortuneFaction; + FormId meltdownExplosion; + FormId unarmedForwardPA; + FormId unarmedBackwardPA; + FormId unarmedLeftPA; + FormId unarmedRightPA; + FormId unarmedCrouchPA; + FormId unarmedCounterPA; + FormId spotterEffect; + FormId itemDetectedEfect; + FormId cateyeMobileEffectNYI; + }; + + struct DefaultObj + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + Defaults mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_DOBJ_H diff --git a/components/esm4/loaddoor.cpp b/components/esm4/loaddoor.cpp new file mode 100644 index 0000000000..555c7a7d3f --- /dev/null +++ b/components/esm4/loaddoor.cpp @@ -0,0 +1,81 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loaddoor.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Door::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_SNAM: reader.getFormId(mOpenSound); break; + case ESM4::SUB_ANAM: reader.getFormId(mCloseSound); break; + case ESM4::SUB_BNAM: reader.getFormId(mLoopSound); break; + case ESM4::SUB_FNAM: reader.get(mDoorFlags); break; + case ESM4::SUB_TNAM: reader.getFormId(mRandomTeleport); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_MODS: + case ESM4::SUB_OBND: + case ESM4::SUB_VMAD: + case ESM4::SUB_DEST: // FO3 + case ESM4::SUB_DSTD: // FO3 + case ESM4::SUB_DSTF: // FO3 + case ESM4::SUB_DMDL: // FO3 + case ESM4::SUB_DMDT: // FO3 + { + //std::cout << "DOOR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::DOOR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Door::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Door::blank() +//{ +//} diff --git a/components/esm4/loaddoor.hpp b/components/esm4/loaddoor.hpp new file mode 100644 index 0000000000..9810a45989 --- /dev/null +++ b/components/esm4/loaddoor.hpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_DOOR_H +#define ESM4_DOOR_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Door + { + enum Flags + { + Flag_OblivionGate = 0x01, + Flag_AutomaticDoor = 0x02, + Flag_Hidden = 0x04, + Flag_MinimalUse = 0x08 + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + float mBoundRadius; + + std::uint8_t mDoorFlags; + FormId mScriptId; + FormId mOpenSound; // SNDR for TES5, SOUN for others + FormId mCloseSound; // SNDR for TES5, SOUN for others + FormId mLoopSound; + FormId mRandomTeleport; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_DOOR_H diff --git a/components/esm4/loadeyes.cpp b/components/esm4/loadeyes.cpp new file mode 100644 index 0000000000..f91de07926 --- /dev/null +++ b/components/esm4/loadeyes.cpp @@ -0,0 +1,61 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadeyes.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Eyes::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_DATA: reader.get(mData); break; + default: + throw std::runtime_error("ESM4::EYES::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Eyes::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Eyes::blank() +//{ +//} diff --git a/components/esm4/loadeyes.hpp b/components/esm4/loadeyes.hpp new file mode 100644 index 0000000000..5f0bb0f31b --- /dev/null +++ b/components/esm4/loadeyes.hpp @@ -0,0 +1,65 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_EYES_H +#define ESM4_EYES_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Eyes + { +#pragma pack(push, 1) + struct Data + { + std::uint8_t flags; // 0x01 = playable? + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mIcon; // texture + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_EYES_H diff --git a/components/esm4/loadflor.cpp b/components/esm4/loadflor.cpp new file mode 100644 index 0000000000..ae9770ab74 --- /dev/null +++ b/components/esm4/loadflor.cpp @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadflor.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Flora::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_PFIG: reader.getFormId(mIngredient); break; + case ESM4::SUB_PFPC: reader.get(mPercentHarvest); break; + case ESM4::SUB_SNAM: reader.getFormId(mSound); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_MODS: + case ESM4::SUB_FNAM: + case ESM4::SUB_OBND: + case ESM4::SUB_PNAM: + case ESM4::SUB_RNAM: + case ESM4::SUB_VMAD: + { + //std::cout << "FLOR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::FLOR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Flora::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Flora::blank() +//{ +//} diff --git a/components/esm4/loadflor.hpp b/components/esm4/loadflor.hpp new file mode 100644 index 0000000000..d454108233 --- /dev/null +++ b/components/esm4/loadflor.hpp @@ -0,0 +1,75 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_FLOR_H +#define ESM4_FLOR_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Flora + { +#pragma pack(push, 1) + struct Production + { + std::uint8_t spring; + std::uint8_t summer; + std::uint8_t autumn; + std::uint8_t winter; + + Production() : spring(0), summer(0), autumn(0), winter(0) {} + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + float mBoundRadius; + + FormId mScriptId; + FormId mIngredient; + FormId mSound; + Production mPercentHarvest; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_FLOR_H diff --git a/components/esm4/loadflst.cpp b/components/esm4/loadflst.cpp new file mode 100644 index 0000000000..32de2fe813 --- /dev/null +++ b/components/esm4/loadflst.cpp @@ -0,0 +1,72 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadflst.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::FormIdList::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_LNAM: + { + FormId formId; + reader.getFormId(formId); + + mObjects.push_back(formId); + + break; + } + default: + //std::cout << "FLST " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + throw std::runtime_error("ESM4::FLST::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + //std::cout << "flst " << mEditorId << " " << mObjects.size() << std::endl; // FIXME +} + +//void ESM4::FormIdList::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::FormIdList::blank() +//{ +//} diff --git a/components/esm4/loadflst.hpp b/components/esm4/loadflst.hpp new file mode 100644 index 0000000000..5a3d048fd4 --- /dev/null +++ b/components/esm4/loadflst.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_FLST_H +#define ESM4_FLST_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct FormIdList + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::vector mObjects; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_FLST_H diff --git a/components/esm4/loadfurn.cpp b/components/esm4/loadfurn.cpp new file mode 100644 index 0000000000..0b27f50194 --- /dev/null +++ b/components/esm4/loadfurn.cpp @@ -0,0 +1,86 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadfurn.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Furniture::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MNAM: reader.get(mActiveMarkerFlags); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_DEST: + case ESM4::SUB_DSTD: + case ESM4::SUB_DSTF: + case ESM4::SUB_ENAM: + case ESM4::SUB_FNAM: + case ESM4::SUB_FNMK: + case ESM4::SUB_FNPR: + case ESM4::SUB_KNAM: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_MODS: + case ESM4::SUB_NAM0: + case ESM4::SUB_OBND: + case ESM4::SUB_PNAM: + case ESM4::SUB_VMAD: + case ESM4::SUB_WBDT: + case ESM4::SUB_XMRK: + { + //std::cout << "FURN " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::FURN::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Furniture::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Furniture::blank() +//{ +//} diff --git a/components/esm4/loadfurn.hpp b/components/esm4/loadfurn.hpp new file mode 100644 index 0000000000..051d2b03d6 --- /dev/null +++ b/components/esm4/loadfurn.hpp @@ -0,0 +1,61 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_FURN_H +#define ESM4_FURN_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Furniture + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + float mBoundRadius; + + FormId mScriptId; + std::uint32_t mActiveMarkerFlags; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_FURN_H diff --git a/components/esm4/loadglob.cpp b/components/esm4/loadglob.cpp new file mode 100644 index 0000000000..739aab5b82 --- /dev/null +++ b/components/esm4/loadglob.cpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadglob.hpp" + +#include +#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::GlobalVariable::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FNAM: reader.get(mType); break; + case ESM4::SUB_FLTV: reader.get(mValue); break; + case ESM4::SUB_FULL: + case ESM4::SUB_MODL: + case ESM4::SUB_MODB: + case ESM4::SUB_ICON: + case ESM4::SUB_DATA: + case ESM4::SUB_OBND: // TES5 + case ESM4::SUB_VMAD: // TES5 + { + //std::cout << "GLOB " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::GLOB::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::GlobalVariable::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::GlobalVariable::blank() +//{ +//} diff --git a/components/esm4/loadglob.hpp b/components/esm4/loadglob.hpp new file mode 100644 index 0000000000..abe3a8ac93 --- /dev/null +++ b/components/esm4/loadglob.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_GLOB_H +#define ESM4_GLOB_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct GlobalVariable + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::uint8_t mType; + float mValue; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_GLOB_H diff --git a/components/esm4/loadgras.cpp b/components/esm4/loadgras.cpp new file mode 100644 index 0000000000..8c4699b25b --- /dev/null +++ b/components/esm4/loadgras.cpp @@ -0,0 +1,68 @@ +/* + Copyright (C) 2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadgras.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Grass::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_OBND: + { + //std::cout << "GRAS " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::GRAS::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Grass::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Grass::blank() +//{ +//} diff --git a/components/esm4/loadgras.hpp b/components/esm4/loadgras.hpp new file mode 100644 index 0000000000..e5b9cb18ff --- /dev/null +++ b/components/esm4/loadgras.hpp @@ -0,0 +1,95 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_GRAS_H +#define ESM4_GRAS_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Grass + { +#pragma pack(push, 1) + // unused fields are probably packing + struct Data + { + std::uint8_t density; + std::uint8_t minSlope; + std::uint8_t maxSlope; + std::uint8_t unused; + std::uint16_t distanceFromWater; + std::uint16_t unused2; + /* + 1 Above - At Least + 2 Above - At Most + 3 Below - At Least + 4 Below - At Most + 5 Either - At Least + 6 Either - At Most + 7 Either - At Most Above + 8 Either - At Most Below + */ + std::uint32_t waterDistApplication; + float positionRange; + float heightRange; + float colorRange; + float wavePeriod; + /* + 0x01 Vertex Lighting + 0x02 Uniform Scaling + 0x04 Fit to Slope + */ + std::uint8_t flags; + std::uint8_t unused3; + std::uint16_t unused4; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + float mBoundRadius; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_GRAS_H diff --git a/components/esm4/loadgrup.hpp b/components/esm4/loadgrup.hpp new file mode 100644 index 0000000000..4b9aa9547b --- /dev/null +++ b/components/esm4/loadgrup.hpp @@ -0,0 +1,154 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_GRUP_H +#define ESM4_GRUP_H + +#include +#include + +#include "common.hpp" // GroupLabel + +namespace ESM4 +{ + // http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format#Hierarchical_Top_Groups + // + // Type | Info | + // ------+--------------------------------------+------------------- + // 2 | Interior Cell Block | + // 3 | Interior Cell Sub-Block | + // R | CELL | + // 6 | Cell Childen | + // 8 | Persistent children | + // R | REFR, ACHR, ACRE | + // 10 | Visible distant children | + // R | REFR, ACHR, ACRE | + // 9 | Temp Children | + // R | PGRD | + // R | REFR, ACHR, ACRE | + // | | + // 0 | Top (Type) | + // R | WRLD | + // 1 | World Children | + // R | ROAD | + // R | CELL | + // 6 | Cell Childen | + // 8 | Persistent children | + // R | REFR, ACHR, ACRE | + // 10 | Visible distant children | + // R | REFR, ACHR, ACRE | + // 9 | Temp Children | + // R | PGRD | + // R | REFR, ACHR, ACRE | + // 4 | Exterior World Block | + // 5 | Exterior World Sub-block | + // R | CELL | + // 6 | Cell Childen | + // 8 | Persistent children | + // R | REFR, ACHR, ACRE | + // 10 | Visible distant children | + // R | REFR, ACHR, ACRE | + // 9 | Temp Children | + // R | LAND | + // R | PGRD | + // R | REFR, ACHR, ACRE | + // + struct WorldGroup + { + FormId mWorld; // WRLD record for this group + + // occurs only after World Child (type 1) + // since GRUP label may not be reliable, need to keep the formid of the current WRLD in + // the reader's context + FormId mRoad; + + std::vector mCells; // FIXME should this be CellGroup* instead? + }; + + // http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format/CELL + // + // The block and subblock groups for an interior cell are determined by the last two decimal + // digits of the lower 3 bytes of the cell form ID (the modindex is not included in the + // calculation). For example, for form ID 0x000CF2=3314, the block is 4 and the subblock is 1. + // + // The block and subblock groups for an exterior cell are determined by the X-Y coordinates of + // the cell. Each block contains 16 subblocks (4x4) and each subblock contains 64 cells (8x8). + // So each block contains 1024 cells (32x32). + // + // NOTE: There may be many CELL records in one subblock + struct CellGroup + { + FormId mCell; // CELL record for this cell group + int mCellModIndex; // from which file to get the CELL record (e.g. may have been updated) + + // For retrieving parent group size (for lazy loading or skipping) and sub-block number / grid + // NOTE: There can be more than one file that adds/modifies records to this cell group + // + // Use Case 1: To quickly get only the visble when distant records: + // + // - Find the FormId of the CELL (maybe WRLD/X/Y grid lookup or from XTEL of a REFR) + // - search a map of CELL FormId to CellGroup + // - load CELL and its child groups (or load the visible distant only, or whatever) + // + // Use Case 2: Scan the files but don't load CELL or cell group + // + // - Load referenceables and other records up front, updating them as required + // - Don't load CELL, LAND, PGRD or ROAD (keep FormId's and file index, and file + // context then skip the rest of the group) + // + std::vector mHeaders; // FIXME: is this needed? + + // FIXME: should these be pairs? i.e. so that we know from which file + // the formid came (it may have been updated by a mod) + // but does it matter? the record itself keeps track of whether it is base, + // added or modified anyway + // FIXME: should these be maps? e.g. std::map + // or vector for storage with a corresponding map of index? + + // cache (modindex adjusted) formId's of children + // FIXME: also need file index + file context of all those that has type 8 GRUP + GroupTypeHeader mHdrPersist; + std::vector mPersistent; // REFR, ACHR, ACRE + std::vector mdelPersistent; + + // FIXME: also need file index + file context of all those that has type 10 GRUP + GroupTypeHeader mHdrVisDist; + std::vector mVisibleDist; // REFR, ACHR, ACRE + std::vector mdelVisibleDist; + + // FIXME: also need file index + file context of all those that has type 9 GRUP + GroupTypeHeader mHdrTemp; + FormId mLand; // if present, assume only one LAND per exterior CELL + FormId mPgrd; // if present, seems to be the first record after LAND in Temp Cell Child GRUP + std::vector mTemporary; // REFR, ACHR, ACRE + std::vector mdelTemporary; + + // need to keep modindex and context for lazy loading (of all the files that contribute + // to this group) + }; +} + +#endif // ESM4_GRUP_H diff --git a/components/esm4/loadhair.cpp b/components/esm4/loadhair.cpp new file mode 100644 index 0000000000..64d0b2192e --- /dev/null +++ b/components/esm4/loadhair.cpp @@ -0,0 +1,70 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadhair.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Hair::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + { + //std::cout << "HAIR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::HAIR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Hair::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Hair::blank() +//{ +//} diff --git a/components/esm4/loadhair.hpp b/components/esm4/loadhair.hpp new file mode 100644 index 0000000000..b0b07f9281 --- /dev/null +++ b/components/esm4/loadhair.hpp @@ -0,0 +1,68 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_HAIR +#define ESM4_HAIR + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Hair + { +#pragma pack(push, 1) + struct Data + { + std::uint8_t flags; // 0x01 = not playable, 0x02 = not male, 0x04 = not female, ?? = fixed + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; // mesh + std::string mIcon; // texture + + float mBoundRadius; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_HAIR diff --git a/components/esm4/loadhdpt.cpp b/components/esm4/loadhdpt.cpp new file mode 100644 index 0000000000..ee1d3dd526 --- /dev/null +++ b/components/esm4/loadhdpt.cpp @@ -0,0 +1,99 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadhdpt.hpp" + +#include +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::HeadPart::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + std::optional type; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_HNAM: reader.getFormId(mAdditionalPart); break; + case ESM4::SUB_NAM0: // TES5 + { + std::uint32_t value; + reader.get(value); + type = value; + + break; + } + case ESM4::SUB_NAM1: // TES5 + { + std::string file; + reader.getZString(file); + + if (!type.has_value()) + throw std::runtime_error("Failed to read ESM4 HDPT record: subrecord NAM0 does not precede subrecord NAM1: file type is unknown"); + + if (*type >= mTriFile.size()) + throw std::runtime_error("Failed to read ESM4 HDPT record: invalid file type: " + std::to_string(*type)); + + mTriFile[*type] = std::move(file); + + break; + } + case ESM4::SUB_TNAM: reader.getFormId(mBaseTexture); break; + case ESM4::SUB_PNAM: + case ESM4::SUB_MODS: + case ESM4::SUB_MODT: + case ESM4::SUB_RNAM: + { + //std::cout << "HDPT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::HDPT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::HeadPart::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::HeadPart::blank() +//{ +//} diff --git a/components/esm4/loadhdpt.hpp b/components/esm4/loadhdpt.hpp new file mode 100644 index 0000000000..89a486eba8 --- /dev/null +++ b/components/esm4/loadhdpt.hpp @@ -0,0 +1,63 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_HDPT_H +#define ESM4_HDPT_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct HeadPart + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + + std::uint8_t mData; + + FormId mAdditionalPart; + + std::array mTriFile; + FormId mBaseTexture; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_HDPT_H diff --git a/components/esm4/loadidle.cpp b/components/esm4/loadidle.cpp new file mode 100644 index 0000000000..df84e3df4c --- /dev/null +++ b/components/esm4/loadidle.cpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadidle.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::IdleAnimation::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_DNAM: reader.getZString(mCollision); break; + case ESM4::SUB_ENAM: reader.getZString(mEvent); break; + case ESM4::SUB_ANAM: + { + reader.get(mParent); + reader.get(mPrevious); + break; + } + case ESM4::SUB_CTDA: // formId + case ESM4::SUB_DATA: // formId + { + //std::cout << "IDLE " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::IDLE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::IdleAnimation::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::IdleAnimation::blank() +//{ +//} diff --git a/components/esm4/loadidle.hpp b/components/esm4/loadidle.hpp new file mode 100644 index 0000000000..ad4e796359 --- /dev/null +++ b/components/esm4/loadidle.hpp @@ -0,0 +1,59 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_IDLE_H +#define ESM4_IDLE_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct IdleAnimation + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mCollision; + std::string mEvent; + + FormId mParent; // IDLE or AACT + FormId mPrevious; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_IDLE_H diff --git a/components/esm4/loadidlm.cpp b/components/esm4/loadidlm.cpp new file mode 100644 index 0000000000..c94cd37310 --- /dev/null +++ b/components/esm4/loadidlm.cpp @@ -0,0 +1,94 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadidlm.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::IdleMarker::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + std::uint32_t esmVer = reader.esmVersion(); + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_IDLF: reader.get(mIdleFlags); break; + case ESM4::SUB_IDLC: + { + if (subHdr.dataSize != 1) // FO3 can have 4? + { + reader.skipSubRecordData(); + break; + } + + reader.get(mIdleCount); + break; + } + case ESM4::SUB_IDLT: reader.get(mIdleTimer); break; + case ESM4::SUB_IDLA: + { + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + if (esmVer == ESM::VER_094 || isFONV) // FO3? 4 or 8 bytes + { + reader.skipSubRecordData(); + break; + } + + mIdleAnim.resize(mIdleCount); + for (unsigned int i = 0; i < static_cast(mIdleCount); ++i) + reader.get(mIdleAnim.at(i)); + break; + } + case ESM4::SUB_OBND: // object bounds + { + //std::cout << "IDLM " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::IDLM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::IdleMarker::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::IdleMarker::blank() +//{ +//} diff --git a/components/esm4/loadidlm.hpp b/components/esm4/loadidlm.hpp new file mode 100644 index 0000000000..2874426217 --- /dev/null +++ b/components/esm4/loadidlm.hpp @@ -0,0 +1,60 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_IDLM_H +#define ESM4_IDLM_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct IdleMarker + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + std::uint8_t mIdleFlags; + std::uint8_t mIdleCount; + float mIdleTimer; + std::vector mIdleAnim; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_IDLM_H diff --git a/components/esm4/loadimod.cpp b/components/esm4/loadimod.cpp new file mode 100644 index 0000000000..4ec6518a01 --- /dev/null +++ b/components/esm4/loadimod.cpp @@ -0,0 +1,80 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadimod.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::ItemMod::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_OBND: + case ESM4::SUB_FULL: + case ESM4::SUB_MODL: + case ESM4::SUB_ICON: + case ESM4::SUB_MICO: + case ESM4::SUB_SCRI: + case ESM4::SUB_DESC: + case ESM4::SUB_YNAM: + case ESM4::SUB_ZNAM: + case ESM4::SUB_DATA: + { + //std::cout << "IMOD " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + std::cout << "IMOD " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + //throw std::runtime_error("ESM4::IMOD::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::ItemMod::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::ItemMod::blank() +//{ +//} diff --git a/components/esm4/loadimod.hpp b/components/esm4/loadimod.hpp new file mode 100644 index 0000000000..5e6078384e --- /dev/null +++ b/components/esm4/loadimod.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_IMOD_H +#define ESM4_IMOD_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct ItemMod + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_IMOD_H diff --git a/components/esm4/loadinfo.cpp b/components/esm4/loadinfo.cpp new file mode 100644 index 0000000000..a0a99b13dc --- /dev/null +++ b/components/esm4/loadinfo.cpp @@ -0,0 +1,203 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadinfo.hpp" + +#include +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::DialogInfo::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + mEditorId = formIdToString(mFormId); // FIXME: quick workaround to use existing code + + static ScriptLocalVariableData localVar; + bool ignore = false; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_QSTI: reader.getFormId(mQuest); break; // FormId quest id + case ESM4::SUB_SNDD: reader.getFormId(mSound); break; // FO3 (not used in FONV?) + case ESM4::SUB_TRDT: + { + if (subHdr.dataSize == 16) // TES4 + reader.get(&mResponseData, 16); + else if (subHdr.dataSize == 20) // FO3 + reader.get(&mResponseData, 20); + else // FO3/FONV + { + reader.get(mResponseData); + if (mResponseData.sound) + reader.adjustFormId(mResponseData.sound); + } + + break; + } + case ESM4::SUB_NAM1: reader.getZString(mResponse); break; // response text + case ESM4::SUB_NAM2: reader.getZString(mNotes); break; // actor notes + case ESM4::SUB_NAM3: reader.getZString(mEdits); break; // not in TES4 + case ESM4::SUB_CTDA: // FIXME: how to detect if 1st/2nd param is a formid? + { + if (subHdr.dataSize == 24) // TES4 + reader.get(&mTargetCondition, 24); + else if (subHdr.dataSize == 20) // FO3 + reader.get(&mTargetCondition, 20); + else if (subHdr.dataSize == 28) + { + reader.get(mTargetCondition); // FO3/FONV + if (mTargetCondition.reference) + reader.adjustFormId(mTargetCondition.reference); + } + else // TES5 + { + reader.get(&mTargetCondition, 20); + if (subHdr.dataSize == 36) + reader.getFormId(mParam3); + reader.get(mTargetCondition.runOn); + reader.get(mTargetCondition.reference); + if (mTargetCondition.reference) + reader.adjustFormId(mTargetCondition.reference); + reader.skipSubRecordData(4); // unknown + } + + break; + } + case ESM4::SUB_SCHR: + { + if (!ignore) + reader.get(mScript.scriptHeader); + else + reader.skipSubRecordData(); // TODO: does the second one ever used? + + break; + } + case ESM4::SUB_SCDA: reader.skipSubRecordData(); break; // compiled script data + case ESM4::SUB_SCTX: reader.getString(mScript.scriptSource); break; + case ESM4::SUB_SCRO: reader.getFormId(mScript.globReference); break; + case ESM4::SUB_SLSD: + { + localVar.clear(); + reader.get(localVar.index); + reader.get(localVar.unknown1); + reader.get(localVar.unknown2); + reader.get(localVar.unknown3); + reader.get(localVar.type); + reader.get(localVar.unknown4); + // WARN: assumes SCVR will follow immediately + + break; + } + case ESM4::SUB_SCVR: // assumed always pair with SLSD + { + reader.getZString(localVar.variableName); + + mScript.localVarData.push_back(localVar); + + break; + } + case ESM4::SUB_SCRV: + { + std::uint32_t index; + reader.get(index); + + mScript.localRefVarIndex.push_back(index); + + break; + } + case ESM4::SUB_NEXT: // FO3/FONV marker for next script header + { + ignore = true; + + break; + } + case ESM4::SUB_DATA: // always 3 for TES4 ? + { + if (subHdr.dataSize == 4) // FO3/FONV + { + reader.get(mDialType); + reader.get(mNextSpeaker); + reader.get(mInfoFlags); + } + else + reader.skipSubRecordData(); // FIXME + break; + } + case ESM4::SUB_NAME: // FormId add topic (not always present) + case ESM4::SUB_CTDT: // older version of CTDA? 20 bytes + case ESM4::SUB_SCHD: // 28 bytes + case ESM4::SUB_TCLT: // FormId choice + case ESM4::SUB_TCLF: // FormId + case ESM4::SUB_PNAM: // TES4 DLC + case ESM4::SUB_TPIC: // TES4 DLC + case ESM4::SUB_ANAM: // FO3 speaker formid + case ESM4::SUB_DNAM: // FO3 speech challenge + case ESM4::SUB_KNAM: // FO3 formid + case ESM4::SUB_LNAM: // FONV + case ESM4::SUB_TCFU: // FONV + case ESM4::SUB_TIFC: // TES5 + case ESM4::SUB_TWAT: // TES5 + case ESM4::SUB_CIS2: // TES5 + case ESM4::SUB_CNAM: // TES5 + case ESM4::SUB_ENAM: // TES5 + case ESM4::SUB_EDID: // TES5 + case ESM4::SUB_VMAD: // TES5 + case ESM4::SUB_BNAM: // TES5 + case ESM4::SUB_SNAM: // TES5 + case ESM4::SUB_ONAM: // TES5 + case ESM4::SUB_QNAM: // TES5 for mScript + case ESM4::SUB_RNAM: // TES5 + { + //std::cout << "INFO " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + std::cout << "INFO " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + //throw std::runtime_error("ESM4::INFO::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::DialogInfo::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::DialogInfo::blank() +//{ +//} diff --git a/components/esm4/loadinfo.hpp b/components/esm4/loadinfo.hpp new file mode 100644 index 0000000000..2decff05e0 --- /dev/null +++ b/components/esm4/loadinfo.hpp @@ -0,0 +1,87 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_INFO_H +#define ESM4_INFO_H + +#include +#include + +#include "formid.hpp" +#include "script.hpp" // TargetCondition +#include "dialogue.hpp" // DialType + +namespace ESM4 +{ + class Reader; + class Writer; + + enum InfoFlag + { + INFO_Goodbye = 0x0001, + INFO_Random = 0x0002, + INFO_SayOnce = 0x0004, + INFO_RunImmediately = 0x0008, + INFO_InfoRefusal = 0x0010, + INFO_RandomEnd = 0x0020, + INFO_RunForRumors = 0x0040, + INFO_SpeechChallenge = 0x0080, + INFO_SayOnceADay = 0x0100, + INFO_AlwaysDarken = 0x0200 + }; + + struct DialogInfo + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; // FIXME: no such record for INFO, but keep here to avoid extra work for now + + FormId mQuest; + FormId mSound; // unused? + + TargetResponseData mResponseData; + std::string mResponse; + std::string mNotes; + std::string mEdits; + + std::uint8_t mDialType; // DialType + std::uint8_t mNextSpeaker; + std::uint16_t mInfoFlags; // see above enum + + TargetCondition mTargetCondition; + FormId mParam3; // TES5 only + + ScriptDefinition mScript; // FIXME: ignoring the second one after the NEXT sub-record + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_INFO_H diff --git a/components/esm4/loadingr.cpp b/components/esm4/loadingr.cpp new file mode 100644 index 0000000000..9001329662 --- /dev/null +++ b/components/esm4/loadingr.cpp @@ -0,0 +1,114 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadingr.hpp" + +#include +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Ingredient::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: + { + if (mFullName.empty()) + { + reader.getLocalizedString(mFullName); break; + } + else // in TES4 subsequent FULL records are script effect names + { + // FIXME: should be part of a struct? + std::string scriptEffectName; + if (!reader.getZString(scriptEffectName)) + throw std::runtime_error ("INGR FULL data read error"); + + mScriptEffect.push_back(scriptEffectName); + + break; + } + } + case ESM4::SUB_DATA: + { + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 8) // FO3 is size 4 even though VER_094 + reader.get(mData); + else + reader.get(mData.weight); + + break; + } + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_ENIT: reader.get(mEnchantment); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_SCIT: + { + reader.get(mEffect); + reader.adjustFormId(mEffect.formId); + break; + } + case ESM4::SUB_MODT: + case ESM4::SUB_MODS: // Dragonborn only? + case ESM4::SUB_EFID: + case ESM4::SUB_EFIT: + case ESM4::SUB_OBND: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_VMAD: + case ESM4::SUB_YNAM: + case ESM4::SUB_ZNAM: + case ESM4::SUB_ETYP: // FO3 + { + //std::cout << "INGR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::INGR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Ingredient::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Ingredient::blank() +//{ +//} diff --git a/components/esm4/loadingr.hpp b/components/esm4/loadingr.hpp new file mode 100644 index 0000000000..08b3e73efc --- /dev/null +++ b/components/esm4/loadingr.hpp @@ -0,0 +1,80 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_INGR_H +#define ESM4_INGR_H + +#include +#include + +#include "effect.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Ingredient + { +#pragma pack(push, 1) + struct Data + { + std::uint32_t value; + float weight; + }; + + struct ENIT + { + std::uint32_t value; + std::uint32_t flags; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + + float mBoundRadius; + + std::vector mScriptEffect; // FIXME: prob. should be in a struct + FormId mScriptId; + ScriptEffect mEffect; + ENIT mEnchantment; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_INGR_H diff --git a/components/esm4/loadkeym.cpp b/components/esm4/loadkeym.cpp new file mode 100644 index 0000000000..f0c5dbedb7 --- /dev/null +++ b/components/esm4/loadkeym.cpp @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadkeym.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Key::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIcon); break; // FO3 + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_OBND: + case ESM4::SUB_VMAD: + { + //std::cout << "KEYM " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::KEYM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Key::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Key::blank() +//{ +//} diff --git a/components/esm4/loadkeym.hpp b/components/esm4/loadkeym.hpp new file mode 100644 index 0000000000..68835eeeb6 --- /dev/null +++ b/components/esm4/loadkeym.hpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_KEYM_H +#define ESM4_KEYM_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Key + { +#pragma pack(push, 1) + struct Data + { + std::uint32_t value; // gold + float weight; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + std::string mMiniIcon; // inventory + + FormId mPickUpSound; + FormId mDropSound; + + float mBoundRadius; + FormId mScriptId; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_KEYM_H diff --git a/components/esm4/loadland.cpp b/components/esm4/loadland.cpp new file mode 100644 index 0000000000..40e9d95c58 --- /dev/null +++ b/components/esm4/loadland.cpp @@ -0,0 +1,240 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadland.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include + +#include // FIXME: debug only + +#include "reader.hpp" +//#include "writer.hpp" + +// overlap north +// +// 32 +// 31 +// 30 +// overlap . +// west . +// . +// 2 +// 1 +// 0 +// 0 1 2 ... 30 31 32 +// +// overlap south +// +void ESM4::Land::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + mDataTypes = 0; + + TxtLayer layer; + std::int8_t currentAddQuad = -1; // for VTXT following ATXT + + //std::map uniqueTextures; // FIXME: for temp testing only + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_DATA: + { + reader.get(mLandFlags); + break; + } + case ESM4::SUB_VNML: // vertex normals, 33x33x(1+1+1) = 3267 + { + reader.get(mVertNorm); + mDataTypes |= LAND_VNML; + break; + } + case ESM4::SUB_VHGT: // vertex height gradient, 4+33x33+3 = 4+1089+3 = 1096 + { +#if 0 + reader.get(mHeightMap.heightOffset); + reader.get(mHeightMap.gradientData); + reader.get(mHeightMap.unknown1); + reader.get(mHeightMap.unknown2); +#endif + reader.get(mHeightMap); + mDataTypes |= LAND_VHGT; + break; + } + case ESM4::SUB_VCLR: // vertex colours, 24bit RGB, 33x33x(1+1+1) = 3267 + { + reader.get(mVertColr); + mDataTypes |= LAND_VCLR; + break; + } + case ESM4::SUA_BTXT: + { + BTXT base; + if (reader.getExact(base)) + { + assert(base.quadrant < 4 && "base texture quadrant index error"); + + reader.adjustFormId(base.formId); + mTextures[base.quadrant].base = std::move(base); +#if 0 + std::cout << "Base Texture formid: 0x" + << std::hex << mTextures[base.quadrant].base.formId + << ", quad " << std::dec << (int)base.quadrant << std::endl; +#endif + } + break; + } + case ESM4::SUB_ATXT: + { + if (currentAddQuad != -1) + { + // FIXME: sometimes there are no VTXT following an ATXT? Just add a dummy one for now + std::cout << "ESM4::Land VTXT empty layer " << (int)layer.texture.layerIndex << std::endl; + mTextures[currentAddQuad].layers.push_back(layer); + } + reader.get(layer.texture); + reader.adjustFormId(layer.texture.formId); + assert(layer.texture.quadrant < 4 && "additional texture quadrant index error"); +#if 0 + FormId txt = layer.texture.formId; + std::map::iterator lb = uniqueTextures.lower_bound(txt); + if (lb != uniqueTextures.end() && !(uniqueTextures.key_comp()(txt, lb->first))) + { + lb->second += 1; + } + else + uniqueTextures.insert(lb, std::make_pair(txt, 1)); +#endif +#if 0 + std::cout << "Additional Texture formId: 0x" + << std::hex << layer.texture.formId + << ", quad " << std::dec << (int)layer.texture.quadrant << std::endl; + std::cout << "Additional Texture layer: " + << std::dec << (int)layer.texture.layerIndex << std::endl; +#endif + currentAddQuad = layer.texture.quadrant; + break; + } + case ESM4::SUB_VTXT: + { + assert(currentAddQuad != -1 && "VTXT without ATXT found"); + + int count = (int)reader.subRecordHeader().dataSize / sizeof(ESM4::Land::VTXT); + assert((reader.subRecordHeader().dataSize % sizeof(ESM4::Land::VTXT)) == 0 + && "ESM4::LAND VTXT data size error"); + + if (count) + { + layer.data.resize(count); + std::vector::iterator it = layer.data.begin(); + for (;it != layer.data.end(); ++it) + { + reader.get(*it); + // FIXME: debug only + //std::cout << "pos: " << std::dec << (int)(*it).position << std::endl; + } + } + mTextures[currentAddQuad].layers.push_back(layer); + + // Assumed that the layers are added in the correct sequence + // FIXME: Knights.esp doesn't seem to observe this - investigate more + //assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()-1 + //&& "additional texture layer index error"); + + currentAddQuad = -1; + layer.data.clear(); + // FIXME: debug only + //std::cout << "VTXT: count " << std::dec << count << std::endl; + break; + } + case ESM4::SUB_VTEX: // only in Oblivion? + { + int count = (int)reader.subRecordHeader().dataSize / sizeof(FormId); + assert((reader.subRecordHeader().dataSize % sizeof(FormId)) == 0 + && "ESM4::LAND VTEX data size error"); + + if (count) + { + mIds.resize(count); + for (std::vector::iterator it = mIds.begin(); it != mIds.end(); ++it) + { + reader.getFormId(*it); + // FIXME: debug only + //std::cout << "VTEX: " << std::hex << *it << std::endl; + } + } + break; + } + default: + throw std::runtime_error("ESM4::LAND::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + + if (currentAddQuad != -1) + { + // FIXME: not sure if it happens here as well + std::cout << "ESM4::Land VTXT empty layer " << (int)layer.texture.layerIndex << " quad " << (int)layer.texture.quadrant << std::endl; + mTextures[currentAddQuad].layers.push_back(layer); + } + + bool missing = false; + for (int i = 0; i < 4; ++i) + { + if (mTextures[i].base.formId == 0) + { + //std::cout << "ESM4::LAND " << ESM4::formIdToString(mFormId) << " missing base, quad " << i << std::endl; + //std::cout << "layers " << mTextures[i].layers.size() << std::endl; + // NOTE: can't set the default here since FO3/FONV may have different defaults + //mTextures[i].base.formId = 0x000008C0; // TerrainHDDirt01.dds + missing = true; + } + //else + //{ + // std::cout << "ESM4::LAND " << ESM4::formIdToString(mFormId) << " base, quad " << i << std::endl; + // std::cout << "layers " << mTextures[i].layers.size() << std::endl; + //} + } + // at least one of the quadrants do not have a base texture, return without setting the flag + if (!missing) + mDataTypes |= LAND_VTEX; +} + +//void ESM4::Land::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Land::blank() +//{ +//} diff --git a/components/esm4/loadland.hpp b/components/esm4/loadland.hpp new file mode 100644 index 0000000000..305b48c157 --- /dev/null +++ b/components/esm4/loadland.hpp @@ -0,0 +1,133 @@ +/* + Copyright (C) 2015-2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LAND_H +#define ESM4_LAND_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + + struct Land + { + enum + { + LAND_VNML = 1, + LAND_VHGT = 2, + LAND_WNAM = 4, // only in TES3? + LAND_VCLR = 8, + LAND_VTEX = 16 + }; + + // number of vertices per side + static const int VERTS_PER_SIDE = 33; + + // cell terrain size in world coords + static const int REAL_SIZE = 4096; + + // total number of vertices + static const int LAND_NUM_VERTS = VERTS_PER_SIDE * VERTS_PER_SIDE; + + static const int HEIGHT_SCALE = 8; + + // number of textures per side of a land quadrant + // (for TES4 - based on vanilla observations) + static const int QUAD_TEXTURE_PER_SIDE = 6; + +#pragma pack(push,1) + struct VHGT + { + float heightOffset; + std::int8_t gradientData[VERTS_PER_SIDE * VERTS_PER_SIDE]; + std::uint16_t unknown1; + unsigned char unknown2; + }; + + struct BTXT + { + FormId formId; + std::uint8_t quadrant; // 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right + std::uint8_t unknown1; + std::uint16_t unknown2; + }; + + struct ATXT + { + FormId formId; + std::uint8_t quadrant; // 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right + std::uint8_t unknown; + std::uint16_t layerIndex; // texture layer, 0..7 + }; + + struct VTXT + { + std::uint16_t position; // 0..288 (17x17 grid) + std::uint8_t unknown1; + std::uint8_t unknown2; + float opacity; + }; +#pragma pack(pop) + + struct TxtLayer + { + ATXT texture; + std::vector data; // alpha data + }; + + struct Texture + { + BTXT base; + std::vector layers; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::uint32_t mLandFlags; // from DATA subrecord + + // FIXME: lazy loading not yet implemented + int mDataTypes; // which data types are loaded + + signed char mVertNorm[VERTS_PER_SIDE * VERTS_PER_SIDE * 3]; // from VNML subrecord + signed char mVertColr[VERTS_PER_SIDE * VERTS_PER_SIDE * 3]; // from VCLR subrecord + VHGT mHeightMap; + Texture mTextures[4]; // 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right + std::vector mIds; // land texture (LTEX) formids + + virtual void load(Reader& reader); + //virtual void save(Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LAND_H diff --git a/components/esm4/loadlgtm.cpp b/components/esm4/loadlgtm.cpp new file mode 100644 index 0000000000..d0aa3a5b49 --- /dev/null +++ b/components/esm4/loadlgtm.cpp @@ -0,0 +1,85 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadlgtm.hpp" + +#include +#include // FLT_MAX for gcc +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::LightingTemplate::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_DATA: + { + if (subHdr.dataSize == 36) // TES4 + reader.get(&mLighting, 36); + if (subHdr.dataSize == 40) // FO3/FONV + reader.get(mLighting); + else if (subHdr.dataSize == 92) // TES5 + { + reader.get(mLighting); + reader.skipSubRecordData(52); // FIXME + } + else + reader.skipSubRecordData(); // throw? + + break; + } + case ESM4::SUB_DALC: // TES5 + { + //std::cout << "LGTM " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::LGTM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::LightingTemplate::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::LightingTemplate::blank() +//{ +//} diff --git a/components/esm4/loadlgtm.hpp b/components/esm4/loadlgtm.hpp new file mode 100644 index 0000000000..1b625e458f --- /dev/null +++ b/components/esm4/loadlgtm.hpp @@ -0,0 +1,60 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_LGTM_H +#define ESM4_LGTM_H + +#include +#include + +#include "formid.hpp" +#include "lighting.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + typedef std::uint32_t FormId; + + struct LightingTemplate + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + Lighting mLighting; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LGTM_H diff --git a/components/esm4/loadligh.cpp b/components/esm4/loadligh.cpp new file mode 100644 index 0000000000..394d10114d --- /dev/null +++ b/components/esm4/loadligh.cpp @@ -0,0 +1,107 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadligh.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Light::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + // FIXME: TES4 might be uint32 as well, need to check + if (isFONV || (esmVer == ESM::VER_094 && subHdr.dataSize == 32)/*FO3*/) + { + reader.get(mData.time); // uint32 + } + else + reader.get(mData.duration); // float + + reader.get(mData.radius); + reader.get(mData.colour); + reader.get(mData.flags); + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 48) + { + reader.get(mData.falloff); + reader.get(mData.FOV); + reader.get(mData.nearClip); + reader.get(mData.frequency); + reader.get(mData.intensityAmplitude); + reader.get(mData.movementAmplitude); + } + else if (subHdr.dataSize == 32) // TES4 + { + reader.get(mData.falloff); + reader.get(mData.FOV); + } + reader.get(mData.value); + reader.get(mData.weight); + break; + } + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_SNAM: reader.getFormId(mSound); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_FNAM: reader.get(mFade); break; + case ESM4::SUB_MODT: + case ESM4::SUB_OBND: + case ESM4::SUB_VMAD: // Dragonborn only? + { + //std::cout << "LIGH " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::LIGH::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Light::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Light::blank() +//{ +//} diff --git a/components/esm4/loadligh.hpp b/components/esm4/loadligh.hpp new file mode 100644 index 0000000000..974385dbf8 --- /dev/null +++ b/components/esm4/loadligh.hpp @@ -0,0 +1,94 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LIGH_H +#define ESM4_LIGH_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Light + { + struct Data + { + std::uint32_t time; // FO/FONV only + float duration = -1; + std::uint32_t radius; + std::uint32_t colour; // RGBA + // flags: + // 0x00000001 = Dynamic + // 0x00000002 = Can be Carried + // 0x00000004 = Negative + // 0x00000008 = Flicker + // 0x00000020 = Off By Default + // 0x00000040 = Flicker Slow + // 0x00000080 = Pulse + // 0x00000100 = Pulse Slow + // 0x00000200 = Spot Light + // 0x00000400 = Spot Shadow + std::int32_t flags; + float falloff = 1.f; + float FOV = 90; // FIXME: FOV in degrees or radians? + float nearClip; // TES5 only + float frequency; // TES5 only + float intensityAmplitude; // TES5 only + float movementAmplitude; // TES5 only + std::uint32_t value; // gold + float weight; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; + + float mBoundRadius; + + FormId mScriptId; + FormId mSound; + + float mFade; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LIGH_H diff --git a/components/esm4/loadltex.cpp b/components/esm4/loadltex.cpp new file mode 100644 index 0000000000..6edcd4ae05 --- /dev/null +++ b/components/esm4/loadltex.cpp @@ -0,0 +1,95 @@ +/* + Copyright (C) 2015-2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadltex.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include +//#include // FIXME: debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::LandTexture::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_HNAM: + { + if (isFONV) + { + reader.skipSubRecordData(); // FIXME: skip FONV for now + break; + } + + if ((reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + && subHdr.dataSize == 2) // FO3 is VER_094 but dataSize 3 + { + //assert(subHdr.dataSize == 2 && "LTEX unexpected HNAM size"); + reader.get(mHavokFriction); + reader.get(mHavokRestitution); + } + else + { + assert(subHdr.dataSize == 3 && "LTEX unexpected HNAM size"); + reader.get(mHavokMaterial); + reader.get(mHavokFriction); + reader.get(mHavokRestitution); + } + break; + } + case ESM4::SUB_ICON: reader.getZString(mTextureFile); break; // Oblivion only? + case ESM4::SUB_SNAM: reader.get(mTextureSpecular); break; + case ESM4::SUB_GNAM: reader.getFormId(mGrass); break; + case ESM4::SUB_TNAM: reader.getFormId(mTexture); break; // TES5 only + case ESM4::SUB_MNAM: reader.getFormId(mMaterial); break; // TES5 only + default: + throw std::runtime_error("ESM4::LTEX::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::LandTexture::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::LandTexture::blank() +//{ +//} diff --git a/components/esm4/loadltex.hpp b/components/esm4/loadltex.hpp new file mode 100644 index 0000000000..e445f54525 --- /dev/null +++ b/components/esm4/loadltex.hpp @@ -0,0 +1,72 @@ +/* + Copyright (C) 2015-2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LTEX_H +#define ESM4_LTEX_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct LandTexture + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::uint8_t mHavokFriction; + std::uint8_t mHavokRestitution; + + std::uint8_t mTextureSpecular; // default 30 + FormId mGrass; + + // ------ TES4 only ----- + + std::string mTextureFile; + std::uint8_t mHavokMaterial; + + // ------ TES5 only ----- + + FormId mTexture; + FormId mMaterial; + + // ---------------------- + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LTEX_H diff --git a/components/esm4/loadlvlc.cpp b/components/esm4/loadlvlc.cpp new file mode 100644 index 0000000000..a3067a0561 --- /dev/null +++ b/components/esm4/loadlvlc.cpp @@ -0,0 +1,120 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadlvlc.hpp" + +#include +//#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::LevelledCreature::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_TNAM: reader.getFormId(mTemplate); break; + case ESM4::SUB_LVLD: reader.get(mChanceNone); break; + case ESM4::SUB_LVLF: reader.get(mLvlCreaFlags); break; + case ESM4::SUB_LVLO: + { + static LVLO lvlo; + if (subHdr.dataSize != 12) + { + if (subHdr.dataSize == 8) + { + reader.get(lvlo.level); + reader.get(lvlo.item); + reader.get(lvlo.count); + //std::cout << "LVLC " << mEditorId << " LVLO lev " << lvlo.level << ", item " << lvlo.item + //<< ", count " << lvlo.count << std::endl; + // FIXME: seems to happen only once, don't add to mLvlObject + // LVLC TesKvatchCreature LVLO lev 1, item 1393819648, count 2 + // 0x0001, 0x5314 0000, 0x0002 + break; + } + else + throw std::runtime_error("ESM4::LVLC::load - " + mEditorId + " LVLO size error"); + } + else + reader.get(lvlo); + + reader.adjustFormId(lvlo.item); + mLvlObject.push_back(lvlo); + break; + } + case ESM4::SUB_OBND: // FO3 + { + //std::cout << "LVLC " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::LVLC::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +bool ESM4::LevelledCreature::calcAllLvlLessThanPlayer() const +{ + if (mHasLvlCreaFlags) + return (mLvlCreaFlags & 0x01) != 0; + else + return (mChanceNone & 0x80) != 0; // FIXME: 0x80 is just a guess +} + +bool ESM4::LevelledCreature::calcEachItemInCount() const +{ + if (mHasLvlCreaFlags) + return (mLvlCreaFlags & 0x02) != 0; + else + return true; // FIXME: just a guess +} + +std::int8_t ESM4::LevelledCreature::chanceNone() const +{ + if (mHasLvlCreaFlags) + return mChanceNone; + else + return (mChanceNone & 0x7f); // FIXME: 0x80 is just a guess +} + +//void ESM4::LevelledCreature::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::LevelledCreature::blank() +//{ +//} diff --git a/components/esm4/loadlvlc.hpp b/components/esm4/loadlvlc.hpp new file mode 100644 index 0000000000..44aadbe36e --- /dev/null +++ b/components/esm4/loadlvlc.hpp @@ -0,0 +1,68 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LVLC_H +#define ESM4_LVLC_H + +#include +#include + +#include "formid.hpp" +#include "inventory.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct LevelledCreature + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + FormId mScriptId; + FormId mTemplate; + std::int8_t mChanceNone; + + bool mHasLvlCreaFlags; + std::uint8_t mLvlCreaFlags; + + std::vector mLvlObject; + + bool calcAllLvlLessThanPlayer() const; + bool calcEachItemInCount() const; + std::int8_t chanceNone() const; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LVLC_H diff --git a/components/esm4/loadlvli.cpp b/components/esm4/loadlvli.cpp new file mode 100644 index 0000000000..68e458e73c --- /dev/null +++ b/components/esm4/loadlvli.cpp @@ -0,0 +1,132 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadlvli.hpp" + +#include +#include // FIXME: for debugging + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::LevelledItem::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_LVLD: reader.get(mChanceNone); break; + case ESM4::SUB_LVLF: reader.get(mLvlItemFlags); mHasLvlItemFlags = true; break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_LVLO: + { + static LVLO lvlo; + if (subHdr.dataSize != 12) + { + if (subHdr.dataSize == 8) + { + reader.get(lvlo.level); + reader.get(lvlo.item); + reader.get(lvlo.count); +// std::cout << "LVLI " << mEditorId << " LVLO lev " << lvlo.level << ", item " << lvlo.item +// << ", count " << lvlo.count << std::endl; + break; + } + else + throw std::runtime_error("ESM4::LVLI::load - " + mEditorId + " LVLO size error"); + } + else + reader.get(lvlo); + + reader.adjustFormId(lvlo.item); + mLvlObject.push_back(lvlo); + break; + } + case ESM4::SUB_LLCT: + case ESM4::SUB_OBND: // FO3/FONV + case ESM4::SUB_COED: // FO3/FONV + case ESM4::SUB_LVLG: // FO3/FONV + { + + //std::cout << "LVLI " << ESM::printName(subHdr.typeId) << " skipping..." << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::LVLI::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + + // FIXME: testing + //if (mHasLvlItemFlags && mChanceNone >= 90) + //std::cout << "LVLI " << mEditorId << " chance none " << int(mChanceNone) << std::endl; +} + +bool ESM4::LevelledItem::calcAllLvlLessThanPlayer() const +{ + if (mHasLvlItemFlags) + return (mLvlItemFlags & 0x01) != 0; + else + return (mChanceNone & 0x80) != 0; // FIXME: 0x80 is just a guess +} + +bool ESM4::LevelledItem::calcEachItemInCount() const +{ + if (mHasLvlItemFlags) + return (mLvlItemFlags & 0x02) != 0; + else + return mData != 0; +} + +std::int8_t ESM4::LevelledItem::chanceNone() const +{ + if (mHasLvlItemFlags) + return mChanceNone; + else + return (mChanceNone & 0x7f); // FIXME: 0x80 is just a guess +} + +bool ESM4::LevelledItem::useAll() const +{ + if (mHasLvlItemFlags) + return (mLvlItemFlags & 0x04) != 0; + else + return false; +} + +//void ESM4::LevelledItem::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::LevelledItem::blank() +//{ +//} diff --git a/components/esm4/loadlvli.hpp b/components/esm4/loadlvli.hpp new file mode 100644 index 0000000000..290658e86d --- /dev/null +++ b/components/esm4/loadlvli.hpp @@ -0,0 +1,69 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LVLI_H +#define ESM4_LVLI_H + +#include +#include + +#include "formid.hpp" +#include "inventory.hpp" // LVLO + +namespace ESM4 +{ + class Reader; + class Writer; + + struct LevelledItem + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::int8_t mChanceNone; + + bool mHasLvlItemFlags; + std::uint8_t mLvlItemFlags; + + std::uint8_t mData; + + std::vector mLvlObject; + + bool calcAllLvlLessThanPlayer() const; + bool calcEachItemInCount() const; + bool useAll() const; + std::int8_t chanceNone() const; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LVLI_H diff --git a/components/esm4/loadlvln.cpp b/components/esm4/loadlvln.cpp new file mode 100644 index 0000000000..bd1423d0ac --- /dev/null +++ b/components/esm4/loadlvln.cpp @@ -0,0 +1,104 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadlvln.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::LevelledNpc::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + //std::uint32_t esmVer = reader.esmVersion(); // currently unused + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_LLCT: reader.get(mListCount); break; + case ESM4::SUB_LVLD: reader.get(mChanceNone); break; + case ESM4::SUB_LVLF: reader.get(mLvlActorFlags); break; + case ESM4::SUB_LVLO: + { + static LVLO lvlo; + if (subHdr.dataSize != 12) + { + if (subHdr.dataSize == 8) + { + reader.get(lvlo.level); + reader.get(lvlo.item); + reader.get(lvlo.count); + break; + } + else + throw std::runtime_error("ESM4::LVLN::load - " + mEditorId + " LVLO size error"); + } +// else if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) +// { +// std::uint32_t level; +// reader.get(level); +// lvlo.level = static_cast(level); +// reader.get(lvlo.item); +// std::uint32_t count; +// reader.get(count); +// lvlo.count = static_cast(count); +// } + else + reader.get(lvlo); + + reader.adjustFormId(lvlo.item); + mLvlObject.push_back(lvlo); + break; + } + case ESM4::SUB_COED: // owner + case ESM4::SUB_OBND: // object bounds + case ESM4::SUB_MODT: // model texture data + { + //std::cout << "LVLN " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::LVLN::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::LevelledNpc::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::LevelledNpc::blank() +//{ +//} diff --git a/components/esm4/loadlvln.hpp b/components/esm4/loadlvln.hpp new file mode 100644 index 0000000000..49a7bb943b --- /dev/null +++ b/components/esm4/loadlvln.hpp @@ -0,0 +1,66 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_LVLN_H +#define ESM4_LVLN_H + +#include +#include + +#include "formid.hpp" +#include "inventory.hpp" // LVLO + +namespace ESM4 +{ + class Reader; + class Writer; + + struct LevelledNpc + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + std::int8_t mChanceNone; + std::uint8_t mLvlActorFlags; + + std::uint8_t mListCount; + std::vector mLvlObject; + + inline bool calcAllLvlLessThanPlayer() const { return (mLvlActorFlags & 0x01) != 0; } + inline bool calcEachItemInCount() const { return (mLvlActorFlags & 0x02) != 0; } + inline std::int8_t chanceNone() const { return mChanceNone; } + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_LVLN_H diff --git a/components/esm4/loadmato.cpp b/components/esm4/loadmato.cpp new file mode 100644 index 0000000000..7e3ab43026 --- /dev/null +++ b/components/esm4/loadmato.cpp @@ -0,0 +1,66 @@ +/* + Copyright (C) 2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadmato.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Material::load(ESM4::Reader& reader) +{ + //mFormId = reader.adjustFormId(reader.hdr().record.id); // FIXME: use master adjusted? + mFormId = reader.hdr().record.id; + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_DNAM: + case ESM4::SUB_DATA: + { + //std::cout << "MATO " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::MATO::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Material::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Material::blank() +//{ +//} diff --git a/components/esm4/loadmato.hpp b/components/esm4/loadmato.hpp new file mode 100644 index 0000000000..041b158c29 --- /dev/null +++ b/components/esm4/loadmato.hpp @@ -0,0 +1,55 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_MATO_H +#define ESM4_MATO_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Material + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_MATO_H diff --git a/components/esm4/loadmisc.cpp b/components/esm4/loadmisc.cpp new file mode 100644 index 0000000000..966910f775 --- /dev/null +++ b/components/esm4/loadmisc.cpp @@ -0,0 +1,79 @@ +/* + Copyright (C) 2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadmisc.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::MiscItem::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIcon); break; // FO3 + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_MODS: + case ESM4::SUB_OBND: + case ESM4::SUB_VMAD: + case ESM4::SUB_RNAM: // FONV + { + //std::cout << "MISC " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::MISC::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::MiscItem::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::MiscItem::blank() +//{ +//} diff --git a/components/esm4/loadmisc.hpp b/components/esm4/loadmisc.hpp new file mode 100644 index 0000000000..49297330ea --- /dev/null +++ b/components/esm4/loadmisc.hpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_MISC_H +#define ESM4_MISC_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct MiscItem + { +#pragma pack(push, 1) + struct Data + { + std::uint32_t value; // gold + float weight; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + std::string mMiniIcon; // inventory + + FormId mPickUpSound; + FormId mDropSound; + + float mBoundRadius; + FormId mScriptId; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_MISC_H diff --git a/components/esm4/loadmset.cpp b/components/esm4/loadmset.cpp new file mode 100644 index 0000000000..55a2619239 --- /dev/null +++ b/components/esm4/loadmset.cpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadmset.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::MediaSet::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mFullName); break; + case ESM4::SUB_NAM1: reader.get(mSetType); break; + case ESM4::SUB_PNAM: reader.get(mEnabled); break; + case ESM4::SUB_NAM2: reader.getZString(mSet2); break; + case ESM4::SUB_NAM3: reader.getZString(mSet3); break; + case ESM4::SUB_NAM4: reader.getZString(mSet4); break; + case ESM4::SUB_NAM5: reader.getZString(mSet5); break; + case ESM4::SUB_NAM6: reader.getZString(mSet6); break; + case ESM4::SUB_NAM7: reader.getZString(mSet7); break; + case ESM4::SUB_HNAM: reader.getFormId(mSoundIntro); break; + case ESM4::SUB_INAM: reader.getFormId(mSoundOutro); break; + case ESM4::SUB_NAM8: reader.get(mLevel8); break; + case ESM4::SUB_NAM9: reader.get(mLevel9); break; + case ESM4::SUB_NAM0: reader.get(mLevel0); break; + case ESM4::SUB_ANAM: reader.get(mLevelA); break; + case ESM4::SUB_BNAM: reader.get(mLevelB); break; + case ESM4::SUB_CNAM: reader.get(mLevelC); break; + case ESM4::SUB_JNAM: reader.get(mBoundaryDayOuter); break; + case ESM4::SUB_KNAM: reader.get(mBoundaryDayMiddle); break; + case ESM4::SUB_LNAM: reader.get(mBoundaryDayInner); break; + case ESM4::SUB_MNAM: reader.get(mBoundaryNightOuter); break; + case ESM4::SUB_NNAM: reader.get(mBoundaryNightMiddle); break; + case ESM4::SUB_ONAM: reader.get(mBoundaryNightInner); break; + case ESM4::SUB_DNAM: reader.get(mTime1); break; + case ESM4::SUB_ENAM: reader.get(mTime2); break; + case ESM4::SUB_FNAM: reader.get(mTime3); break; + case ESM4::SUB_GNAM: reader.get(mTime4); break; + case ESM4::SUB_DATA: + { + //std::cout << "MSET " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::MSET::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::MediaSet::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::MediaSet::blank() +//{ +//} diff --git a/components/esm4/loadmset.hpp b/components/esm4/loadmset.hpp new file mode 100644 index 0000000000..7fa9450236 --- /dev/null +++ b/components/esm4/loadmset.hpp @@ -0,0 +1,96 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_MSET_H +#define ESM4_MSET_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct MediaSet + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + + // -1 none, 0 battle, 1 location, 2 dungeon, 3 incidental + // Battle - intro (HNAM), loop (NAM2), outro (INAM) + // Location - day outer (NAM2), day middle (NAM3), day inner (NAM4), + // night outer (NAM5), night middle (NAM6), night inner (NAM7) + // Dungeon - intro (HNAM), battle (NAM2), explore (NAM3), suspence (NAM4), outro (INAM) + // Incidental - daytime (HNAM), nighttime (INAM) + std::int32_t mSetType = -1; + // 0x01 day outer, 0x02 day middle, 0x04 day inner + // 0x08 night outer, 0x10 night middle, 0x20 night inner + std::uint8_t mEnabled; // for location + + float mBoundaryDayOuter; // % + float mBoundaryDayMiddle; // % + float mBoundaryDayInner; // % + float mBoundaryNightOuter; // % + float mBoundaryNightMiddle; // % + float mBoundaryNightInner; // % + + // start at 2 to reduce confusion + std::string mSet2; // NAM2 + std::string mSet3; // NAM3 + std::string mSet4; // NAM4 + std::string mSet5; // NAM5 + std::string mSet6; // NAM6 + std::string mSet7; // NAM7 + + float mLevel8; // dB + float mLevel9; // dB + float mLevel0; // dB + float mLevelA; // dB + float mLevelB; // dB + float mLevelC; // dB + + float mTime1; + float mTime2; + float mTime3; + float mTime4; + + FormId mSoundIntro; // HNAM + FormId mSoundOutro; // INAM + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_MSET_H diff --git a/components/esm4/loadmstt.cpp b/components/esm4/loadmstt.cpp new file mode 100644 index 0000000000..1deb5603f8 --- /dev/null +++ b/components/esm4/loadmstt.cpp @@ -0,0 +1,77 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadmstt.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::MovableStatic::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_SNAM: reader.get(mLoopingSound); break; + case ESM4::SUB_DEST: // destruction data + case ESM4::SUB_OBND: // object bounds + case ESM4::SUB_MODT: // model texture data + case ESM4::SUB_DMDL: + case ESM4::SUB_DMDT: + case ESM4::SUB_DSTD: + case ESM4::SUB_DSTF: + case ESM4::SUB_MODS: + case ESM4::SUB_FULL: + case ESM4::SUB_MODB: + { + //std::cout << "MSTT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::MSTT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::MovableStatic::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::MovableStatic::blank() +//{ +//} diff --git a/components/esm4/loadmstt.hpp b/components/esm4/loadmstt.hpp new file mode 100644 index 0000000000..eceabda161 --- /dev/null +++ b/components/esm4/loadmstt.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_MSTT_H +#define ESM4_MSTT_H + +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct MovableStatic + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + std::int8_t mData; + FormId mLoopingSound; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_MSTT_H diff --git a/components/esm4/loadmusc.cpp b/components/esm4/loadmusc.cpp new file mode 100644 index 0000000000..566fcb2401 --- /dev/null +++ b/components/esm4/loadmusc.cpp @@ -0,0 +1,76 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadmusc.hpp" + +#include +//#include // FIXME: for debugging only + +//#include "formid.hpp" + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Music::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FNAM: reader.getZString(mMusicFile); + //std::cout << "music: " << /*formIdToString(mFormId)*/mEditorId << " " << mMusicFile << std::endl; + break; + case ESM4::SUB_ANAM: // FONV float (attenuation in db? loop if positive?) + case ESM4::SUB_WNAM: // TES5 + case ESM4::SUB_PNAM: // TES5 + case ESM4::SUB_TNAM: // TES5 + { + //std::cout << "MUSC " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::MUSC::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Music::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Music::blank() +//{ +//} diff --git a/components/esm4/loadmusc.hpp b/components/esm4/loadmusc.hpp new file mode 100644 index 0000000000..c54c9fa67c --- /dev/null +++ b/components/esm4/loadmusc.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_MUSC_H +#define ESM4_MUSC_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Music + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mMusicFile; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_MUSC_H diff --git a/components/esm4/loadnavi.cpp b/components/esm4/loadnavi.cpp new file mode 100644 index 0000000000..ae8d433f28 --- /dev/null +++ b/components/esm4/loadnavi.cpp @@ -0,0 +1,364 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadnavi.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include + +#include // FIXME: debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Navigation::IslandInfo::load(ESM4::Reader& reader) +{ + reader.get(minX); + reader.get(minY); + reader.get(minZ); + reader.get(maxX); + reader.get(maxY); + reader.get(maxZ); + + std::uint32_t count; + reader.get(count); // countTriangle; + if (count) + { + triangles.resize(count); + //std::cout << "NVMI island triangles " << std::dec << count << std::endl; // FIXME + for (std::vector::iterator it = triangles.begin(); it != triangles.end(); ++it) + { + reader.get(*it); + } + } + + reader.get(count); // countVertex; + if (count) + { + verticies.resize(count); + for (std::vector::iterator it = verticies.begin(); it != verticies.end(); ++it) + { + reader.get(*it); +// FIXME: debugging only +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "NVMI vert " << std::dec << (*it).x << ", " << (*it).y << ", " << (*it).z << std::endl; +#endif + } + } +} + +void ESM4::Navigation::NavMeshInfo::load(ESM4::Reader& reader) +{ + std::uint32_t count; + + reader.get(formId); + reader.get(flags); + reader.get(x); + reader.get(y); + reader.get(z); + +// FIXME: for debugging only +#if 0 + std::string padding; + if (flags == ESM4::FLG_Modified) + padding.insert(0, 2, '-'); + else if (flags == ESM4::FLG_Unmodified) + padding.insert(0, 4, '.'); + + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "NVMI formId: 0x" << std::hex << formId << std::endl; + std::cout << padding << "NVMI flags: " << std::hex << flags << std::endl; + std::cout << padding << "NVMI center: " << std::dec << x << ", " << y << ", " << z << std::endl; +#endif + + reader.get(flagPrefMerges); + + reader.get(count); // countMerged; + if (count) + { + //std::cout << "NVMI countMerged " << std::dec << count << std::endl; + formIdMerged.resize(count); + for (std::vector::iterator it = formIdMerged.begin(); it != formIdMerged.end(); ++it) + { + reader.get(*it); + } + } + + reader.get(count); // countPrefMerged; + if (count) + { + //std::cout << "NVMI countPrefMerged " << std::dec << count << std::endl; + formIdPrefMerged.resize(count); + for (std::vector::iterator it = formIdPrefMerged.begin(); it != formIdPrefMerged.end(); ++it) + { + reader.get(*it); + } + } + + reader.get(count); // countLinkedDoors; + if (count) + { + //std::cout << "NVMI countLinkedDoors " << std::dec << count << std::endl; + linkedDoors.resize(count); + for (std::vector::iterator it = linkedDoors.begin(); it != linkedDoors.end(); ++it) + { + reader.get(*it); + } + } + + unsigned char island; + reader.get(island); + if (island) + { + Navigation::IslandInfo island2; + island2.load(reader); + islandInfo.push_back(island2); // Maybe don't use a vector for just one entry? + } + else if (flags == FLG_Island) // FIXME: debug only + std::cerr << "nvmi no island but has 0x20 flag" << std::endl; + + reader.get(locationMarker); + + reader.get(worldSpaceId); + //FLG_Tamriel = 0x0000003c, // grid info follows, possibly Tamriel? + //FLG_Morrowind = 0x01380000, // grid info follows, probably Skywind + if (worldSpaceId == 0x0000003c || worldSpaceId == 0x01380000) + { + reader.get(cellGrid.grid.y); // NOTE: reverse order + reader.get(cellGrid.grid.x); +// FIXME: debugging only +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + if (worldSpaceId == ESM4::FLG_Morrowind) + std::cout << padding << "NVMI MW: X " << std::dec << cellGrid.grid.x << ", Y " << cellGrid.grid.y << std::endl; + else + std::cout << padding << "NVMI SR: X " << std::dec << cellGrid.grid.x << ", Y " << cellGrid.grid.y << std::endl; +#endif + } + else + { + reader.get(cellGrid.cellId); + +#if 0 + if (worldSpaceId == 0) // interior + std::cout << "NVMI Interior: cellId " << std::hex << cellGrid.cellId << std::endl; + else + std::cout << "NVMI FormID: cellId " << std::hex << cellGrid.cellId << std::endl; +#endif + } +} + +// NVPP data seems to be organised this way (total is 0x64 = 100) +// +// (0) total | 0x1 | formid (index 0) | count | formid's +// (1) | count | formid's +// (2) | count | formid's +// (3) | count | formid's +// (4) | count | formid's +// (5) | count | formid's +// (6) | count | formid's +// (7) | count | formid's +// (8) | count | formid's +// (9) | count | formid's +// (10) | 0x1 | formid (index 1) | count | formid's +// (11) | count | formid's +// (12) | count | formid's +// (13) | count | formid's +// (14) | count | formid's +// (15) | count | formid's +// ... +// +// (88) | count | formid's +// (89) | count | formid's +// +// Here the pattern changes (final count is 0xa = 10) +// +// (90) | 0x1 | formid (index 9) | count | formid | index +// (91) | formid | index +// (92) | formid | index +// (93) | formid | index +// (94) | formid | index +// (95) | formid | index +// (96) | formid | index +// (97) | formid | index +// (98) | formid | index +// (99) | formid | index +// +// Note that the index values are not sequential, i.e. the first index value +// (i.e. row 90) for Update.esm is 2. +// +// Also note that there's no list of formid's following the final node (index 9) +// +// The same 10 formids seem to be used for the indices, but not necessarily +// with the same index value (but only Update.esm differs?) +// +// formid cellid X Y Editor ID other formids in same X,Y S U D D +// -------- ------ --- --- --------------------------- ---------------------------- - - - - +// 00079bbf 9639 5 -4 WhiterunExterior17 00079bc3 0 6 0 0 +// 0010377b 8ed5 6 24 DawnstarWesternMineExterior 1 1 1 1 +// 000a3f44 9577 -22 2 RoriksteadEdge 2 9 2 2 +// 00100f4b 8ea2 26 25 WinterholdExterior01 00100f4a, 00100f49 3 3 3 3 +// 00103120 bc8e 42 -22 (near Riften) 4 2 4 4 +// 00105e9a 929d -18 24 SolitudeExterior03 5 0 5 5 +// 001030cb 7178 -40 1 SalviusFarmExterior01 (east of Markarth) 6 8 6 6 +// 00098776 980b 4 -19 HelgenExterior 000cce3d 7 5 7 7 +// 000e88cc 93de -9 14 (near Morthal) 0010519e, 0010519d, 000e88d2 8 7 8 8 +// 000b87df b51d 33 5 WindhelmAttackStart05 9 4 9 9 +// +void ESM4::Navigation::load(ESM4::Reader& reader) +{ + //mFormId = reader.hdr().record.id; + //mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: // seems to be unused? + { + if (!reader.getZString(mEditorId)) + throw std::runtime_error ("NAVI EDID data read error"); + break; + } + case ESM4::SUB_NVPP: + { + std::uint32_t total; + std::uint32_t count; + reader.get(total); + if (!total) + { + reader.get(count); // throw away + break; + } + + total -= 10; // HACK + std::uint32_t node; + for (std::uint32_t i = 0; i < total; ++i) + { + std::vector preferredPaths; + reader.get(count); + if (count == 1) + { + reader.get(node); + reader.get(count); + } + if (count) + { + preferredPaths.resize(count); + for (std::vector::iterator it = preferredPaths.begin(); + it != preferredPaths.end(); ++it) + { + reader.get(*it); + } + } + mPreferredPaths.push_back(std::make_pair(node, preferredPaths)); +#if 0 + std::cout << "node " << std::hex << node // FIXME: debugging only + << ", count " << count << ", i " << std::dec << i << std::endl; +#endif + } + reader.get(count); + assert(count == 1 && "expected separator"); + + reader.get(node); // HACK + std::vector preferredPaths; + mPreferredPaths.push_back(std::make_pair(node, preferredPaths)); // empty +#if 0 + std::cout << "node " << std::hex << node // FIXME: debugging only + << ", count " << 0 << std::endl; +#endif + + reader.get(count); // HACK + assert(count == 10 && "expected 0xa"); + std::uint32_t index; + for (std::uint32_t i = 0; i < count; ++i) + { + reader.get(node); + reader.get(index); +#if 0 + std::cout << "node " << std::hex << node // FIXME: debugging only + << ", index " << index << ", i " << std::dec << total+i << std::endl; +#endif + //std::pair::iterator, bool> res = + mPathIndexMap.insert(std::make_pair(node, index)); + // FIXME: this throws if more than one file is being loaded + //if (!res.second) + //throw std::runtime_error ("node already exists in the preferred path index map"); + } + break; + } + case ESM4::SUB_NVER: + { + std::uint32_t version; // always the same? (0x0c) + reader.get(version); // TODO: store this or use it for merging? + //std::cout << "NAVI version " << std::dec << version << std::endl; + break; + } + case ESM4::SUB_NVMI: // multiple + { + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + { + reader.skipSubRecordData(); // FIXME: FO3/FONV have different form of NavMeshInfo + break; + } + + //std::cout << "\nNVMI start" << std::endl; + NavMeshInfo nvmi; + nvmi.load(reader); + mNavMeshInfo.push_back (nvmi); + break; + } + case ESM4::SUB_NVSI: // from Dawnguard onwards + case ESM4::SUB_NVCI: // FO3 + { + reader.skipSubRecordData(); // FIXME: + break; + } + default: + { + throw std::runtime_error("ESM4::NAVI::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + } +} + +//void ESM4::Navigation::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Navigation::blank() +//{ +//} diff --git a/components/esm4/loadnavi.hpp b/components/esm4/loadnavi.hpp new file mode 100644 index 0000000000..117aa102d0 --- /dev/null +++ b/components/esm4/loadnavi.hpp @@ -0,0 +1,114 @@ +/* + Copyright (C) 2015-2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_NAVI_H +#define ESM4_NAVI_H + +#include +#include +#include + +#include "common.hpp" // CellGrid, Vertex + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Navigation + { +#pragma pack(push,1) + struct DoorRef + { + std::uint32_t unknown; + FormId formId; + }; + + struct Triangle + { + std::uint16_t vertexIndex0; + std::uint16_t vertexIndex1; + std::uint16_t vertexIndex2; + }; +#pragma pack(pop) + + struct IslandInfo + { + float minX; + float minY; + float minZ; + float maxX; + float maxY; + float maxZ; + std::vector triangles; + std::vector verticies; + + void load(ESM4::Reader& reader); + }; + + enum Flags // NVMI island flags (not certain) + { + FLG_Island = 0x00000020, + FLG_Modified = 0x00000000, // not island + FLG_Unmodified = 0x00000040 // not island + }; + + struct NavMeshInfo + { + FormId formId; + std::uint32_t flags; + // center point of the navmesh + float x; + float y; + float z; + std::uint32_t flagPrefMerges; + std::vector formIdMerged; + std::vector formIdPrefMerged; + std::vector linkedDoors; + std::vector islandInfo; + std::uint32_t locationMarker; + FormId worldSpaceId; + CellGrid cellGrid; + + void load(ESM4::Reader& reader); + }; + + std::string mEditorId; + + std::vector mNavMeshInfo; + + std::vector > > mPreferredPaths; + + std::map mPathIndexMap; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_NAVI_H diff --git a/components/esm4/loadnavm.cpp b/components/esm4/loadnavm.cpp new file mode 100644 index 0000000000..5a90f67b01 --- /dev/null +++ b/components/esm4/loadnavm.cpp @@ -0,0 +1,262 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadnavm.hpp" + +#include +#include +#include + +#include // FIXME: debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::NavMesh::NVNMstruct::load(ESM4::Reader& reader) +{ + //std::cout << "start: divisor " << std::dec << divisor << ", segments " << triSegments.size() << //std::endl; + //"this 0x" << this << std::endl; // FIXME + + std::uint32_t count; + + reader.get(unknownNVER); + reader.get(unknownLCTN); + reader.get(worldSpaceId); + //FLG_Tamriel = 0x0000003c, // grid info follows, possibly Tamriel? + //FLG_Morrowind = 0x01380000, // grid info follows, probably Skywind + if (worldSpaceId == 0x0000003c || worldSpaceId == 0x01380000) + { + // ^ + // Y | X Y Index + // | 0,0 0 + // 1 |23 0,1 1 + // 0 |01 1,0 2 + // +--- 1,1 3 + // 01 -> + // X + // + // e.g. Dagonfel X:13,14,15,16 Y:43,44,45,46 (Morrowind X:7 Y:22) + // + // Skywind: -4,-3 -2,-1 0,1 2,3 4,5 6,7 + // Morrowind: -2 -1 0 1 2 3 + // + // Formula seems to be floor(Skywind coord / 2) + // + reader.get(cellGrid.grid.y); // NOTE: reverse order + reader.get(cellGrid.grid.x); +// FIXME: debugging only +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + if (worldSpaceId == ESM4::FLG_Morrowind) + std::cout << padding << "NVNM MW: X " << std::dec << cellGrid.grid.x << ", Y " << cellGrid.grid.y << std::endl; + else + std::cout << padding << "NVNM SR: X " << std::dec << cellGrid.grid.x << ", Y " << cellGrid.grid.y << std::endl; +#endif + } + else + { + reader.get(cellGrid.cellId); + +#if 0 + std::string padding; // FIXME + padding.insert(0, reader.stackSize()*2, ' '); + if (worldSpaceId == 0) // interior + std::cout << padding << "NVNM Interior: cellId " << std::hex << cellGrid.cellId << std::endl; + else + std::cout << padding << "NVNM FormID: cellId " << std::hex << cellGrid.cellId << std::endl; +#endif + } + + reader.get(count); // numVerticies + if (count) + { + verticies.resize(count); + for (std::vector::iterator it = verticies.begin(); it != verticies.end(); ++it) + { + reader.get(*it); +// FIXME: debugging only +#if 0 + //if (reader.hdr().record.id == 0x2004ecc) // FIXME + std::cout << "nvnm vert " << (*it).x << ", " << (*it).y << ", " << (*it).z << std::endl; +#endif + } + } + + reader.get(count); // numTriangles; + if (count) + { + triangles.resize(count); + for (std::vector::iterator it = triangles.begin(); it != triangles.end(); ++it) + { + reader.get(*it); + } + } + + reader.get(count); // numExtConn; + if (count) + { + extConns.resize(count); + for (std::vector::iterator it = extConns.begin(); it != extConns.end(); ++it) + { + reader.get(*it); +// FIXME: debugging only +#if 0 + std::cout << "nvnm ext 0x" << std::hex << (*it).navMesh << std::endl; +#endif + } + } + + reader.get(count); // numDoorTriangles; + if (count) + { + doorTriangles.resize(count); + for (std::vector::iterator it = doorTriangles.begin(); it != doorTriangles.end(); ++it) + { + reader.get(*it); + } + } + + reader.get(count); // numCoverTriangles; + if (count) + { + coverTriangles.resize(count); + for (std::vector::iterator it = coverTriangles.begin(); it != coverTriangles.end(); ++it) + { + reader.get(*it); + } + } + + // abs((maxX - minX) / divisor) = Max X Distance + reader.get(divisor); // FIXME: latest over-writes old + + reader.get(maxXDist); // FIXME: update with formula + reader.get(maxYDist); + reader.get(minX); // FIXME: use std::min + reader.get(minY); + reader.get(minZ); + reader.get(maxX); + reader.get(maxY); + reader.get(maxZ); + + // FIXME: should check remaining size here + // there are divisor^2 segments, each segment is a vector of triangle indices + for (unsigned int i = 0; i < divisor*divisor; ++i) + { + reader.get(count); // NOTE: count may be zero + + std::vector indices; + indices.resize(count); + for (std::vector::iterator it = indices.begin(); it != indices.end(); ++it) + { + reader.get(*it); + } + triSegments.push_back(indices); + } + assert(triSegments.size() == divisor*divisor && "tiangle segments size is not the square of divisor"); +#if 0 + if (triSegments.size() != divisor*divisor) + std::cout << "divisor " << std::dec << divisor << ", segments " << triSegments.size() << //std::endl; + "this 0x" << this << std::endl; +#endif +} + +void ESM4::NavMesh::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + mFlags = reader.hdr().record.flags; + + //std::cout << "NavMesh 0x" << std::hex << this << std::endl; // FIXME + std::uint32_t subSize = 0; // for XXXX sub record + +// FIXME: debugging only +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "NAVM flags 0x" << std::hex << reader.hdr().record.flags << std::endl; + std::cout << padding << "NAVM id 0x" << std::hex << reader.hdr().record.id << std::endl; +#endif + while (reader.getSubRecordHeader()) + { + switch (reader.subRecordHeader().typeId) + { + case ESM4::SUB_NVNM: + { + NVNMstruct nvnm; + nvnm.load(reader); + mData.push_back(nvnm); // FIXME try swap + break; + } + case ESM4::SUB_ONAM: + case ESM4::SUB_PNAM: + case ESM4::SUB_NNAM: + { + if (subSize) + { + reader.skipSubRecordData(subSize); // special post XXXX + reader.updateRecordRead(subSize); // WARNING: manual update + subSize = 0; + } + else + //const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + //std::cout << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); // FIXME: process the subrecord rather than skip + + break; + } + case ESM4::SUB_XXXX: + { + reader.get(subSize); + break; + } + case ESM4::SUB_NVER: // FO3 + case ESM4::SUB_DATA: // FO3 + case ESM4::SUB_NVVX: // FO3 + case ESM4::SUB_NVTR: // FO3 + case ESM4::SUB_NVCA: // FO3 + case ESM4::SUB_NVDP: // FO3 + case ESM4::SUB_NVGD: // FO3 + case ESM4::SUB_NVEX: // FO3 + case ESM4::SUB_EDID: // FO3 + { + reader.skipSubRecordData(); // FIXME: + break; + } + default: + throw std::runtime_error("ESM4::NAVM::load - Unknown subrecord " + + ESM::printName(reader.subRecordHeader().typeId)); + } + } + //std::cout << "num nvnm " << std::dec << mData.size() << std::endl; // FIXME +} + +//void ESM4::NavMesh::save(ESM4::Writer& writer) const +//{ +//} + +void ESM4::NavMesh::blank() +{ +} diff --git a/components/esm4/loadnavm.hpp b/components/esm4/loadnavm.hpp new file mode 100644 index 0000000000..a7a86cf71d --- /dev/null +++ b/components/esm4/loadnavm.hpp @@ -0,0 +1,109 @@ +/* + Copyright (C) 2015, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_NAVM_H +#define ESM4_NAVM_H + +#include +#include + +#include "common.hpp" // CellGrid, Vertex + +namespace ESM4 +{ + class Reader; + class Writer; + + struct NavMesh + { +#pragma pack(push,1) + struct Triangle + { + std::uint16_t vertexIndex0; + std::uint16_t vertexIndex1; + std::uint16_t vertexIndex2; + std::uint16_t edge0; + std::uint16_t edge1; + std::uint16_t edge2; + std::uint16_t coverMarker; + std::uint16_t coverFlags; + }; + + struct ExtConnection + { + std::uint32_t unknown; + FormId navMesh; + std::uint16_t triangleIndex; + }; + + struct DoorTriangle + { + std::uint16_t triangleIndex; + std::uint32_t unknown; + FormId doorRef; + }; +#pragma pack(pop) + + struct NVNMstruct + { + std::uint32_t unknownNVER; + std::uint32_t unknownLCTN; + FormId worldSpaceId; + CellGrid cellGrid; + std::vector verticies; + std::vector triangles; + std::vector extConns; + std::vector doorTriangles; + std::vector coverTriangles; + std::uint32_t divisor; + float maxXDist; + float maxYDist; + float minX; + float minY; + float minZ; + float maxX; + float maxY; + float maxZ; + // there are divisor^2 segments, each segment is a vector of triangle indices + std::vector > triSegments; + + void load(ESM4::Reader& esm); + }; + + std::vector mData; // Up to 4 skywind cells in one Morrowind cell + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + void blank(); + }; + +} + +#endif // ESM4_NAVM_H diff --git a/components/esm4/loadnote.cpp b/components/esm4/loadnote.cpp new file mode 100644 index 0000000000..5f4b836fcf --- /dev/null +++ b/components/esm4/loadnote.cpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadnote.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Note::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_DATA: + case ESM4::SUB_MODB: + case ESM4::SUB_ONAM: + case ESM4::SUB_SNAM: + case ESM4::SUB_TNAM: + case ESM4::SUB_XNAM: + case ESM4::SUB_OBND: + { + //std::cout << "NOTE " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::NOTE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Note::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Note::blank() +//{ +//} diff --git a/components/esm4/loadnote.hpp b/components/esm4/loadnote.hpp new file mode 100644 index 0000000000..33f08257e9 --- /dev/null +++ b/components/esm4/loadnote.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_NOTE_H +#define ESM4_NOTE_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Note + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_NOTE_H diff --git a/components/esm4/loadnpc.cpp b/components/esm4/loadnpc.cpp new file mode 100644 index 0000000000..a05bedd90e --- /dev/null +++ b/components/esm4/loadnpc.cpp @@ -0,0 +1,308 @@ +/* + Copyright (C) 2016-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadnpc.hpp" + +#include +#include +#include // getline +#include // NOTE: for testing only +#include // NOTE: for testing only +#include // NOTE: for testing only + +//#include +//#include +#include + +#include "formid.hpp" // NOTE: for testing only +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Npc::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + std::uint32_t esmVer = reader.esmVersion(); + mIsTES4 = esmVer == ESM::VER_080 || esmVer == ESM::VER_100; + mIsFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + //mIsTES5 = esmVer == ESM::VER_094 || esmVer == ESM::VER_170; // WARN: FO3 is also VER_094 + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; // not for TES5, see Race + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_CNTO: + { + static InventoryItem inv; // FIXME: use unique_ptr here? + reader.get(inv); + reader.adjustFormId(inv.item); + mInventory.push_back(inv); + break; + } + case ESM4::SUB_SPLO: + { + FormId id; + reader.getFormId(id); + mSpell.push_back(id); + break; + } + case ESM4::SUB_PKID: + { + FormId id; + reader.getFormId(id); + mAIPackages.push_back(id); + break; + } + case ESM4::SUB_SNAM: + { + reader.get(mFaction); + reader.adjustFormId(mFaction.faction); + break; + } + case ESM4::SUB_RNAM: reader.getFormId(mRace); break; + case ESM4::SUB_CNAM: reader.getFormId(mClass); break; + case ESM4::SUB_HNAM: reader.getFormId(mHair); break; // not for TES5 + case ESM4::SUB_ENAM: reader.getFormId(mEyes); break; + // + case ESM4::SUB_INAM: reader.getFormId(mDeathItem); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + // + case ESM4::SUB_AIDT: + { + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || mIsFONV) + { + reader.skipSubRecordData(); // FIXME: process the subrecord rather than skip + break; + } + + reader.get(mAIData); // TES4 + break; + } + case ESM4::SUB_ACBS: + { + //if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || mIsFONV) + if (subHdr.dataSize == 24) + reader.get(mBaseConfig); + else + reader.get(&mBaseConfig, 16); // TES4 + + break; + } + case ESM4::SUB_DATA: + { + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || mIsFONV) + { + if (subHdr.dataSize != 0) // FIXME FO3 + reader.skipSubRecordData(); + break; // zero length + } + + reader.get(&mData, 33); // FIXME: check packing + break; + } + case ESM4::SUB_ZNAM: reader.getFormId(mCombatStyle); break; + case ESM4::SUB_CSCR: reader.getFormId(mSoundBase); break; + case ESM4::SUB_CSDI: reader.getFormId(mSound); break; + case ESM4::SUB_CSDC: reader.get(mSoundChance); break; + case ESM4::SUB_WNAM: + { + if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + reader.get(mWornArmor); + else + reader.get(mFootWeight); + break; + } + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_KFFZ: + { + std::string str; + if (!reader.getZString(str)) + throw std::runtime_error ("NPC_ KFFZ data read error"); + + // Seems to be only below 3, and only happens 3 times while loading TES4: + // Forward_SheogorathWithCane.kf + // TurnLeft_SheogorathWithCane.kf + // TurnRight_SheogorathWithCane.kf + std::stringstream ss(str); + std::string file; + while (std::getline(ss, file, '\0')) // split the strings + mKf.push_back(file); + + break; + } + case ESM4::SUB_LNAM: reader.get(mHairLength); break; + case ESM4::SUB_HCLR: + { + reader.get(mHairColour.red); + reader.get(mHairColour.green); + reader.get(mHairColour.blue); + reader.get(mHairColour.custom); + + break; + } + case ESM4::SUB_TPLT: reader.get(mBaseTemplate); break; + case ESM4::SUB_FGGS: + { + mSymShapeModeCoefficients.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymShapeModeCoefficients.at(i)); + + break; + } + case ESM4::SUB_FGGA: + { + mAsymShapeModeCoefficients.resize(30); + for (std::size_t i = 0; i < 30; ++i) + reader.get(mAsymShapeModeCoefficients.at(i)); + + break; + } + case ESM4::SUB_FGTS: + { + mSymTextureModeCoefficients.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymTextureModeCoefficients.at(i)); + + break; + } + case ESM4::SUB_FNAM: + { + reader.get(mFgRace); + //std::cout << "race " << mEditorId << " " << mRace << std::endl; // FIXME + //std::cout << "fg race " << mEditorId << " " << mFgRace << std::endl; // FIXME + break; + } + case ESM4::SUB_PNAM: // FO3/FONV/TES5 + { + FormId headPart; + reader.getFormId(headPart); + mHeadParts.push_back(headPart); + + break; + } + case ESM4::SUB_HCLF: // TES5 hair colour + { + reader.getFormId(mHairColourId); + + break; + } + case ESM4::SUB_COCT: // TES5 + { + std::uint32_t count; + reader.get(count); + + break; + } + case ESM4::SUB_DOFT: reader.getFormId(mDefaultOutfit); break; + case ESM4::SUB_SOFT: reader.getFormId(mSleepOutfit); break; + case ESM4::SUB_DPLT: reader.getFormId(mDefaultPkg); break; // AI package list + case ESM4::SUB_DEST: + case ESM4::SUB_DSTD: + case ESM4::SUB_DSTF: + { +#if 1 + boost::scoped_array dataBuf(new unsigned char[subHdr.dataSize]); + reader.get(&dataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + ss << mEditorId << " " << ESM::printName(subHdr.typeId) << ":size " << subHdr.dataSize << "\n"; + for (std::size_t i = 0; i < subHdr.dataSize; ++i) + { + if (dataBuf[i] > 64 && dataBuf[i] < 91) // looks like printable ascii char + ss << (char)(dataBuf[i]) << " "; + else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(dataBuf[i]); + if ((i & 0x000f) == 0xf) // wrap around + ss << "\n"; + else if (i < (size_t)(subHdr.dataSize-1)) // quiesce gcc + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + //std::cout << "NPC_ " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); +#endif + break; + } + case ESM4::SUB_NAM6: // height mult + case ESM4::SUB_NAM7: // weight mult + case ESM4::SUB_ATKR: + case ESM4::SUB_CRIF: + case ESM4::SUB_CSDT: + case ESM4::SUB_DNAM: + case ESM4::SUB_ECOR: + case ESM4::SUB_ANAM: + case ESM4::SUB_ATKD: + case ESM4::SUB_ATKE: + case ESM4::SUB_FTST: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_NAM5: + case ESM4::SUB_NAM8: + case ESM4::SUB_NAM9: + case ESM4::SUB_NAMA: + case ESM4::SUB_OBND: + case ESM4::SUB_PRKR: + case ESM4::SUB_PRKZ: + case ESM4::SUB_QNAM: + case ESM4::SUB_SPCT: + case ESM4::SUB_TIAS: + case ESM4::SUB_TINC: + case ESM4::SUB_TINI: + case ESM4::SUB_TINV: + case ESM4::SUB_VMAD: + case ESM4::SUB_VTCK: + case ESM4::SUB_GNAM: + case ESM4::SUB_SHRT: + case ESM4::SUB_SPOR: + case ESM4::SUB_EAMT: // FO3 + case ESM4::SUB_NAM4: // FO3 + case ESM4::SUB_COED: // FO3 + { + //std::cout << "NPC_ " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::NPC_::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Npc::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Npc::blank() +//{ +//} diff --git a/components/esm4/loadnpc.hpp b/components/esm4/loadnpc.hpp new file mode 100644 index 0000000000..316caeecc2 --- /dev/null +++ b/components/esm4/loadnpc.hpp @@ -0,0 +1,227 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_NPC__H +#define ESM4_NPC__H + +#include +#include +#include + +#include "actor.hpp" +#include "inventory.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Npc + { + enum ACBS_TES4 + { + TES4_Female = 0x000001, + TES4_Essential = 0x000002, + TES4_Respawn = 0x000008, + TES4_AutoCalcStats = 0x000010, + TES4_PCLevelOffset = 0x000080, + TES4_NoLowLevelProc = 0x000200, + TES4_NoRumors = 0x002000, + TES4_Summonable = 0x004000, + TES4_NoPersuasion = 0x008000, // different meaning to crea + TES4_CanCorpseCheck = 0x100000 // opposite of crea + }; + + enum ACBS_FO3 + { + FO3_Female = 0x00000001, + FO3_Essential = 0x00000002, + FO3_PresetFace = 0x00000004, // Is CharGen Face Preset + FO3_Respawn = 0x00000008, + FO3_AutoCalcStats = 0x00000010, + FO3_PCLevelMult = 0x00000080, + FO3_UseTemplate = 0x00000100, + FO3_NoLowLevelProc = 0x00000200, + FO3_NoBloodSpray = 0x00000800, + FO3_NoBloodDecal = 0x00001000, + FO3_NoVATSMelee = 0x00100000, + FO3_AnyRace = 0x00400000, + FO3_AutoCalcServ = 0x00800000, + FO3_NoKnockdown = 0x04000000, + FO3_NotPushable = 0x08000000, + FO3_NoRotateHead = 0x40000000 + }; + + enum ACBS_TES5 + { + TES5_Female = 0x00000001, + TES5_Essential = 0x00000002, + TES5_PresetFace = 0x00000004, // Is CharGen Face Preset + TES5_Respawn = 0x00000008, + TES5_AutoCalcStats = 0x00000010, + TES5_Unique = 0x00000020, + TES5_NoStealth = 0x00000040, // Doesn't affect stealth meter + TES5_PCLevelMult = 0x00000080, + //TES5_Unknown = 0x00000100, // Audio template? + TES5_Protected = 0x00000800, + TES5_Summonable = 0x00004000, + TES5_NoBleeding = 0x00010000, + TES5_Owned = 0x00040000, // owned/follow? (Horses, Atronachs, NOT Shadowmere) + TES5_GenderAnim = 0x00080000, // Opposite Gender Anims + TES5_SimpleActor = 0x00100000, + TES5_LoopedScript = 0x00200000, // AAvenicci, Arcadia, Silvia, Afflicted, TortureVictims + TES5_LoopedAudio = 0x10000000, // AAvenicci, Arcadia, Silvia, DA02 Cultists, Afflicted, TortureVictims + TES5_IsGhost = 0x20000000, // Ghost/non-interactable (Ghosts, Nocturnal) + TES5_Invulnerable = 0x80000000 + }; + + enum Template_Flags + { + TES5_UseTraits = 0x0001, // Destructible Object; Traits tab, including race, gender, height, weight, + // voice type, death item; Sounds tab; Animation tab; Character Gen tabs + TES5_UseStats = 0x0002, // Stats tab, including level, autocalc, skills, health/magicka/stamina, + // speed, bleedout, class + TES5_UseFactions = 0x0004, // both factions and assigned crime faction + TES5_UseSpellList = 0x0008, // both spells and perks + TES5_UseAIData = 0x0010, // AI Data tab, including aggression/confidence/morality, combat style and + // gift filter + TES5_UseAIPackage = 0x0020, // only the basic Packages listed on the AI Packages tab; + // rest of tab controlled by Def Pack List + TES5_UseBaseData = 0x0080, // including name and short name, and flags for Essential, Protected, + // Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter + TES5_UseInventory = 0x0100, // Inventory tab, including all outfits and geared-up item + // -- but not death item + TES5_UseScript = 0x0200, + TES5_UseDefined = 0x0400, // Def Pack List (the dropdown-selected package lists on the AI Packages tab) + TES5_UseAtkData = 0x0800, // Attack Data tab, including override from behavior graph race, + // events, and data) + TES5_UseKeywords = 0x1000 + }; + +#pragma pack(push, 1) + struct SkillValues + { + std::uint8_t armorer; + std::uint8_t athletics; + std::uint8_t blade; + std::uint8_t block; + std::uint8_t blunt; + std::uint8_t handToHand; + std::uint8_t heavyArmor; + std::uint8_t alchemy; + std::uint8_t alteration; + std::uint8_t conjuration; + std::uint8_t destruction; + std::uint8_t illusion; + std::uint8_t mysticism; + std::uint8_t restoration; + std::uint8_t acrobatics; + std::uint8_t lightArmor; + std::uint8_t marksman; + std::uint8_t mercantile; + std::uint8_t security; + std::uint8_t sneak; + std::uint8_t speechcraft; + }; + + struct HairColour + { + std::uint8_t red; + std::uint8_t green; + std::uint8_t blue; + std::uint8_t custom; // alpha? + }; + + struct Data + { + SkillValues skills; + std::uint32_t health; + AttributeValues attribs; + }; + +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + bool mIsTES4; + bool mIsFONV; + + std::string mEditorId; + std::string mFullName; + std::string mModel; // skeleton model (can be a marker in FO3/FONV) + + FormId mRace; + FormId mClass; + FormId mHair; // not for TES5, see mHeadParts + FormId mEyes; + + std::vector mHeadParts; // FO3/FONV/TES5 + + float mHairLength; + HairColour mHairColour; // TES4/FO3/FONV + FormId mHairColourId; // TES5 + + FormId mDeathItem; + std::vector mSpell; + FormId mScriptId; + + AIData mAIData; + std::vector mAIPackages; // seems to be in priority order, 0 = highest priority + ActorBaseConfig mBaseConfig; // union + ActorFaction mFaction; + Data mData; + FormId mCombatStyle; + FormId mSoundBase; + FormId mSound; + std::uint8_t mSoundChance; + float mFootWeight; + + float mBoundRadius; + std::vector mKf; // filenames only, get directory path from mModel + + std::vector mInventory; + + FormId mBaseTemplate; // FO3/FONV/TES5 + FormId mWornArmor; // TES5 only? + + FormId mDefaultOutfit; // TES5 OTFT + FormId mSleepOutfit; // TES5 OTFT + FormId mDefaultPkg; + + std::vector mSymShapeModeCoefficients; // size 0 or 50 + std::vector mAsymShapeModeCoefficients; // size 0 or 30 + std::vector mSymTextureModeCoefficients; // size 0 or 50 + std::int16_t mFgRace; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_NPC__H diff --git a/components/esm4/loadotft.cpp b/components/esm4/loadotft.cpp new file mode 100644 index 0000000000..e24e3fbc9c --- /dev/null +++ b/components/esm4/loadotft.cpp @@ -0,0 +1,75 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadotft.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Outfit::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_INAM: + { + std::size_t numObj = subHdr.dataSize / sizeof(FormId); + for (std::size_t i = 0; i < numObj; ++i) + { + FormId formId; + reader.getFormId(formId); + + mInventory.push_back(formId); + } + + break; + } + default: + //std::cout << "OTFT " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + throw std::runtime_error("ESM4::OTFT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Outfit::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Outfit::blank() +//{ +//} diff --git a/components/esm4/loadotft.hpp b/components/esm4/loadotft.hpp new file mode 100644 index 0000000000..cf15775a91 --- /dev/null +++ b/components/esm4/loadotft.hpp @@ -0,0 +1,57 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_OTFT_H +#define ESM4_OTFT_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Outfit + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::vector mInventory; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_OTFT_H diff --git a/components/esm4/loadpack.cpp b/components/esm4/loadpack.cpp new file mode 100644 index 0000000000..7d0e1027c7 --- /dev/null +++ b/components/esm4/loadpack.cpp @@ -0,0 +1,182 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadpack.hpp" + +#include +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::AIPackage::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_PKDT: + { + if (subHdr.dataSize != sizeof(PKDT) && subHdr.dataSize == 4) + { + //std::cout << "skip fallout" << mEditorId << std::endl; // FIXME + reader.get(mData.flags); + mData.type = 0; // FIXME + } + else if (subHdr.dataSize != sizeof(mData)) + reader.skipSubRecordData(); // FIXME: FO3 + else + reader.get(mData); + + break; + } + case ESM4::SUB_PSDT: //reader.get(mSchedule); break; + { + if (subHdr.dataSize != sizeof(mSchedule)) + reader.skipSubRecordData(); // FIXME: + else + reader.get(mSchedule); // TES4 + + break; + } + case ESM4::SUB_PLDT: + { + if (subHdr.dataSize != sizeof(mLocation)) + reader.skipSubRecordData(); // FIXME: + else + { + reader.get(mLocation); // TES4 + if (mLocation.type != 5) + reader.adjustFormId(mLocation.location); + } + + break; + } + case ESM4::SUB_PTDT: + { + if (subHdr.dataSize != sizeof(mTarget)) + reader.skipSubRecordData(); // FIXME: FO3 + else + { + reader.get(mTarget); // TES4 + if (mLocation.type != 2) + reader.adjustFormId(mTarget.target); + } + + break; + } + case ESM4::SUB_CTDA: + { + if (subHdr.dataSize != sizeof(CTDA)) + { + reader.skipSubRecordData(); // FIXME: FO3 + break; + } + + static CTDA condition; + reader.get(condition); + // FIXME: how to "unadjust" if not FormId? + //adjustFormId(condition.param1); + //adjustFormId(condition.param2); + mConditions.push_back(condition); + + break; + } + case ESM4::SUB_CTDT: // always 20 for TES4 + case ESM4::SUB_TNAM: // FO3 + case ESM4::SUB_INAM: // FO3 + case ESM4::SUB_CNAM: // FO3 + case ESM4::SUB_SCHR: // FO3 + case ESM4::SUB_POBA: // FO3 + case ESM4::SUB_POCA: // FO3 + case ESM4::SUB_POEA: // FO3 + case ESM4::SUB_SCTX: // FO3 + case ESM4::SUB_SCDA: // FO3 + case ESM4::SUB_SCRO: // FO3 + case ESM4::SUB_IDLA: // FO3 + case ESM4::SUB_IDLC: // FO3 + case ESM4::SUB_IDLF: // FO3 + case ESM4::SUB_IDLT: // FO3 + case ESM4::SUB_PKDD: // FO3 + case ESM4::SUB_PKD2: // FO3 + case ESM4::SUB_PKPT: // FO3 + case ESM4::SUB_PKED: // FO3 + case ESM4::SUB_PKE2: // FO3 + case ESM4::SUB_PKAM: // FO3 + case ESM4::SUB_PUID: // FO3 + case ESM4::SUB_PKW3: // FO3 + case ESM4::SUB_PTD2: // FO3 + case ESM4::SUB_PLD2: // FO3 + case ESM4::SUB_PKFD: // FO3 + case ESM4::SUB_SLSD: // FO3 + case ESM4::SUB_SCVR: // FO3 + case ESM4::SUB_SCRV: // FO3 + case ESM4::SUB_IDLB: // FO3 + case ESM4::SUB_ANAM: // TES5 + case ESM4::SUB_BNAM: // TES5 + case ESM4::SUB_FNAM: // TES5 + case ESM4::SUB_PNAM: // TES5 + case ESM4::SUB_QNAM: // TES5 + case ESM4::SUB_UNAM: // TES5 + case ESM4::SUB_XNAM: // TES5 + case ESM4::SUB_PDTO: // TES5 + case ESM4::SUB_PTDA: // TES5 + case ESM4::SUB_PFOR: // TES5 + case ESM4::SUB_PFO2: // TES5 + case ESM4::SUB_PRCB: // TES5 + case ESM4::SUB_PKCU: // TES5 + case ESM4::SUB_PKC2: // TES5 + case ESM4::SUB_CITC: // TES5 + case ESM4::SUB_CIS1: // TES5 + case ESM4::SUB_CIS2: // TES5 + case ESM4::SUB_VMAD: // TES5 + case ESM4::SUB_TPIC: // TES5 + { + //std::cout << "PACK " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::PACK::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::AIPackage::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::AIPackage::blank() +//{ +//} diff --git a/components/esm4/loadpack.hpp b/components/esm4/loadpack.hpp new file mode 100644 index 0000000000..e04c67a7d8 --- /dev/null +++ b/components/esm4/loadpack.hpp @@ -0,0 +1,107 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_PACK_H +#define ESM4_PACK_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct AIPackage + { +#pragma pack(push, 1) + struct PKDT // data + { + std::uint32_t flags; + std::int32_t type; + }; + + struct PSDT // schedule + { + std::uint8_t month; // Any = 0xff + std::uint8_t dayOfWeek; // Any = 0xff + std::uint8_t date; // Any = 0 + std::uint8_t time; // Any = 0xff + std::uint32_t duration; + }; + + struct PLDT // location + { + std::int32_t type = 0xff; // 0 = near ref, 1 = in cell, 2 = current loc, 3 = editor loc, 4 = obj id, 5 = obj type, 0xff = no location data + FormId location; // uint32_t if type = 5 + std::int32_t radius; + }; + + struct PTDT // target + { + std::int32_t type = 0xff; // 0 = specific ref, 1 = obj id, 2 = obj type, 0xff = no target data + FormId target; // uint32_t if type = 2 + std::int32_t distance; + }; + + // NOTE: param1/param2 can be FormId or number, but assume FormId so that adjustFormId + // can be called + struct CTDA + { + std::uint8_t condition; + std::uint8_t unknown1; // probably padding + std::uint8_t unknown2; // probably padding + std::uint8_t unknown3; // probably padding + float compValue; + std::int32_t fnIndex; + FormId param1; + FormId param2; + std::uint32_t unknown4; // probably padding + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + PKDT mData; + PSDT mSchedule; + PLDT mLocation; + PTDT mTarget; + std::vector mConditions; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_PACK_H diff --git a/components/esm4/loadpgrd.cpp b/components/esm4/loadpgrd.cpp new file mode 100644 index 0000000000..d1b9f6d57f --- /dev/null +++ b/components/esm4/loadpgrd.cpp @@ -0,0 +1,163 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadpgrd.hpp" + +#include +//#include // FIXME: for debugging only +//#include // FIXME: for debugging only + +//#include // FIXME for debugging only + +#include "formid.hpp" // FIXME: for mEditorId workaround +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Pathgrid::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + mEditorId = formIdToString(mFormId); // FIXME: quick workaround to use existing code + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_PGRP: + { + std::size_t numNodes = subHdr.dataSize / sizeof(PGRP); + if (numNodes != std::size_t(mData)) // keep gcc quiet + throw std::runtime_error("ESM4::PGRD::load numNodes mismatch"); + + mNodes.resize(numNodes); + for (std::size_t i = 0; i < numNodes; ++i) + { + reader.get(mNodes.at(i)); + + if (int(mNodes.at(i).z) % 2 == 0) + mNodes.at(i).priority = 0; + else + mNodes.at(i).priority = 1; + } + + break; + } + case ESM4::SUB_PGRR: + { + static PGRR link; + + for (std::size_t i = 0; i < std::size_t(mData); ++i) // keep gcc quiet + { + for (std::size_t j = 0; j < mNodes[i].numLinks; ++j) + { + link.startNode = std::int16_t(i); + + reader.get(link.endNode); + if (link.endNode == -1) + continue; + + // ICMarketDistrictTheBestDefenseBasement doesn't have a PGRR sub-record + // CELL formId 00049E2A + // PGRD formId 000304B7 + //if (mFormId == 0x0001C2C8) + //std::cout << link.startNode << "," << link.endNode << std::endl; + mLinks.push_back(link); + } + } + + break; + } + case ESM4::SUB_PGRI: + { + std::size_t numForeign = subHdr.dataSize / sizeof(PGRI); + mForeign.resize(numForeign); + for (std::size_t i = 0; i < numForeign; ++i) + { + reader.get(mForeign.at(i)); + // mForeign.at(i).localNode;// &= 0xffff; // some have junk high bits (maybe flags?) + } + + break; + } + case ESM4::SUB_PGRL: + { + static PGRL objLink; + reader.get(objLink.object); + // object linkedNode + std::size_t numNodes = (subHdr.dataSize - sizeof(int32_t)) / sizeof(int32_t); + + objLink.linkedNodes.resize(numNodes); + for (std::size_t i = 0; i < numNodes; ++i) + reader.get(objLink.linkedNodes.at(i)); + + mObjects.push_back(objLink); + + break; + } + case ESM4::SUB_PGAG: + { +#if 0 + boost::scoped_array mDataBuf(new unsigned char[subHdr.dataSize]); + reader.get(&mDataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + ss << mEditorId << " " << ESM::printName(subHdr.typeId) << ":size " << subHdr.dataSize << "\n"; + for (std::size_t i = 0; i < subHdr.dataSize; ++i) + { + //if (mDataBuf[i] > 64 && mDataBuf[i] < 91) // looks like printable ascii char + //ss << (char)(mDataBuf[i]) << " "; + //else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]); + if ((i & 0x000f) == 0xf) // wrap around + ss << "\n"; + else if (i < subHdr.dataSize-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + //std::cout << "PGRD " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); +#endif + break; + } + default: + throw std::runtime_error("ESM4::PGRD::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Pathgrid::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Pathgrid::blank() +//{ +//} diff --git a/components/esm4/loadpgrd.hpp b/components/esm4/loadpgrd.hpp new file mode 100644 index 0000000000..5ee864b93b --- /dev/null +++ b/components/esm4/loadpgrd.hpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2020 - 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_PGRD_H +#define ESM4_PGRD_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Pathgrid + { +#pragma pack(push, 1) + struct PGRP + { + float x; + float y; + float z; + std::uint8_t numLinks; + std::uint8_t priority; // probably padding, repurposing + std::uint16_t unknown; // probably padding + }; + + struct PGRR + { + std::int16_t startNode; + std::int16_t endNode; + }; + + struct PGRI + { + std::int32_t localNode; + float x; // foreign + float y; // foreign + float z; // foreign + }; +#pragma pack(pop) + + struct PGRL + { + FormId object; + std::vector linkedNodes; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; // FIXME: no such record for PGRD, but keep here to avoid extra work for now + + std::int16_t mData; // number of nodes + std::vector mNodes; + std::vector mLinks; + std::vector mForeign; + std::vector mObjects; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_PGRD_H diff --git a/components/esm4/loadpgre.cpp b/components/esm4/loadpgre.cpp new file mode 100644 index 0000000000..b38305b3b5 --- /dev/null +++ b/components/esm4/loadpgre.cpp @@ -0,0 +1,96 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadpgre.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::PlacedGrenade::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_NAME: + case ESM4::SUB_XEZN: + case ESM4::SUB_XRGD: + case ESM4::SUB_XRGB: + case ESM4::SUB_XPRD: + case ESM4::SUB_XPPA: + case ESM4::SUB_INAM: + case ESM4::SUB_TNAM: + case ESM4::SUB_XOWN: + case ESM4::SUB_XRNK: + case ESM4::SUB_XCNT: + case ESM4::SUB_XRDS: + case ESM4::SUB_XHLP: + case ESM4::SUB_XPWR: + case ESM4::SUB_XDCR: + case ESM4::SUB_XLKR: + case ESM4::SUB_XCLP: + case ESM4::SUB_XAPD: + case ESM4::SUB_XAPR: + case ESM4::SUB_XATO: + case ESM4::SUB_XESP: + case ESM4::SUB_XEMI: + case ESM4::SUB_XMBR: + case ESM4::SUB_XIBS: + case ESM4::SUB_XSCL: + case ESM4::SUB_DATA: + { + //std::cout << "PGRE " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + std::cout << "PGRE " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + //throw std::runtime_error("ESM4::PGRE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::PlacedGrenade::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::PlacedGrenade::blank() +//{ +//} diff --git a/components/esm4/loadpgre.hpp b/components/esm4/loadpgre.hpp new file mode 100644 index 0000000000..90e3749508 --- /dev/null +++ b/components/esm4/loadpgre.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_PGRE_H +#define ESM4_PGRE_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct PlacedGrenade + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_PGRE_H diff --git a/components/esm4/loadpwat.cpp b/components/esm4/loadpwat.cpp new file mode 100644 index 0000000000..8f403470a2 --- /dev/null +++ b/components/esm4/loadpwat.cpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadpwat.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::PlaceableWater::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_OBND: + case ESM4::SUB_MODL: + case ESM4::SUB_DNAM: + { + //std::cout << "PWAT " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + std::cout << "PWAT " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + //throw std::runtime_error("ESM4::PWAT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::PlaceableWater::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::PlaceableWater::blank() +//{ +//} diff --git a/components/esm4/loadpwat.hpp b/components/esm4/loadpwat.hpp new file mode 100644 index 0000000000..4a74faeb02 --- /dev/null +++ b/components/esm4/loadpwat.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_PWAT_H +#define ESM4_PWAT_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct PlaceableWater + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_PWAT_H diff --git a/components/esm4/loadqust.cpp b/components/esm4/loadqust.cpp new file mode 100644 index 0000000000..14aa51e17a --- /dev/null +++ b/components/esm4/loadqust.cpp @@ -0,0 +1,164 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadqust.hpp" + +#include +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Quest::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getZString(mQuestName); break; + case ESM4::SUB_ICON: reader.getZString(mFileName); break; // TES4 (none in FO3/FONV) + case ESM4::SUB_DATA: + { + if (subHdr.dataSize == 2) // TES4 + { + reader.get(&mData, 2); + mData.questDelay = 0.f; // unused in TES4 but keep it clean + + //if ((mData.flags & Flag_StartGameEnabled) != 0) + //std::cout << "start quest " << mEditorId << std::endl; + } + else + reader.get(mData); // FO3 + + break; + } + case ESM4::SUB_SCRI: reader.get(mQuestScript); break; + case ESM4::SUB_CTDA: // FIXME: how to detect if 1st/2nd param is a formid? + { + if (subHdr.dataSize == 24) // TES4 + { + TargetCondition cond; + reader.get(&cond, 24); + cond.reference = 0; // unused in TES4 but keep it clean + mTargetConditions.push_back(cond); + } + else if (subHdr.dataSize == 28) + { + TargetCondition cond; + reader.get(cond); // FO3/FONV + if (cond.reference) + reader.adjustFormId(cond.reference); + mTargetConditions.push_back(cond); + } + else + { + // one record with size 20: EDID GenericSupMutBehemoth + reader.skipSubRecordData(); // FIXME + } + // FIXME: support TES5 + + break; + } + case ESM4::SUB_SCHR: reader.get(mScript.scriptHeader); break; + case ESM4::SUB_SCDA: reader.skipSubRecordData(); break; // compiled script data + case ESM4::SUB_SCTX: reader.getString(mScript.scriptSource); break; + case ESM4::SUB_SCRO: reader.getFormId(mScript.globReference); break; + case ESM4::SUB_INDX: + case ESM4::SUB_QSDT: + case ESM4::SUB_CNAM: + case ESM4::SUB_QSTA: + case ESM4::SUB_NNAM: // FO3 + case ESM4::SUB_QOBJ: // FO3 + case ESM4::SUB_NAM0: // FO3 + case ESM4::SUB_ANAM: // TES5 + case ESM4::SUB_DNAM: // TES5 + case ESM4::SUB_ENAM: // TES5 + case ESM4::SUB_FNAM: // TES5 + case ESM4::SUB_NEXT: // TES5 + case ESM4::SUB_ALCA: // TES5 + case ESM4::SUB_ALCL: // TES5 + case ESM4::SUB_ALCO: // TES5 + case ESM4::SUB_ALDN: // TES5 + case ESM4::SUB_ALEA: // TES5 + case ESM4::SUB_ALED: // TES5 + case ESM4::SUB_ALEQ: // TES5 + case ESM4::SUB_ALFA: // TES5 + case ESM4::SUB_ALFC: // TES5 + case ESM4::SUB_ALFD: // TES5 + case ESM4::SUB_ALFE: // TES5 + case ESM4::SUB_ALFI: // TES5 + case ESM4::SUB_ALFL: // TES5 + case ESM4::SUB_ALFR: // TES5 + case ESM4::SUB_ALID: // TES5 + case ESM4::SUB_ALLS: // TES5 + case ESM4::SUB_ALNA: // TES5 + case ESM4::SUB_ALNT: // TES5 + case ESM4::SUB_ALPC: // TES5 + case ESM4::SUB_ALRT: // TES5 + case ESM4::SUB_ALSP: // TES5 + case ESM4::SUB_ALST: // TES5 + case ESM4::SUB_ALUA: // TES5 + case ESM4::SUB_CIS2: // TES5 + case ESM4::SUB_CNTO: // TES5 + case ESM4::SUB_COCT: // TES5 + case ESM4::SUB_ECOR: // TES5 + case ESM4::SUB_FLTR: // TES5 + case ESM4::SUB_KNAM: // TES5 + case ESM4::SUB_KSIZ: // TES5 + case ESM4::SUB_KWDA: // TES5 + case ESM4::SUB_QNAM: // TES5 + case ESM4::SUB_QTGL: // TES5 + case ESM4::SUB_SPOR: // TES5 + case ESM4::SUB_VMAD: // TES5 + case ESM4::SUB_VTCK: // TES5 + { + //std::cout << "QUST " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::QUST::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + //if (mEditorId == "DAConversations") + //std::cout << mEditorId << std::endl; +} + +//void ESM4::Quest::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Quest::blank() +//{ +//} diff --git a/components/esm4/loadqust.hpp b/components/esm4/loadqust.hpp new file mode 100644 index 0000000000..2fa9ff7097 --- /dev/null +++ b/components/esm4/loadqust.hpp @@ -0,0 +1,81 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_QUST_H +#define ESM4_QUST_H + +#include + +#include "formid.hpp" +#include "script.hpp" // TargetCondition, ScriptDefinition + +namespace ESM4 +{ + class Reader; + class Writer; + +#pragma pack(push, 1) + struct QuestData + { + std::uint8_t flags; // Quest_Flags + std::uint8_t priority; + std::uint16_t padding; // FO3 + float questDelay; // FO3 + }; +#pragma pack(pop) + + struct Quest + { + // NOTE: these values are for TES4 + enum Quest_Flags + { + Flag_StartGameEnabled = 0x01, + Flag_AllowRepeatConvTopic = 0x04, + Flag_AllowRepeatStages = 0x08 + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mQuestName; + std::string mFileName; // texture file + FormId mQuestScript; + + QuestData mData; + + std::vector mTargetConditions; + + ScriptDefinition mScript; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_QUST_H diff --git a/components/esm4/loadrace.cpp b/components/esm4/loadrace.cpp new file mode 100644 index 0000000000..142cfd2adf --- /dev/null +++ b/components/esm4/loadrace.cpp @@ -0,0 +1,689 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadrace.hpp" + +#include +#include +#include // FIXME: for debugging only +#include // FIXME: for debugging only + +#include "formid.hpp" +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Race::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + std::uint32_t esmVer = reader.esmVersion(); + bool isTES4 = esmVer == ESM::VER_080 || esmVer == ESM::VER_100; + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + bool isFO3 = false; + + bool isMale = false; + int curr_part = -1; // 0 = head, 1 = body, 2 = egt, 3 = hkx + std::uint32_t currentIndex = 0xffffffff; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + //std::cout << "RACE " << ESM::printName(subHdr.typeId) << std::endl; + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: + { + reader.getZString(mEditorId); + // TES4 + // Sheogorath 0x0005308E + // GoldenSaint 0x0001208F + // DarkSeducer 0x0001208E + // VampireRace 0x00000019 + // Dremora 0x00038010 + // Argonian 0x00023FE9 + // Nord 0x000224FD + // Breton 0x000224FC + // WoodElf 0x000223C8 + // Khajiit 0x000223C7 + // DarkElf 0x000191C1 + // Orc 0x000191C0 + // HighElf 0x00019204 + // Redguard 0x00000D43 + // Imperial 0x00000907 + break; + } + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DESC: + { + if (subHdr.dataSize == 1) // FO3? + { + reader.skipSubRecordData(); + break; + } + + reader.getLocalizedString(mDesc); break; + } + case ESM4::SUB_SPLO: // bonus spell formid (TES5 may have SPCT and multiple SPLO) + { + FormId magic; + reader.getFormId(magic); + mBonusSpells.push_back(magic); +// std::cout << "RACE " << printName(subHdr.typeId) << " " << formIdToString(magic) << std::endl; + + break; + } + case ESM4::SUB_DATA: // ?? different length for TES5 + { +// DATA:size 128 +// 0f 0f ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 00 00 +// 9a 99 99 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f +// 48 89 10 00 00 00 40 41 00 00 00 00 00 00 48 43 +// 00 00 48 43 00 00 80 3f 9a 99 19 3f 00 00 00 40 +// 01 00 00 00 ff ff ff ff ff ff ff ff 00 00 00 00 +// ff ff ff ff 00 00 00 00 00 00 20 41 00 00 a0 40 +// 00 00 a0 40 00 00 80 42 ff ff ff ff 00 00 00 00 +// 00 00 00 00 9a 99 99 3e 00 00 a0 40 02 00 00 00 +#if 0 + unsigned char mDataBuf[256/*bufSize*/]; + reader.get(&mDataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + ss << ESM::printName(subHdr.typeId) << ":size " << subHdr.dataSize << "\n"; + for (unsigned int i = 0; i < subHdr.dataSize; ++i) + { + //if (mDataBuf[i] > 64 && mDataBuf[i] < 91) + //ss << (char)(mDataBuf[i]) << " "; + //else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]); + if ((i & 0x000f) == 0xf) + ss << "\n"; + else if (i < 256/*bufSize*/-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + if (subHdr.dataSize == 36) // TES4/FO3/FONV + { + if (!isTES4 && !isFONV && !mIsTES5) + isFO3 = true; + + std::uint8_t skill; + std::uint8_t bonus; + for (unsigned int i = 0; i < 8; ++i) + { + reader.get(skill); + reader.get(bonus); + mSkillBonus[static_cast(skill)] = bonus; + } + reader.get(mHeightMale); + reader.get(mHeightFemale); + reader.get(mWeightMale); + reader.get(mWeightFemale); + reader.get(mRaceFlags); + } + else if (subHdr.dataSize >= 128 && subHdr.dataSize <= 164) // TES5 + { + mIsTES5 = true; + + std::uint8_t skill; + std::uint8_t bonus; + for (unsigned int i = 0; i < 7; ++i) + { + reader.get(skill); + reader.get(bonus); + mSkillBonus[static_cast(skill)] = bonus; + } + std::uint16_t unknown; + reader.get(unknown); + + reader.get(mHeightMale); + reader.get(mHeightFemale); + reader.get(mWeightMale); + reader.get(mWeightFemale); + reader.get(mRaceFlags); + + // FIXME + float dummy; + reader.get(dummy); // starting health + reader.get(dummy); // starting magicka + reader.get(dummy); // starting stamina + reader.get(dummy); // base carry weight + reader.get(dummy); // base mass + reader.get(dummy); // accleration rate + reader.get(dummy); // decleration rate + + uint32_t dummy2; + reader.get(dummy2); // size + reader.get(dummy2); // head biped object + reader.get(dummy2); // hair biped object + reader.get(dummy); // injured health % (0.f..1.f) + reader.get(dummy2); // shield biped object + reader.get(dummy); // health regen + reader.get(dummy); // magicka regen + reader.get(dummy); // stamina regen + reader.get(dummy); // unarmed damage + reader.get(dummy); // unarmed reach + reader.get(dummy2); // body biped object + reader.get(dummy); // aim angle tolerance + reader.get(dummy2); // unknown + reader.get(dummy); // angular accleration rate + reader.get(dummy); // angular tolerance + reader.get(dummy2); // flags + + if (subHdr.dataSize > 128) + { + reader.get(dummy2); // unknown 1 + reader.get(dummy2); // unknown 2 + reader.get(dummy2); // unknown 3 + reader.get(dummy2); // unknown 4 + reader.get(dummy2); // unknown 5 + reader.get(dummy2); // unknown 6 + reader.get(dummy2); // unknown 7 + reader.get(dummy2); // unknown 8 + reader.get(dummy2); // unknown 9 + } + } + else + { + reader.skipSubRecordData(); + std::cout << "RACE " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + } +#endif + break; + } + case ESM4::SUB_DNAM: + { + reader.get(mDefaultHair[0]); // male + reader.get(mDefaultHair[1]); // female + + break; + } + case ESM4::SUB_CNAM: // Only in TES4? + // CNAM SNAM VNAM + // Sheogorath 0x0 0000 98 2b 10011000 00101011 + // Golden Saint 0x3 0011 26 46 00100110 01000110 + // Dark Seducer 0xC 1100 df 55 11011111 01010101 + // Vampire Race 0x0 0000 77 44 01110111 10001000 + // Dremora 0x7 0111 bf 32 10111111 00110010 + // Argonian 0x0 0000 dc 3c 11011100 00111100 + // Nord 0x5 0101 b6 03 10110110 00000011 + // Breton 0x5 0101 48 1d 01001000 00011101 00000000 00000907 (Imperial) + // Wood Elf 0xD 1101 2e 4a 00101110 01001010 00019204 00019204 (HighElf) + // khajiit 0x5 0101 54 5b 01010100 01011011 00023FE9 00023FE9 (Argonian) + // Dark Elf 0x0 0000 72 54 01110010 01010100 00019204 00019204 (HighElf) + // Orc 0xC 1100 74 09 01110100 00001001 000224FD 000224FD (Nord) + // High Elf 0xF 1111 e6 21 11100110 00100001 + // Redguard 0xD 1101 a9 61 10101001 01100001 + // Imperial 0xD 1101 8e 35 10001110 00110101 + { + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_PNAM: reader.get(mFaceGenMainClamp); break; // 0x40A00000 = 5.f + case ESM4::SUB_UNAM: reader.get(mFaceGenFaceClamp); break; // 0x40400000 = 3.f + case ESM4::SUB_ATTR: // Only in TES4? + { + if (subHdr.dataSize == 2) // FO3? + { + reader.skipSubRecordData(); + break; + } + reader.get(mAttribMale.strength); + reader.get(mAttribMale.intelligence); + reader.get(mAttribMale.willpower); + reader.get(mAttribMale.agility); + reader.get(mAttribMale.speed); + reader.get(mAttribMale.endurance); + reader.get(mAttribMale.personality); + reader.get(mAttribMale.luck); + reader.get(mAttribFemale.strength); + reader.get(mAttribFemale.intelligence); + reader.get(mAttribFemale.willpower); + reader.get(mAttribFemale.agility); + reader.get(mAttribFemale.speed); + reader.get(mAttribFemale.endurance); + reader.get(mAttribFemale.personality); + reader.get(mAttribFemale.luck); + + break; + } + // [0..9]-> ICON + // NAM0 -> INDX -> MODL --+ + // ^ -> MODB | + // | | + // +-------------+ + // + case ESM4::SUB_NAM0: // start marker head data /* 1 */ + { + curr_part = 0; // head part + + if (isFO3 || isFONV) + { + mHeadParts.resize(8); + mHeadPartsFemale.resize(8); + } + else if (isTES4) + mHeadParts.resize(9); // assumed based on Construction Set + else + { + mHeadPartIdsMale.resize(5); + mHeadPartIdsFemale.resize(5); + } + + currentIndex = 0xffffffff; + break; + } + case ESM4::SUB_INDX: + { + reader.get(currentIndex); + // FIXME: below check is rather useless + //if (headpart) + //{ + // if (currentIndex > 8) + // throw std::runtime_error("ESM4::RACE::load - too many head part " + currentIndex); + //} + //else // bodypart + //{ + // if (currentIndex > 4) + // throw std::runtime_error("ESM4::RACE::load - too many body part " + currentIndex); + //} + + break; + } + case ESM4::SUB_MODL: + { + if (curr_part == 0) // head part + { + if (isMale || isTES4) + reader.getZString(mHeadParts[currentIndex].mesh); + else + reader.getZString(mHeadPartsFemale[currentIndex].mesh); // TODO: check TES4 + + // TES5 keeps head part formid in mHeadPartIdsMale and mHeadPartIdsFemale + } + else if (curr_part == 1) // body part + { + if (isMale) + reader.getZString(mBodyPartsMale[currentIndex].mesh); + else + reader.getZString(mBodyPartsFemale[currentIndex].mesh); + + // TES5 seems to have no body parts at all, instead keep EGT models + } + else if (curr_part == 2) // egt + { + //std::cout << mEditorId << " egt " << currentIndex << std::endl; // FIXME + reader.skipSubRecordData(); // FIXME TES5 egt + } + else + { + //std::cout << mEditorId << " hkx " << currentIndex << std::endl; // FIXME + reader.skipSubRecordData(); // FIXME TES5 hkx + } + + break; + } + case ESM4::SUB_MODB: reader.skipSubRecordData(); break; // always 0x0000? + case ESM4::SUB_ICON: + { + if (curr_part == 0) // head part + { + if (isMale || isTES4) + reader.getZString(mHeadParts[currentIndex].texture); + else + reader.getZString(mHeadPartsFemale[currentIndex].texture); // TODO: check TES4 + } + else if (curr_part == 1) // body part + { + if (isMale) + reader.getZString(mBodyPartsMale[currentIndex].texture); + else + reader.getZString(mBodyPartsFemale[currentIndex].texture); + } + else + reader.skipSubRecordData(); // FIXME TES5 + + break; + } + // + case ESM4::SUB_NAM1: // start marker body data /* 4 */ + { + + if (isFO3 || isFONV) + { + curr_part = 1; // body part + + mBodyPartsMale.resize(4); + mBodyPartsFemale.resize(4); + } + else if (isTES4) + { + curr_part = 1; // body part + + mBodyPartsMale.resize(5); // 0 = upper body, 1 = legs, 2 = hands, 3 = feet, 4 = tail + mBodyPartsFemale.resize(5); // 0 = upper body, 1 = legs, 2 = hands, 3 = feet, 4 = tail + } + else // TES5 + curr_part = 2; // for TES5 NAM1 indicates the start of EGT model + + if (isTES4) + currentIndex = 4; // FIXME: argonian tail mesh without preceeding INDX + else + currentIndex = 0xffffffff; + + break; + } + case ESM4::SUB_MNAM: isMale = true; break; /* 2, 5, 7 */ + case ESM4::SUB_FNAM: isMale = false; break; /* 3, 6, 8 */ + // + case ESM4::SUB_HNAM: + { + std::size_t numHairChoices = subHdr.dataSize / sizeof(FormId); + mHairChoices.resize(numHairChoices); + for (unsigned int i = 0; i < numHairChoices; ++i) + reader.get(mHairChoices.at(i)); + + break; + } + case ESM4::SUB_ENAM: + { + std::size_t numEyeChoices = subHdr.dataSize / sizeof(FormId); + mEyeChoices.resize(numEyeChoices); + for (unsigned int i = 0; i < numEyeChoices; ++i) + reader.get(mEyeChoices.at(i)); + + break; + } + case ESM4::SUB_FGGS: + { + if (isMale || isTES4) + { + mSymShapeModeCoefficients.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymShapeModeCoefficients.at(i)); + } + else + { + mSymShapeModeCoeffFemale.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymShapeModeCoeffFemale.at(i)); + } + + break; + } + case ESM4::SUB_FGGA: + { + if (isMale || isTES4) + { + mAsymShapeModeCoefficients.resize(30); + for (std::size_t i = 0; i < 30; ++i) + reader.get(mAsymShapeModeCoefficients.at(i)); + } + else + { + mAsymShapeModeCoeffFemale.resize(30); + for (std::size_t i = 0; i < 30; ++i) + reader.get(mAsymShapeModeCoeffFemale.at(i)); + } + + break; + } + case ESM4::SUB_FGTS: + { + if (isMale || isTES4) + { + mSymTextureModeCoefficients.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymTextureModeCoefficients.at(i)); + } + else + { + mSymTextureModeCoeffFemale.resize(50); + for (std::size_t i = 0; i < 50; ++i) + reader.get(mSymTextureModeCoeffFemale.at(i)); + } + + break; + } + // + case ESM4::SUB_SNAM: //skipping...2 // only in TES4? + { + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XNAM: + { + FormId race; + std::int32_t adjustment; + reader.get(race); + reader.get(adjustment); + mDisposition[race] = adjustment; + + break; + } + case ESM4::SUB_VNAM: + { + if (subHdr.dataSize == 8) // TES4 + { + reader.get(mVNAM[0]); // For TES4 seems to be 2 race formids + reader.get(mVNAM[1]); + } + else if (subHdr.dataSize == 4) // TES5 + { + // equipment type flags meant to be uint32 ??? GLOB reference? shows up in + // SCRO in sript records and CTDA in INFO records + uint32_t dummy; + reader.get(dummy); + } + else + { + reader.skipSubRecordData(); + std::cout << "RACE " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + } + + break; + } + // + case ESM4::SUB_ANAM: // TES5 + { + if (isMale) + reader.getZString(mModelMale); + else + reader.getZString(mModelFemale); + break; + } + case ESM4::SUB_KSIZ: reader.get(mNumKeywords); break; + case ESM4::SUB_KWDA: + { + std::uint32_t formid; + for (unsigned int i = 0; i < mNumKeywords; ++i) + reader.getFormId(formid); + break; + } + // + case ESM4::SUB_WNAM: // ARMO FormId + { + reader.getFormId(mSkin); + //std::cout << mEditorId << " skin " << formIdToString(mSkin) << std::endl; // FIXME + break; + } + case ESM4::SUB_BODT: // body template + { + reader.get(mBodyTemplate.bodyPart); + reader.get(mBodyTemplate.flags); + reader.get(mBodyTemplate.unknown1); // probably padding + reader.get(mBodyTemplate.unknown2); // probably padding + reader.get(mBodyTemplate.unknown3); // probably padding + reader.get(mBodyTemplate.type); + + break; + } + case ESM4::SUB_BOD2: // TES5 + { + reader.get(mBodyTemplate.bodyPart); + mBodyTemplate.flags = 0; + mBodyTemplate.unknown1 = 0; // probably padding + mBodyTemplate.unknown2 = 0; // probably padding + mBodyTemplate.unknown3 = 0; // probably padding + reader.get(mBodyTemplate.type); + + break; + } + case ESM4::SUB_HEAD: // TES5 + { + FormId formId; + reader.getFormId(formId); + + // FIXME: no order? head, mouth, eyes, brow, hair + if (isMale) + mHeadPartIdsMale[currentIndex] = formId; + else + mHeadPartIdsFemale[currentIndex] = formId; + + //std::cout << mEditorId << (isMale ? " male head " : " female head ") + //<< formIdToString(formId) << " " << currentIndex << std::endl; // FIXME + + break; + } + case ESM4::SUB_NAM3: // start of hkx model + { + curr_part = 3; // for TES5 NAM3 indicates the start of hkx model + + break; + } + // Not sure for what this is used - maybe to indicate which slots are in use? e.g.: + // + // ManakinRace HEAD + // ManakinRace Hair + // ManakinRace BODY + // ManakinRace Hands + // ManakinRace Forearms + // ManakinRace Amulet + // ManakinRace Ring + // ManakinRace Feet + // ManakinRace Calves + // ManakinRace SHIELD + // ManakinRace + // ManakinRace LongHair + // ManakinRace Circlet + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace DecapitateHead + // ManakinRace Decapitate + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace + // ManakinRace FX0 + case ESM4::SUB_NAME: // TES5 biped object names (x32) + { + std::string name; + reader.getZString(name); + //std::cout << mEditorId << " " << name << std::endl; + + break; + } + case ESM4::SUB_MTNM: // movement type + case ESM4::SUB_ATKD: // attack data + case ESM4::SUB_ATKE: // attach event + case ESM4::SUB_GNAM: // body part data + case ESM4::SUB_NAM4: // material type + case ESM4::SUB_NAM5: // unarmed impact? + case ESM4::SUB_LNAM: // close loot sound + case ESM4::SUB_QNAM: // equipment slot formid + case ESM4::SUB_HCLF: // default hair colour + case ESM4::SUB_UNES: // unarmed equipment slot formid + case ESM4::SUB_TINC: + case ESM4::SUB_TIND: + case ESM4::SUB_TINI: + case ESM4::SUB_TINL: + case ESM4::SUB_TINP: + case ESM4::SUB_TINT: + case ESM4::SUB_TINV: + case ESM4::SUB_TIRS: + case ESM4::SUB_PHWT: + case ESM4::SUB_AHCF: + case ESM4::SUB_AHCM: + case ESM4::SUB_MPAI: + case ESM4::SUB_MPAV: + case ESM4::SUB_DFTF: + case ESM4::SUB_DFTM: + case ESM4::SUB_FLMV: + case ESM4::SUB_FTSF: + case ESM4::SUB_FTSM: + case ESM4::SUB_MTYP: + case ESM4::SUB_NAM7: + case ESM4::SUB_NAM8: + case ESM4::SUB_PHTN: + case ESM4::SUB_RNAM: + case ESM4::SUB_RNMV: + case ESM4::SUB_RPRF: + case ESM4::SUB_RPRM: + case ESM4::SUB_SNMV: + case ESM4::SUB_SPCT: + case ESM4::SUB_SPED: + case ESM4::SUB_SWMV: + case ESM4::SUB_WKMV: + // + case ESM4::SUB_YNAM: // FO3 + case ESM4::SUB_NAM2: // FO3 + case ESM4::SUB_VTCK: // FO3 + case ESM4::SUB_MODT: // FO3 + case ESM4::SUB_MODD: // FO3 + case ESM4::SUB_ONAM: // FO3 + { + + //std::cout << "RACE " << ESM::printName(subHdr.typeId) << " skipping..." << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::RACE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Race::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Race::blank() +//{ +//} diff --git a/components/esm4/loadrace.hpp b/components/esm4/loadrace.hpp new file mode 100644 index 0000000000..9166282e17 --- /dev/null +++ b/components/esm4/loadrace.hpp @@ -0,0 +1,172 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_RACE +#define ESM4_RACE + +#include +#include +#include +#include + +#include "formid.hpp" +#include "actor.hpp" // AttributeValues, BodyTemplate + +namespace ESM4 +{ + class Reader; + class Writer; + typedef std::uint32_t FormId; + + struct Race + { +#pragma pack(push, 1) + struct Data + { + std::uint8_t flags; // 0x01 = not playable, 0x02 = not male, 0x04 = not female, ?? = fixed + }; +#pragma pack(pop) + + enum SkillIndex + { + Skill_Armorer = 0x0C, + Skill_Athletics = 0x0D, + Skill_Blade = 0x0E, + Skill_Block = 0x0F, + Skill_Blunt = 0x10, + Skill_HandToHand = 0x11, + Skill_HeavyArmor = 0x12, + Skill_Alchemy = 0x13, + Skill_Alteration = 0x14, + Skill_Conjuration = 0x15, + Skill_Destruction = 0x16, + Skill_Illusion = 0x17, + Skill_Mysticism = 0x18, + Skill_Restoration = 0x19, + Skill_Acrobatics = 0x1A, + Skill_LightArmor = 0x1B, + Skill_Marksman = 0x1C, + Skill_Mercantile = 0x1D, + Skill_Security = 0x1E, + Skill_Sneak = 0x1F, + Skill_Speechcraft = 0x20, + Skill_None = 0xFF, + Skill_Unknown = 0x00 + }; + + enum HeadPartIndex // TES4 + { + Head = 0, + EarMale = 1, + EarFemale = 2, + Mouth = 3, + TeethLower = 4, + TeethUpper = 5, + Tongue = 6, + EyeLeft = 7, // no texture + EyeRight = 8, // no texture + NumHeadParts = 9 + }; + + enum BodyPartIndex // TES4 + { + UpperBody = 0, + LowerBody = 1, + Hands = 2, + Feet = 3, + Tail = 4, + NumBodyParts = 5 + }; + + struct BodyPart + { + std::string mesh; // can be empty for arms, hands, etc + std::string texture; // can be empty e.g. eye left, eye right + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + bool mIsTES5; + + std::string mEditorId; + std::string mFullName; + std::string mDesc; + std::string mModelMale; // TES5 skeleton (in TES4 skeleton is found in npc_) + std::string mModelFemale; // TES5 skeleton (in TES4 skeleton is found in npc_) + + AttributeValues mAttribMale; + AttributeValues mAttribFemale; + std::map mSkillBonus; + + // DATA + float mHeightMale = 1.0f; + float mHeightFemale = 1.0f; + float mWeightMale = 1.0f; + float mWeightFemale = 1.0f; + std::uint32_t mRaceFlags; // 0x0001 = playable? + + std::vector mHeadParts; // see HeadPartIndex + std::vector mHeadPartsFemale; // see HeadPartIndex + + std::vector mBodyPartsMale; // see BodyPartIndex + std::vector mBodyPartsFemale; // see BodyPartIndex + + std::vector mEyeChoices; // texture only + std::vector mHairChoices; // not for TES5 + + float mFaceGenMainClamp; + float mFaceGenFaceClamp; + std::vector mSymShapeModeCoefficients; // should be 50 + std::vector mSymShapeModeCoeffFemale; // should be 50 + std::vector mAsymShapeModeCoefficients; // should be 30 + std::vector mAsymShapeModeCoeffFemale; // should be 30 + std::vector mSymTextureModeCoefficients; // should be 50 + std::vector mSymTextureModeCoeffFemale; // should be 50 + + std::map mDisposition; // race adjustments + std::vector mBonusSpells; // race ability/power + std::array mVNAM; // don't know what these are; 1 or 2 RACE FormIds + std::array mDefaultHair; // male/female (HAIR FormId for TES4) + + std::uint32_t mNumKeywords; + + FormId mSkin; // TES5 + BodyTemplate mBodyTemplate; // TES5 + + // FIXME: there's no fixed order? + // head, mouth, eyes, brow, hair + std::vector mHeadPartIdsMale; // TES5 + std::vector mHeadPartIdsFemale; // TES5 + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_RACE diff --git a/components/esm4/loadrefr.cpp b/components/esm4/loadrefr.cpp new file mode 100644 index 0000000000..91c10c564e --- /dev/null +++ b/components/esm4/loadrefr.cpp @@ -0,0 +1,322 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadrefr.hpp" + +#include +#include // FIXME: debug only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Reference::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + mParent = reader.currCell(); // NOTE: only for persistent refs? + + // TODO: Let the engine apply this? Saved games? + //mInitiallyDisabled = ((mFlags & ESM4::Rec_Disabled) != 0) ? true : false; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + FormId mid; + FormId sid; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_NAME: + { + reader.getFormId(mBaseObj); +#if 0 + if (mFlags & ESM4::Rec_Disabled) + std::cout << "REFR disable at start " << formIdToString(mFormId) << + " baseobj " << formIdToString(mBaseObj) << + " " << (mEditorId.empty() ? "" : mEditorId) << std::endl; // FIXME +#endif + //if (mBaseObj == 0x20) // e.g. FO3 mFormId == 0x0007E90F + //if (mBaseObj == 0x17) + //std::cout << mEditorId << std::endl; + break; + } + case ESM4::SUB_DATA: reader.get(mPlacement); break; + case ESM4::SUB_XSCL: reader.get(mScale); break; + case ESM4::SUB_XOWN: reader.getFormId(mOwner); break; + case ESM4::SUB_XGLB: reader.getFormId(mGlobal); break; + case ESM4::SUB_XRNK: reader.get(mFactionRank); break; + case ESM4::SUB_XESP: + { + reader.get(mEsp); + reader.adjustFormId(mEsp.parent); + //std::cout << "REFR parent: " << formIdToString(mEsp.parent) << " ref " << formIdToString(mFormId) + //<< ", 0x" << std::hex << (mEsp.flags & 0xff) << std::endl;// FIXME + break; + } + case ESM4::SUB_XTEL: + { + reader.getFormId(mDoor.destDoor); + reader.get(mDoor.destPos); + if (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || isFONV) + reader.get(mDoor.flags); // not in Obvlivion + //std::cout << "REFR dest door: " << formIdToString(mDoor.destDoor) << std::endl;// FIXME + break; + } + case ESM4::SUB_XSED: + { + // 1 or 4 bytes + if (subHdr.dataSize == 1) + { + uint8_t data; + reader.get(data); + //std::cout << "REFR XSED " << std::hex << (int)data << std::endl; + break; + } + else if (subHdr.dataSize == 4) + { + uint32_t data; + reader.get(data); + //std::cout << "REFR XSED " << std::hex << (int)data << std::endl; + break; + } + + //std::cout << "REFR XSED dataSize: " << subHdr.dataSize << std::endl;// FIXME + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XLOD: + { + // 12 bytes + if (subHdr.dataSize == 12) + { + float data, data2, data3; + reader.get(data); + reader.get(data2); + reader.get(data3); + //bool hasVisibleWhenDistantFlag = (mFlags & 0x00008000) != 0; // currently unused + // some are trees, e.g. 000E03B6, mBaseObj 00022F32, persistent, visible when distant + // some are doors, e.g. 000270F7, mBaseObj 000CD338, persistent, initially disabled + // (this particular one is an Oblivion Gate) + //std::cout << "REFR XLOD " << std::hex << (int)data << " " << (int)data2 << " " << (int)data3 << std::endl; + break; + } + //std::cout << "REFR XLOD dataSize: " << subHdr.dataSize << std::endl;// FIXME + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XACT: + { + if (subHdr.dataSize == 4) + { + uint32_t data; + reader.get(data); + //std::cout << "REFR XACT " << std::hex << (int)data << std::endl; + break; + } + + //std::cout << "REFR XACT dataSize: " << subHdr.dataSize << std::endl;// FIXME + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XRTM: // formId + { + // seems like another ref, e.g. 00064583 has base object 00000034 which is "XMarkerHeading" + // e.g. some are doors (prob. quest related) + // MS94OblivionGateRef XRTM : 00097C88 + // MQ11SkingradGate XRTM : 00064583 + // MQ11ChorrolGate XRTM : 00188770 + // MQ11LeyawiinGate XRTM : 0018AD7C + // MQ11AnvilGate XRTM : 0018D452 + // MQ11BravilGate XRTM : 0018AE1B + // e.g. some are XMarkerHeading + // XRTM : 000A4DD7 in OblivionRDCavesMiddleA05 (maybe spawn points?) + FormId marker; + reader.getFormId(marker); + //std::cout << "REFR " << mEditorId << " XRTM : " << formIdToString(marker) << std::endl;// FIXME + break; + } + case ESM4::SUB_TNAM: //reader.get(mMapMarker); break; + { + if (subHdr.dataSize != sizeof(mMapMarker)) + //reader.skipSubRecordData(); // FIXME: FO3 + reader.getFormId(mid); + else + reader.get(mMapMarker); // TES4 + + break; + } + case ESM4::SUB_XMRK: mIsMapMarker = true; break; // all have mBaseObj 0x00000010 "MapMarker" + case ESM4::SUB_FNAM: + { + //std::cout << "REFR " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_XTRG: // formId + { + reader.getFormId(mTargetRef); + //std::cout << "REFR XRTG : " << formIdToString(id) << std::endl;// FIXME + break; + } + case ESM4::SUB_CNAM: reader.getFormId(mAudioLocation); break; // FONV + case ESM4::SUB_XRDO: // FO3 + { + reader.get(mRadio.rangeRadius); + reader.get(mRadio.broadcastRange); + reader.get(mRadio.staticPercentage); + reader.getFormId(mRadio.posReference); + + break; + } + case ESM4::SUB_SCRO: // FO3 + { + reader.getFormId(sid); + //if (mFormId == 0x0016b74B) + //std::cout << "REFR SCRO : " << formIdToString(sid) << std::endl;// FIXME + break; + } + case ESM4::SUB_XLOC: + { + mIsLocked = true; + std::int8_t dummy; // FIXME: very poor code + + reader.get(mLockLevel); + reader.get(dummy); + reader.get(dummy); + reader.get(dummy); + reader.getFormId(mKey); + reader.get(dummy); // flag? + reader.get(dummy); + reader.get(dummy); + reader.get(dummy); + if (subHdr.dataSize == 16) + reader.skipSubRecordData(4); // Oblivion (sometimes), flag? + else if (subHdr.dataSize == 20) // Skyrim, FO3 + reader.skipSubRecordData(8); // flag? + + break; + } + // lighting + case ESM4::SUB_LNAM: // lighting template formId + case ESM4::SUB_XLIG: // struct, FOV, fade, etc + case ESM4::SUB_XEMI: // LIGH formId + case ESM4::SUB_XRDS: // Radius or Radiance + case ESM4::SUB_XRGB: + case ESM4::SUB_XRGD: // tangent data? + case ESM4::SUB_XALP: // alpha cutoff + // + case ESM4::SUB_XPCI: // formId + case ESM4::SUB_XLCM: + case ESM4::SUB_XCNT: + case ESM4::SUB_ONAM: + case ESM4::SUB_VMAD: + case ESM4::SUB_XPRM: + case ESM4::SUB_INAM: + case ESM4::SUB_PDTO: + case ESM4::SUB_SCHR: + case ESM4::SUB_SCTX: + case ESM4::SUB_XAPD: + case ESM4::SUB_XAPR: + case ESM4::SUB_XCVL: + case ESM4::SUB_XCZA: + case ESM4::SUB_XCZC: + case ESM4::SUB_XEZN: + case ESM4::SUB_XFVC: + case ESM4::SUB_XHTW: + case ESM4::SUB_XIS2: + case ESM4::SUB_XLCN: + case ESM4::SUB_XLIB: + case ESM4::SUB_XLKR: + case ESM4::SUB_XLRM: + case ESM4::SUB_XLRT: + case ESM4::SUB_XLTW: + case ESM4::SUB_XMBO: + case ESM4::SUB_XMBP: + case ESM4::SUB_XMBR: + case ESM4::SUB_XNDP: + case ESM4::SUB_XOCP: + case ESM4::SUB_XPOD: + case ESM4::SUB_XPPA: + case ESM4::SUB_XPRD: + case ESM4::SUB_XPWR: + case ESM4::SUB_XRMR: + case ESM4::SUB_XSPC: + case ESM4::SUB_XTNM: + case ESM4::SUB_XTRI: + case ESM4::SUB_XWCN: + case ESM4::SUB_XWCU: + case ESM4::SUB_XATR: // Dawnguard only? + case ESM4::SUB_XHLT: // Unofficial Oblivion Patch + case ESM4::SUB_XCHG: // thievery.exp + case ESM4::SUB_XHLP: // FO3 + case ESM4::SUB_XAMT: // FO3 + case ESM4::SUB_XAMC: // FO3 + case ESM4::SUB_XRAD: // FO3 + case ESM4::SUB_XIBS: // FO3 + case ESM4::SUB_XORD: // FO3 + case ESM4::SUB_XCLP: // FO3 + case ESM4::SUB_SCDA: // FO3 + case ESM4::SUB_RCLR: // FO3 + case ESM4::SUB_BNAM: // FONV + case ESM4::SUB_MMRK: // FONV + case ESM4::SUB_MNAM: // FONV + case ESM4::SUB_NNAM: // FONV + case ESM4::SUB_XATO: // FONV + case ESM4::SUB_SCRV: // FONV + case ESM4::SUB_SCVR: // FONV + case ESM4::SUB_SLSD: // FONV + case ESM4::SUB_XSRF: // FONV + case ESM4::SUB_XSRD: // FONV + case ESM4::SUB_WMI1: // FONV + case ESM4::SUB_XLRL: // Unofficial Skyrim Patch + { + //if (mFormId == 0x0007e90f) // XPRM XPOD + //if (mBaseObj == 0x17) //XPRM XOCP occlusion plane data XMBO bound half extents + //std::cout << "REFR " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::REFR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } + //if (mFormId == 0x0016B74B) // base is TACT vCasinoUltraLuxeRadio in cell ULCasino + //std::cout << "REFR SCRO " << formIdToString(sid) << std::endl; +} + +//void ESM4::Reference::save(ESM4::Writer& writer) const +//{ +//} + +void ESM4::Reference::blank() +{ +} diff --git a/components/esm4/loadrefr.hpp b/components/esm4/loadrefr.hpp new file mode 100644 index 0000000000..0494dcb074 --- /dev/null +++ b/components/esm4/loadrefr.hpp @@ -0,0 +1,116 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_REFR_H +#define ESM4_REFR_H + +#include + +#include "reference.hpp" // FormId, Placement, EnableParent + +namespace ESM4 +{ + class Reader; + class Writer; + + enum MapMarkerType + { + Map_None = 0x00, // ? + Map_Camp = 0x01, + Map_Cave = 0x02, + Map_City = 0x03, + Map_ElvenRuin = 0x04, + Map_FortRuin = 0x05, + Map_Mine = 0x06, + Map_Landmark = 0x07, + Map_Tavern = 0x08, + Map_Settlement = 0x09, + Map_DaedricShrine = 0x0A, + Map_OblivionGate = 0x0B, + Map_Unknown = 0x0C // ? (door icon) + }; + + struct TeleportDest + { + FormId destDoor; + Placement destPos; + std::uint32_t flags; // 0x01 no alarm (only in TES5) + }; + + struct RadioStationData + { + float rangeRadius; + // 0 radius, 1 everywhere, 2 worldspace and linked int, 3 linked int, 4 current cell only + std::uint32_t broadcastRange; + float staticPercentage; + FormId posReference; // only used if broadcastRange == 0 + }; + + struct Reference + { + FormId mParent; // cell FormId (currently persistent refs only), from the loading sequence + // NOTE: for exterior cells it will be the dummy cell FormId + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + FormId mBaseObj; + + Placement mPlacement; + float mScale = 1.0f; + FormId mOwner; + FormId mGlobal; + std::int32_t mFactionRank = -1; + + bool mInitiallyDisabled; // TODO may need to check mFlags & 0x800 (initially disabled) + bool mIsMapMarker; + std::uint16_t mMapMarker; + + EnableParent mEsp; + + std::uint32_t mCount = 1; // only if > 1 + + FormId mAudioLocation; + + RadioStationData mRadio; + + TeleportDest mDoor; + bool mIsLocked; + std::int8_t mLockLevel; + FormId mKey; + + FormId mTargetRef; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + void blank(); + }; +} + +#endif // ESM4_REFR_H diff --git a/components/esm4/loadregn.cpp b/components/esm4/loadregn.cpp new file mode 100644 index 0000000000..b57ce81aa0 --- /dev/null +++ b/components/esm4/loadregn.cpp @@ -0,0 +1,153 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadregn.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include + +//#include // FIXME: debug only +//#include "formid.hpp" + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Region::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_RCLR: reader.get(mColour); break; + case ESM4::SUB_WNAM: reader.getFormId(mWorldId); break; + case ESM4::SUB_ICON: reader.getZString(mShader); break; + case ESM4::SUB_RPLI: reader.get(mEdgeFalloff); break; + case ESM4::SUB_RPLD: + { + mRPLD.resize(subHdr.dataSize/sizeof(std::uint32_t)); + for (std::vector::iterator it = mRPLD.begin(); it != mRPLD.end(); ++it) + { + reader.get(*it); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "RPLD: 0x" << std::hex << *it << std::endl; +#endif + } + + break; + } + case ESM4::SUB_RDAT: reader.get(mData); break; + case ESM4::SUB_RDMP: + { + assert(mData.type == RDAT_Map && "REGN unexpected data type"); + reader.getLocalizedString(mMapName); break; + } + // FO3 only 2: DemoMegatonSound and DC01 (both 0 RDMD) + // FONV none + case ESM4::SUB_RDMD: // music type; 0 default, 1 public, 2 dungeon + { +#if 0 + int dummy; + reader.get(dummy); + std::cout << "REGN " << mEditorId << " " << dummy << std::endl; +#else + reader.skipSubRecordData(); +#endif + break; + } + case ESM4::SUB_RDMO: // not seen in FO3/FONV? + { + //std::cout << "REGN " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + case ESM4::SUB_RDSD: // Possibly the same as RDSA + { + //assert(mData.type == RDAT_Map && "REGN unexpected data type"); + if (mData.type != RDAT_Sound) + throw std::runtime_error("ESM4::REGN::load - unexpected data type " + + ESM::printName(subHdr.typeId)); + + std::size_t numSounds = subHdr.dataSize / sizeof(RegionSound); + mSounds.resize(numSounds); + for (std::size_t i = 0; i < numSounds; ++i) + reader.get(mSounds.at(i)); + + break; + } + case ESM4::SUB_RDGS: // Only in Oblivion? (ToddTestRegion1) // formId + case ESM4::SUB_RDSA: + case ESM4::SUB_RDWT: // formId + case ESM4::SUB_RDOT: // formId + case ESM4::SUB_RDID: // FONV + case ESM4::SUB_RDSB: // FONV + case ESM4::SUB_RDSI: // FONV + case ESM4::SUB_NVMI: // TES5 + { + //RDAT skipping... following is a map + //RDMP skipping... map name + // + //RDAT skipping... following is weather + //RDWT skipping... weather data + // + //RDAT skipping... following is sound + //RDMD skipping... unknown, maybe music data + // + //RDSD skipping... unknown, maybe sound data + // + //RDAT skipping... following is grass + //RDGS skipping... unknown, maybe grass + + //std::cout << "REGN " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); // FIXME: process the subrecord rather than skip + break; + } + default: + throw std::runtime_error("ESM4::REGN::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Region::save(ESM4::Writer& writer) const +//{ +//} + +void ESM4::Region::blank() +{ +} diff --git a/components/esm4/loadregn.hpp b/components/esm4/loadregn.hpp new file mode 100644 index 0000000000..9b7962ba3c --- /dev/null +++ b/components/esm4/loadregn.hpp @@ -0,0 +1,95 @@ +/* + Copyright (C) 2015-2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_REGN_H +#define ESM4_REGN_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Region + { + enum RegionDataType + { + RDAT_None = 0x00, + RDAT_Objects = 0x02, + RDAT_Weather = 0x03, + RDAT_Map = 0x04, + RDAT_Landscape = 0x05, + RDAT_Grass = 0x06, + RDAT_Sound = 0x07, + RDAT_Imposter = 0x08 + }; + +#pragma pack(push, 1) + struct RegionData + { + std::uint32_t type; + std::uint8_t flag; + std::uint8_t priority; + std::uint16_t unknown; + }; + + struct RegionSound + { + FormId sound; + std::uint32_t flags; // 0 pleasant, 1 cloudy, 2 rainy, 3 snowy + std::uint32_t chance; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::uint32_t mColour; // RGBA + FormId mWorldId; // worldspace formid + + std::string mShader; //?? ICON + std::string mMapName; + + std::uint32_t mEdgeFalloff; + std::vector mRPLD; // unknown, point data? + + RegionData mData; + std::vector mSounds; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + void blank(); + }; +} + +#endif // ESM4_REGN_H diff --git a/components/esm4/loadroad.cpp b/components/esm4/loadroad.cpp new file mode 100644 index 0000000000..b131811c64 --- /dev/null +++ b/components/esm4/loadroad.cpp @@ -0,0 +1,111 @@ +/* + Copyright (C) 2020 - 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadroad.hpp" + +#include +//#include // FIXME: for debugging only + +#include "formid.hpp" // FIXME: for workaround +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Road::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + mParent = reader.currWorld(); + + mEditorId = formIdToString(mFormId); // FIXME: quick workaround to use existing code + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_PGRP: + { + std::size_t numNodes = subHdr.dataSize / sizeof(PGRP); + + mNodes.resize(numNodes); + for (std::size_t i = 0; i < numNodes; ++i) + { + reader.get(mNodes.at(i)); + } + + break; + } + case ESM4::SUB_PGRR: + { + static PGRR link; + static RDRP linkPt; + + for (std::size_t i = 0; i < mNodes.size(); ++i) + { + for (std::size_t j = 0; j < mNodes[i].numLinks; ++j) + { + link.startNode = std::int16_t(i); + + reader.get(linkPt); + + // FIXME: instead of looping each time, maybe use a map? + bool found = false; + for (std::size_t k = 0; k < mNodes.size(); ++k) + { + if (linkPt.x != mNodes[k].x || linkPt.y != mNodes[k].y || linkPt.z != mNodes[k].z) + continue; + else + { + link.endNode = std::int16_t(k); + mLinks.push_back(link); + + found = true; + break; + } + } + + if (!found) + throw std::runtime_error("ESM4::ROAD::PGRR - Unknown link point " + + std::to_string(j) + "at node " + std::to_string(i) + "."); + } + } + + break; + } + default: + throw std::runtime_error("ESM4::ROAD::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Road::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Road::blank() +//{ +//} diff --git a/components/esm4/loadroad.hpp b/components/esm4/loadroad.hpp new file mode 100644 index 0000000000..33fc332643 --- /dev/null +++ b/components/esm4/loadroad.hpp @@ -0,0 +1,86 @@ +/* + Copyright (C) 2020 - 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_ROAD_H +#define ESM4_ROAD_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Road + { +#pragma pack(push, 1) + // FIXME: duplicated from PGRD + struct PGRP + { + float x; + float y; + float z; + std::uint8_t numLinks; + std::uint8_t unknown1; + std::uint16_t unknown2; + }; + + // FIXME: duplicated from PGRD + struct PGRR + { + std::int16_t startNode; + std::int16_t endNode; + }; + + struct RDRP + { + float x; + float y; + float z; + }; +#pragma pack(pop) + FormId mParent; // world FormId, from the loading sequence + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::vector mNodes; + std::vector mLinks; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_ROAD_H diff --git a/components/esm4/loadsbsp.cpp b/components/esm4/loadsbsp.cpp new file mode 100644 index 0000000000..79ad846414 --- /dev/null +++ b/components/esm4/loadsbsp.cpp @@ -0,0 +1,65 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadsbsp.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::SubSpace::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_DNAM: + { + reader.get(mDimension.x); + reader.get(mDimension.y); + reader.get(mDimension.z); + break; + } + default: + throw std::runtime_error("ESM4::SBSP::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::SubSpace::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::SubSpace::blank() +//{ +//} diff --git a/components/esm4/loadsbsp.hpp b/components/esm4/loadsbsp.hpp new file mode 100644 index 0000000000..8e949cc6ad --- /dev/null +++ b/components/esm4/loadsbsp.hpp @@ -0,0 +1,61 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SBSP_H +#define ESM4_SBSP_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + + struct SubSpace + { + struct Dimension + { + float x; + float y; + float z; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + Dimension mDimension; + + virtual void load(Reader& reader); + //virtual void save(Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SBSP_H diff --git a/components/esm4/loadscol.cpp b/components/esm4/loadscol.cpp new file mode 100644 index 0000000000..0dd206a08f --- /dev/null +++ b/components/esm4/loadscol.cpp @@ -0,0 +1,75 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#include "loadscol.hpp" + +#include +#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::StaticCollection::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_OBND: + case ESM4::SUB_MODL: + case ESM4::SUB_MODT: + case ESM4::SUB_ONAM: + case ESM4::SUB_DATA: + { + //std::cout << "SCOL " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + std::cout << "SCOL " << ESM::printName(subHdr.typeId) << " skipping..." + << subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + //throw std::runtime_error("ESM4::SCOL::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::StaticCollection::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::StaticCollection::blank() +//{ +//} diff --git a/components/esm4/loadscol.hpp b/components/esm4/loadscol.hpp new file mode 100644 index 0000000000..6a0005699d --- /dev/null +++ b/components/esm4/loadscol.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_SCOL_H +#define ESM4_SCOL_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct StaticCollection + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SCOL_H diff --git a/components/esm4/loadscpt.cpp b/components/esm4/loadscpt.cpp new file mode 100644 index 0000000000..4c606e3351 --- /dev/null +++ b/components/esm4/loadscpt.cpp @@ -0,0 +1,164 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadscpt.hpp" + +#include +#include // FIXME: debugging only +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Script::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + static ScriptLocalVariableData localVar; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: + { + reader.getZString(mEditorId); + break; + } + case ESM4::SUB_SCHR: + { + // For debugging only +#if 0 + unsigned char mDataBuf[256/*bufSize*/]; + reader.get(&mDataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + for (unsigned int i = 0; i < subHdr.dataSize; ++i) + { + //if (mDataBuf[i] > 64 && mDataBuf[i] < 91) + //ss << (char)(mDataBuf[i]) << " "; + //else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]); + if ((i & 0x000f) == 0xf) + ss << "\n"; + else if (i < 256/*bufSize*/-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + reader.get(mScript.scriptHeader); +#endif + break; + } + case ESM4::SUB_SCTX: reader.getString(mScript.scriptSource); + //if (mEditorId == "CTrapLogs01SCRIPT") + //std::cout << mScript.scriptSource << std::endl; + break; + case ESM4::SUB_SCDA: // compiled script data + { + // For debugging only +#if 0 + if (subHdr.dataSize >= 4096) + { + std::cout << "Skipping " << mEditorId << std::endl; + reader.skipSubRecordData(); + break; + } + + std::cout << mEditorId << std::endl; + + unsigned char mDataBuf[4096/*bufSize*/]; + reader.get(&mDataBuf[0], subHdr.dataSize); + + std::ostringstream ss; + for (unsigned int i = 0; i < subHdr.dataSize; ++i) + { + //if (mDataBuf[i] > 64 && mDataBuf[i] < 91) + //ss << (char)(mDataBuf[i]) << " "; + //else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]); + if ((i & 0x000f) == 0xf) + ss << "\n"; + else if (i < 4096/*bufSize*/-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +#else + reader.skipSubRecordData(); +#endif + break; + } + case ESM4::SUB_SCRO: reader.getFormId(mScript.globReference); break; + case ESM4::SUB_SLSD: + { + localVar.clear(); + reader.get(localVar.index); + reader.get(localVar.unknown1); + reader.get(localVar.unknown2); + reader.get(localVar.unknown3); + reader.get(localVar.type); + reader.get(localVar.unknown4); + // WARN: assumes SCVR will follow immediately + + break; + } + case ESM4::SUB_SCVR: // assumed always pair with SLSD + { + reader.getZString(localVar.variableName); + + mScript.localVarData.push_back(localVar); + + break; + } + case ESM4::SUB_SCRV: + { + std::uint32_t index; + reader.get(index); + + mScript.localRefVarIndex.push_back(index); + + break; + } + default: + //std::cout << "SCPT " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + //reader.skipSubRecordData(); + //break; + throw std::runtime_error("ESM4::SCPT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Script::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Script::blank() +//{ +//} diff --git a/components/esm4/loadscpt.hpp b/components/esm4/loadscpt.hpp new file mode 100644 index 0000000000..dbf36aa367 --- /dev/null +++ b/components/esm4/loadscpt.hpp @@ -0,0 +1,56 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SCPT_H +#define ESM4_SCPT_H + +#include + +#include "formid.hpp" +#include "script.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Script + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + ScriptDefinition mScript; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SCPT_H diff --git a/components/esm4/loadscrl.cpp b/components/esm4/loadscrl.cpp new file mode 100644 index 0000000000..ae084afae3 --- /dev/null +++ b/components/esm4/loadscrl.cpp @@ -0,0 +1,84 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadscrl.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Scroll::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_DATA: + { + reader.get(mData.value); + reader.get(mData.weight); + break; + } + case ESM4::SUB_MODL: reader.getZString(mModel); break; + //case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_OBND: + case ESM4::SUB_CTDA: + case ESM4::SUB_EFID: + case ESM4::SUB_EFIT: + case ESM4::SUB_ETYP: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_MDOB: + case ESM4::SUB_MODT: + case ESM4::SUB_SPIT: + { + //std::cout << "SCRL " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::SCRL::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Scroll::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Scroll::blank() +//{ +//} diff --git a/components/esm4/loadscrl.hpp b/components/esm4/loadscrl.hpp new file mode 100644 index 0000000000..d7872f551e --- /dev/null +++ b/components/esm4/loadscrl.hpp @@ -0,0 +1,65 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SCRL_H +#define ESM4_SCRL_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Scroll + { + struct Data + { + std::uint32_t value; + float weight; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mText; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SCRL_H diff --git a/components/esm4/loadsgst.cpp b/components/esm4/loadsgst.cpp new file mode 100644 index 0000000000..843644fe8b --- /dev/null +++ b/components/esm4/loadsgst.cpp @@ -0,0 +1,100 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadsgst.hpp" + +#include +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::SigilStone::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: + { + if (mFullName.empty()) + { + if (!reader.getZString(mFullName)) + throw std::runtime_error ("SGST FULL data read error"); + } + else + { + // FIXME: should be part of a struct? + std::string scriptEffectName; + if (!reader.getZString(scriptEffectName)) + throw std::runtime_error ("SGST FULL data read error"); + mScriptEffect.push_back(scriptEffectName); + } + break; + } + case ESM4::SUB_DATA: + { + reader.get(mData.uses); + reader.get(mData.value); + reader.get(mData.weight); + break; + } + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_SCIT: + { + reader.get(mEffect); + reader.adjustFormId(mEffect.formId); + break; + } + case ESM4::SUB_MODT: + case ESM4::SUB_EFID: + case ESM4::SUB_EFIT: + { + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::SGST::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::SigilStone::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::SigilStone::blank() +//{ +//} diff --git a/components/esm4/loadsgst.hpp b/components/esm4/loadsgst.hpp new file mode 100644 index 0000000000..aa9b95166b --- /dev/null +++ b/components/esm4/loadsgst.hpp @@ -0,0 +1,72 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SGST_H +#define ESM4_SGST_H + +#include +#include + +#include "effect.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct SigilStone + { + struct Data + { + std::uint8_t uses; + std::uint32_t value; // gold + float weight; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + + float mBoundRadius; + + std::vector mScriptEffect; // FIXME: prob. should be in a struct + FormId mScriptId; + ScriptEffect mEffect; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SGST_H diff --git a/components/esm4/loadslgm.cpp b/components/esm4/loadslgm.cpp new file mode 100644 index 0000000000..53e30fd1da --- /dev/null +++ b/components/esm4/loadslgm.cpp @@ -0,0 +1,76 @@ +/* + Copyright (C) 2016, 2018, 2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadslgm.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::SoulGem::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_DATA: reader.get(mData); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_SOUL: reader.get(mSoul); break; + case ESM4::SUB_SLCP: reader.get(mSoulCapacity); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_NAM0: + case ESM4::SUB_OBND: + { + //std::cout << "SLGM " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::SLGM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::SoulGem::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::SoulGem::blank() +//{ +//} diff --git a/components/esm4/loadslgm.hpp b/components/esm4/loadslgm.hpp new file mode 100644 index 0000000000..893603a1fe --- /dev/null +++ b/components/esm4/loadslgm.hpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SLGM_H +#define ESM4_SLGM_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct SoulGem + { +#pragma pack(push, 1) + struct Data + { + std::uint32_t value; // gold + float weight; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mIcon; // inventory + + float mBoundRadius; + + FormId mScriptId; + std::uint8_t mSoul; // 0 = None, 1 = Petty, 2 = Lesser, 3 = Common, 4 = Greater, 5 = Grand + std::uint8_t mSoulCapacity; // 0 = None, 1 = Petty, 2 = Lesser, 3 = Common, 4 = Greater, 5 = Grand + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SLGM_H diff --git a/components/esm4/loadsndr.cpp b/components/esm4/loadsndr.cpp new file mode 100644 index 0000000000..d01fb0ed74 --- /dev/null +++ b/components/esm4/loadsndr.cpp @@ -0,0 +1,84 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadsndr.hpp" + +#include +//#include // FIXME: for debugging only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::SoundReference::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_CTDA: + { + reader.get(&mTargetCondition, 20); + reader.get(mTargetCondition.runOn); + reader.get(mTargetCondition.reference); + if (mTargetCondition.reference) + reader.adjustFormId(mTargetCondition.reference); + reader.skipSubRecordData(4); // unknown + + break; + } + case ESM4::SUB_GNAM: reader.getFormId(mSoundCategory); break; + case ESM4::SUB_SNAM: reader.getFormId(mSoundId); break; + case ESM4::SUB_ONAM: reader.getFormId(mOutputModel); break; + case ESM4::SUB_ANAM: reader.getZString(mSoundFile); break; + case ESM4::SUB_LNAM: reader.get(mLoopInfo); break; + case ESM4::SUB_BNAM: reader.get(mData); break; + case ESM4::SUB_CNAM: // CRC32 hash + case ESM4::SUB_FNAM: // unknown + { + //std::cout << "SNDR " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::SNDR::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::SoundReference::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::SoundReference::blank() +//{ +//} diff --git a/components/esm4/loadsndr.hpp b/components/esm4/loadsndr.hpp new file mode 100644 index 0000000000..de0ca3eb1a --- /dev/null +++ b/components/esm4/loadsndr.hpp @@ -0,0 +1,83 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SNDR_H +#define ESM4_SNDR_H + +#include +#include + +#include "formid.hpp" +#include "script.hpp" // TargetCondition + +namespace ESM4 +{ + class Reader; + class Writer; + +#pragma pack(push, 1) + struct LoopInfo + { + std::uint16_t flags; + std::uint8_t unknown; + std::uint8_t rumble; + }; + + struct SoundInfo + { + std::int8_t frequencyAdjustment; // %, signed + std::uint8_t frequencyVariance; // % + std::uint8_t priority; // default 128 + std::uint8_t dBVriance; + std::uint16_t staticAttenuation; // divide by 100 to get value in dB + }; +#pragma pack(pop) + + struct SoundReference + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + FormId mSoundCategory; // SNCT + FormId mSoundId; // another SNDR + FormId mOutputModel; // SOPM + + std::string mSoundFile; + LoopInfo mLoopInfo; + SoundInfo mData; + + TargetCondition mTargetCondition; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SNDR_H diff --git a/components/esm4/loadsoun.cpp b/components/esm4/loadsoun.cpp new file mode 100644 index 0000000000..a3358236b0 --- /dev/null +++ b/components/esm4/loadsoun.cpp @@ -0,0 +1,85 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadsoun.hpp" + +#include +//#include // FIXME + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Sound::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FNAM: reader.getZString(mSoundFile); break; + case ESM4::SUB_SNDX: reader.get(mData); break; + case ESM4::SUB_SNDD: + { + if (subHdr.dataSize == 8) + reader.get(&mData, 8); + else + { + reader.get(mData); + reader.get(mExtra); + } + + break; + } + case ESM4::SUB_OBND: // TES5 only + case ESM4::SUB_SDSC: // TES5 only + case ESM4::SUB_ANAM: // FO3 + case ESM4::SUB_GNAM: // FO3 + case ESM4::SUB_HNAM: // FO3 + case ESM4::SUB_RNAM: // FONV + { + //std::cout << "SOUN " << ESM::printName(subHdr.typeId) << " skipping..." + //<< subHdr.dataSize << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::SOUN::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Sound::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Sound::blank() +//{ +//} diff --git a/components/esm4/loadsoun.hpp b/components/esm4/loadsoun.hpp new file mode 100644 index 0000000000..da1d7fbd71 --- /dev/null +++ b/components/esm4/loadsoun.hpp @@ -0,0 +1,98 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_SOUN_H +#define ESM4_SOUN_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Sound + { + enum Flags + { + Flag_RandomFreqShift = 0x0001, + Flag_PlayAtRandom = 0x0002, + Flag_EnvIgnored = 0x0004, + Flag_RandomLocation = 0x0008, + Flag_Loop = 0x0010, + Flag_MenuSound = 0x0020, + Flag_2D = 0x0040, + Flag_360LFE = 0x0080 + }; + +#pragma pack(push, 1) + struct SNDX + { + std::uint8_t minAttenuation; // distance? + std::uint8_t maxAttenuation; // distance? + std::int8_t freqAdjustment; // %, signed + std::uint8_t unknown; // probably padding + std::uint16_t flags; + std::uint16_t unknown2; // probably padding + std::uint16_t staticAttenuation; // divide by 100 to get value in dB + std::uint8_t stopTime; // multiply by 1440/256 to get value in minutes + std::uint8_t startTime; // multiply by 1440/256 to get value in minutes + }; + + struct SoundData + { + std::int16_t attenuationPoint1; + std::int16_t attenuationPoint2; + std::int16_t attenuationPoint3; + std::int16_t attenuationPoint4; + std::int16_t attenuationPoint5; + std::int16_t reverbAttenuationControl; + std::int32_t priority; + std::int32_t x; + std::int32_t y; + }; +#pragma pack(pop) + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::string mSoundFile; + SNDX mData; + SoundData mExtra; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_SOUN_H diff --git a/components/esm4/loadstat.cpp b/components/esm4/loadstat.cpp new file mode 100644 index 0000000000..d57294cc40 --- /dev/null +++ b/components/esm4/loadstat.cpp @@ -0,0 +1,91 @@ +/* + Copyright (C) 2015-2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadstat.hpp" + +#include +#include // FIXME: debug only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Static::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + { + // version is only availabe in TES5 (seems to be 27 or 28?) + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + //std::cout << "STAT MODT ver: " << std::hex << reader.hdr().record.version << std::endl; + + // for TES4 these are just a sequence of bytes + mMODT.resize(subHdr.dataSize/sizeof(std::uint8_t)); + for (std::vector::iterator it = mMODT.begin(); it != mMODT.end(); ++it) + { + reader.get(*it); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "MODT: " << std::hex << *it << std::endl; +#endif + } + break; + } + case ESM4::SUB_MODS: + case ESM4::SUB_OBND: + case ESM4::SUB_DNAM: + case ESM4::SUB_MNAM: + case ESM4::SUB_BRUS: // FONV + case ESM4::SUB_RNAM: // FONV + { + //std::cout << "STAT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::STAT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Static::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Static::blank() +//{ +//} diff --git a/components/esm4/loadstat.hpp b/components/esm4/loadstat.hpp new file mode 100644 index 0000000000..07369b3357 --- /dev/null +++ b/components/esm4/loadstat.hpp @@ -0,0 +1,59 @@ +/* + Copyright (C) 2015-2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_STAT_H +#define ESM4_STAT_H + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Static + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + float mBoundRadius; + std::vector mMODT; // FIXME texture hash + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_STAT_H diff --git a/components/esm4/loadtact.cpp b/components/esm4/loadtact.cpp new file mode 100644 index 0000000000..0d684719be --- /dev/null +++ b/components/esm4/loadtact.cpp @@ -0,0 +1,77 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadtact.hpp" + +#include +#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::TalkingActivator::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_VNAM: reader.getFormId(mVoiceType); break; + case ESM4::SUB_SNAM: reader.getFormId(mLoopSound); break; + case ESM4::SUB_INAM: reader.getFormId(mRadioTemplate); break; // FONV + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_DEST: // FO3 destruction + case ESM4::SUB_DSTD: // FO3 destruction + case ESM4::SUB_DSTF: // FO3 destruction + case ESM4::SUB_FNAM: + case ESM4::SUB_PNAM: + case ESM4::SUB_MODT: // texture file hash? + case ESM4::SUB_OBND: + { + //std::cout << "TACT " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::TACT::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::TalkingActivator::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::TalkingActivator::blank() +//{ +//} diff --git a/components/esm4/loadtact.hpp b/components/esm4/loadtact.hpp new file mode 100644 index 0000000000..3a79006d78 --- /dev/null +++ b/components/esm4/loadtact.hpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_TACT_H +#define ESM4_TACT_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + enum TalkingActivatorFlags + { + TACT_OnLocalMap = 0x00000200, + TACT_QuestItem = 0x00000400, + TACT_NoVoiceFilter = 0x00002000, + TACT_RandomAnimStart = 0x00010000, + TACT_RadioStation = 0x00020000, + TACT_NonProxy = 0x10000000, // only valid if Radio Station + TACT_ContBroadcast = 0x40000000 // only valid if Radio Station + }; + + struct TalkingActivator + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see above for details + + std::string mEditorId; + std::string mFullName; + + std::string mModel; + + FormId mScriptId; + FormId mVoiceType; // VTYP + FormId mLoopSound; // SOUN + FormId mRadioTemplate; // SOUN + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_TACT_H diff --git a/components/esm4/loadterm.cpp b/components/esm4/loadterm.cpp new file mode 100644 index 0000000000..fc1d10cf70 --- /dev/null +++ b/components/esm4/loadterm.cpp @@ -0,0 +1,87 @@ +/* + Copyright (C) 2019-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadterm.hpp" + +#include +//#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Terminal::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_PNAM: reader.getFormId(mPasswordNote); break; + case ESM4::SUB_SNAM: reader.getFormId(mSound); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_RNAM: reader.getZString(mResultText); break; + case ESM4::SUB_DNAM: // difficulty + case ESM4::SUB_ANAM: // flags + case ESM4::SUB_CTDA: + case ESM4::SUB_INAM: + case ESM4::SUB_ITXT: + case ESM4::SUB_MODT: // texture hash? + case ESM4::SUB_SCDA: + case ESM4::SUB_SCHR: + case ESM4::SUB_SCRO: + case ESM4::SUB_SCRV: + case ESM4::SUB_SCTX: + case ESM4::SUB_SCVR: + case ESM4::SUB_SLSD: + case ESM4::SUB_TNAM: + case ESM4::SUB_OBND: + case ESM4::SUB_MODS: // FONV + { + //std::cout << "TERM " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::TERM::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Terminal::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Terminal::blank() +//{ +//} diff --git a/components/esm4/loadterm.hpp b/components/esm4/loadterm.hpp new file mode 100644 index 0000000000..32ec613ff5 --- /dev/null +++ b/components/esm4/loadterm.hpp @@ -0,0 +1,63 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_TERM_H +#define ESM4_TERM_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Terminal + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mText; + + std::string mModel; + std::string mResultText; + + FormId mScriptId; + FormId mPasswordNote; + FormId mSound; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_TERM_H diff --git a/components/esm4/loadtes4.cpp b/components/esm4/loadtes4.cpp new file mode 100644 index 0000000000..453a2f12a7 --- /dev/null +++ b/components/esm4/loadtes4.cpp @@ -0,0 +1,112 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadtes4.hpp" + +#ifdef NDEBUG // FIXME: debuggigng only +#undef NDEBUG +#endif + +#include +#include + +#include // FIXME: debugging only + +#include "common.hpp" +#include "formid.hpp" +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Header::load(ESM4::Reader& reader) +{ + mFlags = reader.hdr().record.flags; // 0x01 = Rec_ESM, 0x80 = Rec_Localized + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_HEDR: + { + if (!reader.getExact(mData.version) || !reader.getExact(mData.records) || !reader.getExact(mData.nextObjectId)) + throw std::runtime_error("TES4 HEDR data read error"); + if ((size_t)subHdr.dataSize != sizeof(mData.version)+sizeof(mData.records)+sizeof(mData.nextObjectId)) + throw std::runtime_error("TES4 HEDR data size mismatch"); + break; + } + case ESM4::SUB_CNAM: reader.getZString(mAuthor); break; + case ESM4::SUB_SNAM: reader.getZString(mDesc); break; + case ESM4::SUB_MAST: // multiple + { + ESM::MasterData m; + if (!reader.getZString(m.name)) + throw std::runtime_error("TES4 MAST data read error"); + + // NOTE: some mods do not have DATA following MAST so can't read DATA here + m.size = 0; + mMaster.push_back (m); + break; + } + case ESM4::SUB_DATA: + { + // WARNING: assumes DATA always follows MAST + if (!reader.getExact(mMaster.back().size)) + throw std::runtime_error("TES4 DATA data read error"); + break; + } + case ESM4::SUB_ONAM: + { + mOverrides.resize(subHdr.dataSize/sizeof(FormId)); + for (unsigned int & mOverride : mOverrides) + { + if (!reader.getExact(mOverride)) + throw std::runtime_error("TES4 ONAM data read error"); +#if 0 + std::string padding; + padding.insert(0, reader.stackSize()*2, ' '); + std::cout << padding << "ESM4::Header::ONAM overrides: " << formIdToString(mOverride) << std::endl; +#endif + } + break; + } + case ESM4::SUB_INTV: + case ESM4::SUB_INCC: + case ESM4::SUB_OFST: // Oblivion only? + case ESM4::SUB_DELE: // Oblivion only? + { + //std::cout << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::Header::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Header::save(ESM4::Writer& writer) +//{ +//} diff --git a/components/esm4/loadtes4.hpp b/components/esm4/loadtes4.hpp new file mode 100644 index 0000000000..6d84fe360b --- /dev/null +++ b/components/esm4/loadtes4.hpp @@ -0,0 +1,69 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_TES4_H +#define ESM4_TES4_H + +#include + +#include "formid.hpp" +#include "../esm/common.hpp" // ESMVersion, MasterData + +namespace ESM4 +{ + class Reader; + class Writer; + +#pragma pack(push, 1) + struct Data + { + ESM::ESMVersion version; // File format version. + std::int32_t records; // Number of records + std::uint32_t nextObjectId; + }; +#pragma pack(pop) + + struct Header + { + std::uint32_t mFlags; // 0x01 esm, 0x80 localised strings + + Data mData; + std::string mAuthor; // Author's name + std::string mDesc; // File description + std::vector mMaster; + + std::vector mOverrides; // Skyrim only, cell children (ACHR, LAND, NAVM, PGRE, PHZD, REFR) + + // position in the vector = mod index of master files above + // value = adjusted mod index based on all the files loaded so far + //std::vector mModIndices; + + void load (Reader& reader); + //void save (Writer& writer); + }; +} + +#endif // ESM4_TES4_H diff --git a/components/esm4/loadtree.cpp b/components/esm4/loadtree.cpp new file mode 100644 index 0000000000..1ac3301c3f --- /dev/null +++ b/components/esm4/loadtree.cpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 2016, 2018 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadtree.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Tree::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mLeafTexture); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_MODT: + case ESM4::SUB_CNAM: + case ESM4::SUB_BNAM: + case ESM4::SUB_SNAM: + case ESM4::SUB_FULL: + case ESM4::SUB_OBND: + case ESM4::SUB_PFIG: + case ESM4::SUB_PFPC: + { + //std::cout << "TREE " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::TREE::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Tree::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Tree::blank() +//{ +//} diff --git a/components/esm4/loadtree.hpp b/components/esm4/loadtree.hpp new file mode 100644 index 0000000000..48c59468b2 --- /dev/null +++ b/components/esm4/loadtree.hpp @@ -0,0 +1,59 @@ +/* + Copyright (C) 2016, 2018, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_TREE_H +#define ESM4_TREE_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Tree + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mModel; + + float mBoundRadius; + + std::string mLeafTexture; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_TREE_H diff --git a/components/esm4/loadtxst.cpp b/components/esm4/loadtxst.cpp new file mode 100644 index 0000000000..b0f8c1113c --- /dev/null +++ b/components/esm4/loadtxst.cpp @@ -0,0 +1,75 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadtxst.hpp" + +#include +#include // FIXME: testing only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::TextureSet::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_TX00: reader.getZString(mDiffuse); break; + case ESM4::SUB_TX01: reader.getZString(mNormalMap); break; + case ESM4::SUB_TX02: reader.getZString(mEnvMask); break; + case ESM4::SUB_TX03: reader.getZString(mToneMap); break; + case ESM4::SUB_TX04: reader.getZString(mDetailMap); break; + case ESM4::SUB_TX05: reader.getZString(mEnvMap); break; + case ESM4::SUB_TX06: reader.getZString(mUnknown); break; + case ESM4::SUB_TX07: reader.getZString(mSpecular); break; + case ESM4::SUB_DNAM: + case ESM4::SUB_DODT: + case ESM4::SUB_OBND: // object bounds + { + //std::cout << "TXST " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::TXST::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::TextureSet::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::TextureSet::blank() +//{ +//} diff --git a/components/esm4/loadtxst.hpp b/components/esm4/loadtxst.hpp new file mode 100644 index 0000000000..be968f2e6e --- /dev/null +++ b/components/esm4/loadtxst.hpp @@ -0,0 +1,63 @@ +/* + Copyright (C) 2019, 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_TXST_H +#define ESM4_TXST_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct TextureSet + { + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + + std::string mDiffuse; // includes alpha info + std::string mNormalMap; // includes specular info (alpha channel) + std::string mEnvMask; + std::string mToneMap; + std::string mDetailMap; + std::string mEnvMap; + std::string mUnknown; + std::string mSpecular; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_TXST_H diff --git a/components/esm4/loadweap.cpp b/components/esm4/loadweap.cpp new file mode 100644 index 0000000000..56ae2174d2 --- /dev/null +++ b/components/esm4/loadweap.cpp @@ -0,0 +1,173 @@ +/* + Copyright (C) 2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadweap.hpp" + +#include + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::Weapon::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + std::uint32_t esmVer = reader.esmVersion(); + bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_DATA: + { + //if (reader.esmVersion() == ESM::VER_094 || reader.esmVersion() == ESM::VER_170) + if (subHdr.dataSize == 10) // FO3 has 15 bytes even though VER_094 + { + reader.get(mData.value); + reader.get(mData.weight); + reader.get(mData.damage); + } + else if (isFONV || subHdr.dataSize == 15) + { + reader.get(mData.value); + reader.get(mData.health); + reader.get(mData.weight); + reader.get(mData.damage); + reader.get(mData.clipSize); + } + else + { + reader.get(mData.type); + reader.get(mData.speed); + reader.get(mData.reach); + reader.get(mData.flags); + reader.get(mData.value); + reader.get(mData.health); + reader.get(mData.weight); + reader.get(mData.damage); + } + break; + } + case ESM4::SUB_MODL: reader.getZString(mModel); break; + case ESM4::SUB_ICON: reader.getZString(mIcon); break; + case ESM4::SUB_MICO: reader.getZString(mMiniIcon); break; // FO3 + case ESM4::SUB_SCRI: reader.getFormId(mScriptId); break; + case ESM4::SUB_ANAM: reader.get(mEnchantmentPoints); break; + case ESM4::SUB_ENAM: reader.getFormId(mEnchantment); break; + case ESM4::SUB_MODB: reader.get(mBoundRadius); break; + case ESM4::SUB_DESC: reader.getLocalizedString(mText); break; + case ESM4::SUB_YNAM: reader.getFormId(mPickUpSound); break; + case ESM4::SUB_ZNAM: reader.getFormId(mDropSound); break; + case ESM4::SUB_MODT: + case ESM4::SUB_BAMT: + case ESM4::SUB_BIDS: + case ESM4::SUB_INAM: + case ESM4::SUB_CNAM: + case ESM4::SUB_CRDT: + case ESM4::SUB_DNAM: + case ESM4::SUB_EAMT: + case ESM4::SUB_EITM: + case ESM4::SUB_ETYP: + case ESM4::SUB_KSIZ: + case ESM4::SUB_KWDA: + case ESM4::SUB_NAM8: + case ESM4::SUB_NAM9: + case ESM4::SUB_OBND: + case ESM4::SUB_SNAM: + case ESM4::SUB_TNAM: + case ESM4::SUB_UNAM: + case ESM4::SUB_VMAD: + case ESM4::SUB_VNAM: + case ESM4::SUB_WNAM: + case ESM4::SUB_XNAM: // Dawnguard only? + case ESM4::SUB_NNAM: + case ESM4::SUB_MODS: + case ESM4::SUB_NAM0: // FO3 + case ESM4::SUB_REPL: // FO3 + case ESM4::SUB_MOD2: // FO3 + case ESM4::SUB_MO2T: // FO3 + case ESM4::SUB_MO2S: // FO3 + case ESM4::SUB_NAM6: // FO3 + case ESM4::SUB_MOD4: // FO3 + case ESM4::SUB_MO4T: // FO3 + case ESM4::SUB_MO4S: // FO3 + case ESM4::SUB_BIPL: // FO3 + case ESM4::SUB_NAM7: // FO3 + case ESM4::SUB_MOD3: // FO3 + case ESM4::SUB_MO3T: // FO3 + case ESM4::SUB_MO3S: // FO3 + case ESM4::SUB_MODD: // FO3 + //case ESM4::SUB_MOSD: // FO3 + case ESM4::SUB_DEST: // FO3 + case ESM4::SUB_DSTD: // FO3 + case ESM4::SUB_DSTF: // FO3 + case ESM4::SUB_DMDL: // FO3 + case ESM4::SUB_DMDT: // FO3 + case ESM4::SUB_VATS: // FONV + case ESM4::SUB_VANM: // FONV + case ESM4::SUB_MWD1: // FONV + case ESM4::SUB_MWD2: // FONV + case ESM4::SUB_MWD3: // FONV + case ESM4::SUB_MWD4: // FONV + case ESM4::SUB_MWD5: // FONV + case ESM4::SUB_MWD6: // FONV + case ESM4::SUB_MWD7: // FONV + case ESM4::SUB_WMI1: // FONV + case ESM4::SUB_WMI2: // FONV + case ESM4::SUB_WMI3: // FONV + case ESM4::SUB_WMS1: // FONV + case ESM4::SUB_WMS2: // FONV + case ESM4::SUB_WNM1: // FONV + case ESM4::SUB_WNM2: // FONV + case ESM4::SUB_WNM3: // FONV + case ESM4::SUB_WNM4: // FONV + case ESM4::SUB_WNM5: // FONV + case ESM4::SUB_WNM6: // FONV + case ESM4::SUB_WNM7: // FONV + case ESM4::SUB_EFSD: // FONV DeadMoney + { + //std::cout << "WEAP " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); + break; + } + default: + throw std::runtime_error("ESM4::WEAP::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + } +} + +//void ESM4::Weapon::save(ESM4::Writer& writer) const +//{ +//} + +//void ESM4::Weapon::blank() +//{ +//} diff --git a/components/esm4/loadweap.hpp b/components/esm4/loadweap.hpp new file mode 100644 index 0000000000..1abfc5b577 --- /dev/null +++ b/components/esm4/loadweap.hpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_WEAP_H +#define ESM4_WEAP_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct Weapon + { + struct Data + { + // type + // 0 = Blade One Hand + // 1 = Blade Two Hand + // 2 = Blunt One Hand + // 3 = Blunt Two Hand + // 4 = Staff + // 5 = Bow + std::uint32_t type; + float speed; + float reach; + std::uint32_t flags; + std::uint32_t value; // gold + std::uint32_t health; + float weight; + std::uint16_t damage; + std::uint8_t clipSize; // FO3/FONV only + + Data() : type(0), speed(0.f), reach(0.f), flags(0), value(0), + health(0), weight(0.f), damage(0), clipSize(0) {} + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mText; + std::string mIcon; + std::string mMiniIcon; + + FormId mPickUpSound; + FormId mDropSound; + + float mBoundRadius; + + FormId mScriptId; + std::uint16_t mEnchantmentPoints; + FormId mEnchantment; + + Data mData; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + + //void blank(); + }; +} + +#endif // ESM4_WEAP_H diff --git a/components/esm4/loadwrld.cpp b/components/esm4/loadwrld.cpp new file mode 100644 index 0000000000..d8ec79b625 --- /dev/null +++ b/components/esm4/loadwrld.cpp @@ -0,0 +1,182 @@ +/* + Copyright (C) 2015-2016, 2018-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#include "loadwrld.hpp" + +#include +//#include // FIXME: debug only + +#include "reader.hpp" +//#include "writer.hpp" + +void ESM4::World::load(ESM4::Reader& reader) +{ + mFormId = reader.hdr().record.id; + reader.adjustFormId(mFormId); + mFlags = reader.hdr().record.flags; + + // It should be possible to save the current world formId automatically while reading in + // the record header rather than doing it manually here but possibly less efficient (may + // need to check each record?). + // + // Alternatively it may be possible to figure it out by examining the group headers, but + // apparently the label field is not reliable so the parent world formid may have been + // corrupted by the use of ignore flag (TODO: should check to verify). + reader.setCurrWorld(mFormId); // save for CELL later + + std::uint32_t subSize = 0; // for XXXX sub record + + std::uint32_t esmVer = reader.esmVersion(); + //bool isTES4 = (esmVer == ESM::VER_080 || esmVer == ESM::VER_100); + //bool isFONV = (esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134); + bool isTES5 = (esmVer == ESM::VER_094 || esmVer == ESM::VER_170); // WARN: FO3 is also VER_094 + bool usingDefaultLevels = true; + + while (reader.getSubRecordHeader()) + { + const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader(); + switch (subHdr.typeId) + { + case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; + case ESM4::SUB_WCTR: reader.get(mCenterCell); break; // Center cell, TES5 only + case ESM4::SUB_WNAM: reader.getFormId(mParent); break; + case ESM4::SUB_SNAM: reader.get(mSound); break; // sound, Oblivion only? + case ESM4::SUB_ICON: reader.getZString(mMapFile); break; // map filename, Oblivion only? + case ESM4::SUB_CNAM: reader.get(mClimate); break; + case ESM4::SUB_NAM2: reader.getFormId(mWater); break; + case ESM4::SUB_NAM0: + { + reader.get(mMinX); + reader.get(mMinY); + break; + } + case ESM4::SUB_NAM9: + { + reader.get(mMaxX); + reader.get(mMaxY); + break; + } + case ESM4::SUB_DATA: reader.get(mWorldFlags); break; + case ESM4::SUB_MNAM: + { + reader.get(mMap.width); + reader.get(mMap.height); + reader.get(mMap.NWcellX); + reader.get(mMap.NWcellY); + reader.get(mMap.SEcellX); + reader.get(mMap.SEcellY); + + if (subHdr.dataSize == 28) // Skyrim? + { + reader.get(mMap.minHeight); + reader.get(mMap.maxHeight); + reader.get(mMap.initialPitch); + } + + break; + } + case ESM4::SUB_DNAM: // defaults + { + reader.get(mLandLevel); // -2700.f for TES5 + reader.get(mWaterLevel); // -14000.f for TES5 + usingDefaultLevels = false; + + break; + } + // Only a few worlds in FO3 have music (I'm guessing 00090908 "explore" is the default?) + // 00090906 public WRLD: 00000A74 MegatonWorld + // 00090CE7 base WRLD: 0001A25D DCWorld18 (Arlington National Cemeteray) + // 00090CE7 base WRLD: 0001A266 DCWorld09 (The Mall) + // 00090CE7 base WRLD: 0001A267 DCWorld08 (Pennsylvania Avenue) + // 000BAD30 tranquilitylane WRLD: 000244A7 TranquilityLane + // 00090CE7 base WRLD: 000271C0 MonumentWorld (The Washington Monument) + // 00090907 dungeon WRLD: 0004C4D1 MamaDolcesWorld (Mama Dolce's Loading Yard) + // + // FONV has only 3 (note the different format, also can't find the files?): + // 00119D2E freeside\freeside_01.mp3 0010BEEA FreesideWorld (Freeside) + // 00119D2E freeside\freeside_01.mp3 0012D94D FreesideNorthWorld (Freeside) + // 00119D2E freeside\freeside_01.mp3 0012D94E FreesideFortWorld (Old Mormon Fort) + // NOTE: FONV DefaultObjectManager has 00090908 "explore" as the default music + case ESM4::SUB_ZNAM: reader.getFormId(mMusic); break; + case ESM4::SUB_PNAM: reader.get(mParentUseFlags); break; + case ESM4::SUB_RNAM: // multiple + case ESM4::SUB_MHDT: + case ESM4::SUB_LTMP: + case ESM4::SUB_XEZN: + case ESM4::SUB_XLCN: + case ESM4::SUB_NAM3: + case ESM4::SUB_NAM4: + case ESM4::SUB_MODL: + case ESM4::SUB_NAMA: + case ESM4::SUB_ONAM: + case ESM4::SUB_TNAM: + case ESM4::SUB_UNAM: + case ESM4::SUB_XWEM: + case ESM4::SUB_MODT: // from Dragonborn onwards? + case ESM4::SUB_INAM: // FO3 + case ESM4::SUB_NNAM: // FO3 + case ESM4::SUB_XNAM: // FO3 + case ESM4::SUB_IMPS: // FO3 Anchorage + case ESM4::SUB_IMPF: // FO3 Anchorage + { + //std::cout << "WRLD " << ESM::printName(subHdr.typeId) << " skipping..." << std::endl; + reader.skipSubRecordData(); // FIXME: process the subrecord rather than skip + break; + } + case ESM4::SUB_OFST: + { + if (subSize) + { + reader.skipSubRecordData(subSize); // special post XXXX + reader.updateRecordRead(subSize); // WARNING: manually update + subSize = 0; + } + else + reader.skipSubRecordData(); // FIXME: process the subrecord rather than skip + + break; + } + case ESM4::SUB_XXXX: + { + reader.get(subSize); + break; + } + default: + throw std::runtime_error("ESM4::WRLD::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); + } + + if (isTES5 && usingDefaultLevels) + { + mLandLevel = -2700.f; + mWaterLevel = -14000.f; + } + } +} + +//void ESM4::World::save(ESM4::Writer& writer) const +//{ +//} diff --git a/components/esm4/loadwrld.hpp b/components/esm4/loadwrld.hpp new file mode 100644 index 0000000000..b6b643446c --- /dev/null +++ b/components/esm4/loadwrld.hpp @@ -0,0 +1,134 @@ +/* + Copyright (C) 2015-2016, 2018-2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_WRLD_H +#define ESM4_WRLD_H + +#include +#include +#include + +#include "common.hpp" + +namespace ESM4 +{ + class Reader; + class Writer; + + struct World + { + enum WorldFlags // TES4 TES5 + { // -------------------- ----------------- + WLD_Small = 0x01, // Small World Small World + WLD_NoFastTravel = 0x02, // Can't Fast Travel Can't Fast Travel + WLD_Oblivion = 0x04, // Oblivion worldspace + WLD_NoLODWater = 0x08, // No LOD Water + WLD_NoLandscpe = 0x10, // No LOD Water No Landscape + WLD_NoSky = 0x20, // No Sky + wLD_FixedDimension = 0x40, // Fixed Dimensions + WLD_NoGrass = 0x80 // No Grass + }; + + struct REFRcoord + { + FormId formId; + std::int16_t unknown1; + std::int16_t unknown2; + }; + + struct RNAMstruct + { + std::int16_t unknown1; + std::int16_t unknown2; + std::vector refrs; + }; + + //Map size struct 16 or 28 byte structure + struct Map + { + std::uint32_t width; // usable width of the map + std::uint32_t height; // usable height of the map + std::int16_t NWcellX; + std::int16_t NWcellY; + std::int16_t SEcellX; + std::int16_t SEcellY; + float minHeight; // Camera Data (default 50000), new as of Skyrim 1.8, purpose is not yet known. + float maxHeight; // Camera Data (default 80000) + float initialPitch; + }; + + FormId mFormId; // from the header + std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + + std::string mEditorId; + std::string mFullName; + FormId mParent; // parent worldspace formid + std::uint8_t mWorldFlags; + FormId mClimate; + FormId mWater; + float mLandLevel; + float mWaterLevel; + + Map mMap; + + std::int32_t mMinX; + std::int32_t mMinY; + std::int32_t mMaxX; + std::int32_t mMaxY; + + // ------ TES4 only ----- + + std::int32_t mSound; // 0 = no record, 1 = Public, 2 = Dungeon + std::string mMapFile; + + // ------ TES5 only ----- + + Grid mCenterCell; + RNAMstruct mData; + + // ---------------------- + FormId mMusic; + + // 0x01 use Land data + // 0x02 use LOD data + // 0x04 use Map data + // 0x08 use Water data + // 0x10 use Climate data + // 0x20 use Image Space data (Climate for TES5) + // 0x40 use SkyCell (TES5) + // 0x80 needs water adjustment (this isn't for parent I think? FONV only set for wastelandnv) + std::uint16_t mParentUseFlags; // FO3/FONV + + // cache formId's of children (e.g. CELL, ROAD) + std::vector mCells; + std::vector mRoads; + + void load(ESM4::Reader& reader); + //void save(ESM4::Writer& writer) const; + }; +} + +#endif // ESM4_WRLD_H diff --git a/components/esm4/reader.cpp b/components/esm4/reader.cpp new file mode 100644 index 0000000000..3c0ac470c4 --- /dev/null +++ b/components/esm4/reader.cpp @@ -0,0 +1,649 @@ +/* + Copyright (C) 2015-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + +*/ +#include "reader.hpp" + +#ifdef NDEBUG // FIXME: debugging only +#undef NDEBUG +#endif + +#undef DEBUG_GROUPSTACK + +#include +#include +#include +#include +#include // for debugging +#include // for debugging +#include // for debugging + +#if defined(_MSC_VER) + #pragma warning (push) + #pragma warning (disable : 4706) + #include + #pragma warning (pop) +#else + #include +#endif +#include +#include + +#include +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ + +ReaderContext::ReaderContext() : modIndex(0), recHeaderSize(sizeof(RecordHeader)), + filePos(0), fileRead(0), recordRead(0), currWorld(0), currCell(0), cellGridValid(false) +{ + currCellGrid.cellId = 0; + currCellGrid.grid.x = 0; + currCellGrid.grid.y = 0; + subRecordHeader.typeId = 0; + subRecordHeader.dataSize = 0; +} + +Reader::Reader(Files::IStreamPtr&& esmStream, const std::string& filename) + : mEncoder(nullptr), mFileSize(0), mStream(std::move(esmStream)) +{ + // used by ESMReader only? + mCtx.filename = filename; + + mCtx.fileRead = 0; + mStream->seekg(0, mStream->end); + mFileSize = mStream->tellg(); + mStream->seekg(20); // go to the start but skip the "TES4" record header + + mSavedStream.reset(); + + // determine header size + std::uint32_t subRecName = 0; + mStream->read((char*)&subRecName, sizeof(subRecName)); + if (subRecName == 0x52444548) // "HEDR" + mCtx.recHeaderSize = sizeof(RecordHeader) - 4; // TES4 header size is 4 bytes smaller than TES5 header + else + mCtx.recHeaderSize = sizeof(RecordHeader); + + // restart from the beginning (i.e. "TES4" record header) + mStream->seekg(0, mStream->beg); +#if 0 + unsigned int esmVer = mHeader.mData.version.ui; + bool isTes4 = esmVer == ESM::VER_080 || esmVer == ESM::VER_100; + //bool isTes5 = esmVer == ESM::VER_094 || esmVer == ESM::VER_170; + //bool isFONV = esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134; + + // TES4 header size is 4 bytes smaller than TES5 header + mCtx.recHeaderSize = isTes4 ? sizeof(ESM4::RecordHeader) - 4 : sizeof(ESM4::RecordHeader); +#endif + getRecordHeader(); + if (mCtx.recordHeader.record.typeId == REC_TES4) + { + mHeader.load(*this); + mCtx.fileRead += mCtx.recordHeader.record.dataSize; + + buildLStringIndex(); // for localised strings in Skyrim + } + else + fail("Unknown file format"); +} + +Reader::~Reader() +{ + close(); +} + +// Since the record data may have been compressed, it is not always possible to use seek() to +// go to a position of a sub record. +// +// The record header needs to be saved in the context or the header needs to be re-loaded after +// restoring the context. The latter option was chosen. +ReaderContext Reader::getContext() +{ + mCtx.filePos = mStream->tellg(); + mCtx.filePos -= mCtx.recHeaderSize; // update file position + return mCtx; +} + +// NOTE: Assumes that the caller has reopened the file if necessary +bool Reader::restoreContext(const ReaderContext& ctx) +{ + if (mSavedStream) // TODO: doesn't seem to ever happen + { + mStream = std::move(mSavedStream); + } + + mCtx.groupStack.clear(); // probably not necessary since it will be overwritten + mCtx = ctx; + mStream->seekg(ctx.filePos); // update file position + + return getRecordHeader(); +} + +void Reader::close() +{ + mStream.reset(); + //clearCtx(); + //mHeader.blank(); +} + +void Reader::openRaw(Files::IStreamPtr&& stream, const std::string& filename) +{ + close(); + + mStream = std::move(stream); + mCtx.filename = filename; + mCtx.fileRead = 0; + mStream->seekg(0, mStream->end); + mFileSize = mStream->tellg(); + mStream->seekg(0, mStream->beg); + +} + +void Reader::open(Files::IStreamPtr&& stream, const std::string &filename) +{ + openRaw(std::move(stream), filename); + + // should at least have the size of ESM3 record header (20 or 24 bytes for ESM4) + assert (mFileSize >= 16); + std::uint32_t modVer = 0; + if (getExact(modVer)) // get the first 4 bytes of the record header only + { + // FIXME: need to setup header/context + if (modVer == REC_TES4) + { + } + else + { + } + } + + throw std::runtime_error("Unknown file format"); // can't yet use fail() as mCtx is not setup +} + +void Reader::openRaw(const std::string& filename) +{ + openRaw(Files::openConstrainedFileStream(filename), filename); +} + +void Reader::open(const std::string& filename) +{ + open(Files::openConstrainedFileStream(filename), filename); +} + +void Reader::setRecHeaderSize(const std::size_t size) +{ + mCtx.recHeaderSize = size; +} + +// FIXME: only "English" strings supported for now +void Reader::buildLStringIndex() +{ + if ((mHeader.mFlags & Rec_ESM) == 0 || (mHeader.mFlags & Rec_Localized) == 0) + return; + + std::filesystem::path p(mCtx.filename); + std::string filename = p.stem().filename().string(); + + buildLStringIndex("Strings/" + filename + "_English.STRINGS", Type_Strings); + buildLStringIndex("Strings/" + filename + "_English.ILSTRINGS", Type_ILStrings); + buildLStringIndex("Strings/" + filename + "_English.DLSTRINGS", Type_DLStrings); +} + +void Reader::buildLStringIndex(const std::string& stringFile, LocalizedStringType stringType) +{ + std::uint32_t numEntries; + std::uint32_t dataSize; + std::uint32_t stringId; + LStringOffset sp; + sp.type = stringType; + + // TODO: possibly check if the resource exists? + Files::IStreamPtr filestream = Files::openConstrainedFileStream(stringFile); + + filestream->seekg(0, std::ios::end); + std::size_t fileSize = filestream->tellg(); + filestream->seekg(0, std::ios::beg); + + std::istream* stream = filestream.get(); + switch (stringType) + { + case Type_Strings: mStrings = std::move(filestream); break; + case Type_ILStrings: mILStrings = std::move(filestream); break; + case Type_DLStrings: mDLStrings = std::move(filestream); break; + default: + throw std::runtime_error("ESM4::Reader::unknown localised string type"); + } + + stream->read((char*)&numEntries, sizeof(numEntries)); + stream->read((char*)&dataSize, sizeof(dataSize)); + std::size_t dataStart = fileSize - dataSize; + for (unsigned int i = 0; i < numEntries; ++i) + { + stream->read((char*)&stringId, sizeof(stringId)); + stream->read((char*)&sp.offset, sizeof(sp.offset)); + sp.offset += (std::uint32_t)dataStart; + mLStringIndex[stringId] = sp; + } + //assert (dataStart - stream->tell() == 0 && "String file start of data section mismatch"); +} + +void Reader::getLocalizedString(std::string& str) +{ + if (!hasLocalizedStrings()) + return (void)getZString(str); + + std::uint32_t stringId; // FormId + get(stringId); + if (stringId) // TES5 FoxRace, BOOK + getLocalizedStringImpl(stringId, str); +} + +// FIXME: very messy and probably slow/inefficient +void Reader::getLocalizedStringImpl(const FormId stringId, std::string& str) +{ + const std::map::const_iterator it = mLStringIndex.find(stringId); + + if (it != mLStringIndex.end()) + { + std::istream* filestream = nullptr; + + switch (it->second.type) + { + case Type_Strings: // no string size provided + { + filestream = mStrings.get(); + filestream->seekg(it->second.offset); + + char ch; + std::vector data; + do { + filestream->read(&ch, sizeof(ch)); + data.push_back(ch); + } while (ch != 0); + + str = std::string(data.data()); + return; + } + case Type_ILStrings: filestream = mILStrings.get(); break; + case Type_DLStrings: filestream = mDLStrings.get(); break; + default: + throw std::runtime_error("ESM4::Reader::getLocalizedString unknown string type"); + } + + // get ILStrings or DLStrings (they provide string size) + filestream->seekg(it->second.offset); + std::uint32_t size = 0; + filestream->read((char*)&size, sizeof(size)); + getStringImpl(str, size, *filestream, mEncoder, true); // expect null terminated string + } + else + throw std::runtime_error("ESM4::Reader::getLocalizedString localized string not found"); +} + +bool Reader::getRecordHeader() +{ + // FIXME: this seems very hacky but we may have skipped subrecords from within an inflated data block + if (/*mStream->eof() && */mSavedStream) + { + mStream = std::move(mSavedStream); + } + + mStream->read((char*)&mCtx.recordHeader, mCtx.recHeaderSize); + std::size_t bytesRead = (std::size_t)mStream->gcount(); + + // keep track of data left to read from the file + mCtx.fileRead += mCtx.recHeaderSize; + + mCtx.recordRead = 0; // for keeping track of sub records + + // After reading the record header we can cache a WRLD or CELL formId for convenient access later. + // FIXME: currently currWorld and currCell are set manually when loading the WRLD and CELL records + + // HACK: mCtx.groupStack.back() is updated before the record data are read/skipped + // N.B. the data must be fully read/skipped for this to work + if (mCtx.recordHeader.record.typeId != REC_GRUP && !mCtx.groupStack.empty()) + { + mCtx.groupStack.back().second += (std::uint32_t)mCtx.recHeaderSize + mCtx.recordHeader.record.dataSize; + + // keep track of data left to read from the file + mCtx.fileRead += mCtx.recordHeader.record.dataSize; + } + + return bytesRead == mCtx.recHeaderSize; +} + +void Reader::getRecordData(bool dump) +{ + std::uint32_t uncompressedSize = 0; + + if ((mCtx.recordHeader.record.flags & Rec_Compressed) != 0) + { + mStream->read(reinterpret_cast(&uncompressedSize), sizeof(std::uint32_t)); + + std::size_t recordSize = mCtx.recordHeader.record.dataSize - sizeof(std::uint32_t); + Bsa::MemoryInputStream compressedRecord(recordSize); + mStream->read(compressedRecord.getRawData(), recordSize); + std::istream *fileStream = (std::istream*)&compressedRecord; + mSavedStream = std::move(mStream); + + mCtx.recordHeader.record.dataSize = uncompressedSize - sizeof(uncompressedSize); + + auto memoryStreamPtr = std::make_unique(uncompressedSize); + + boost::iostreams::filtering_streambuf inputStreamBuf; + inputStreamBuf.push(boost::iostreams::zlib_decompressor()); + inputStreamBuf.push(*fileStream); + + boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData(), uncompressedSize); + boost::iostreams::copy(inputStreamBuf, sr); + + // For debugging only +//#if 0 +if (dump) +{ + std::ostringstream ss; + char* data = memoryStreamPtr->getRawData(); + for (unsigned int i = 0; i < uncompressedSize; ++i) + { + if (data[i] > 64 && data[i] < 91) + ss << (char)(data[i]) << " "; + else + ss << std::setfill('0') << std::setw(2) << std::hex << (int)(data[i]); + if ((i & 0x000f) == 0xf) + ss << "\n"; + else if (i < uncompressedSize-1) + ss << " "; + } + std::cout << ss.str() << std::endl; +} +//#endif + mStream = std::make_unique>(std::move(memoryStreamPtr)); + } +} + +void Reader::skipRecordData() +{ + assert (mCtx.recordRead <= mCtx.recordHeader.record.dataSize && "Skipping after reading more than available"); + mStream->ignore(mCtx.recordHeader.record.dataSize - mCtx.recordRead); + mCtx.recordRead = mCtx.recordHeader.record.dataSize; // for getSubRecordHeader() +} + +bool Reader::getSubRecordHeader() +{ + bool result = false; + // NOTE: some SubRecords have 0 dataSize (e.g. SUB_RDSD in one of REC_REGN records in Oblivion.esm). + // Also SUB_XXXX has zero dataSize and the following 4 bytes represent the actual dataSize + // - hence it require manual updtes to mCtx.recordRead via updateRecordRead() + // See ESM4::NavMesh and ESM4::World. + if (mCtx.recordHeader.record.dataSize - mCtx.recordRead >= sizeof(mCtx.subRecordHeader)) + { + result = getExact(mCtx.subRecordHeader); + // HACK: below assumes sub-record data will be read or skipped in full; + // this hack aims to avoid updating mCtx.recordRead each time anything is read + mCtx.recordRead += (sizeof(mCtx.subRecordHeader) + mCtx.subRecordHeader.dataSize); + } + else if (mCtx.recordRead > mCtx.recordHeader.record.dataSize) + { + // try to correct any overshoot, seek to the end of the expected data + // this will only work if mCtx.subRecordHeader.dataSize was fully read or skipped + // (i.e. it will only correct mCtx.subRecordHeader.dataSize being incorrect) + // TODO: not tested + std::uint32_t overshoot = (std::uint32_t)mCtx.recordRead - mCtx.recordHeader.record.dataSize; + + std::size_t pos = mStream->tellg(); + mStream->seekg(pos - overshoot); + + return false; + } + + return result; +} + +void Reader::skipSubRecordData() +{ + mStream->ignore(mCtx.subRecordHeader.dataSize); +} + +void Reader::skipSubRecordData(std::uint32_t size) +{ + mStream->ignore(size); +} + +void Reader::enterGroup() +{ +#ifdef DEBUG_GROUPSTACK + std::string padding; // FIXME: debugging only + padding.insert(0, mCtx.groupStack.size()*2, ' '); + std::cout << padding << "Starting record group " + << printLabel(mCtx.recordHeader.group.label, mCtx.recordHeader.group.type) << std::endl; +#endif + // empty group if the group size is same as the header size + if (mCtx.recordHeader.group.groupSize == (std::uint32_t)mCtx.recHeaderSize) + { +#ifdef DEBUG_GROUPSTACK + std::cout << padding << "Ignoring record group " // FIXME: debugging only + << printLabel(mCtx.recordHeader.group.label, mCtx.recordHeader.group.type) + << " (empty)" << std::endl; +#endif + if (!mCtx.groupStack.empty()) // top group may be empty (e.g. HAIR in Skyrim) + { + // don't put on the stack, exitGroupCheck() may not get called before recursing into this method + mCtx.groupStack.back().second += mCtx.recordHeader.group.groupSize; + exitGroupCheck(); + } + + return; // don't push an empty group, just return + } + + // push group + mCtx.groupStack.push_back(std::make_pair(mCtx.recordHeader.group, (std::uint32_t)mCtx.recHeaderSize)); +} + +void Reader::exitGroupCheck() +{ + if (mCtx.groupStack.empty()) + return; + + // pop finished groups (note reading too much is allowed here) + std::uint32_t lastGroupSize = mCtx.groupStack.back().first.groupSize; + while (mCtx.groupStack.back().second >= lastGroupSize) + { +#ifdef DEBUG_GROUPSTACK + GroupTypeHeader grp = mCtx.groupStack.back().first; // FIXME: grp is for debugging only +#endif + // try to correct any overshoot + // TODO: not tested + std::uint32_t overshoot = mCtx.groupStack.back().second - lastGroupSize; + if (overshoot > 0) + { + std::size_t pos = mStream->tellg(); + mStream->seekg(pos - overshoot); + } + + mCtx.groupStack.pop_back(); +#ifdef DEBUG_GROUPSTACK + std::string padding; // FIXME: debugging only + padding.insert(0, mCtx.groupStack.size()*2, ' '); + std::cout << padding << "Finished record group " << printLabel(grp.label, grp.type) << std::endl; +#endif + // if the previous group was the final one no need to do below + if (mCtx.groupStack.empty()) + return; + + mCtx.groupStack.back().second += lastGroupSize; + lastGroupSize = mCtx.groupStack.back().first.groupSize; + + assert (lastGroupSize >= mCtx.groupStack.back().second && "Read more records than available"); +//#if 0 + if (mCtx.groupStack.back().second > lastGroupSize) // FIXME: debugging only + std::cerr << printLabel(mCtx.groupStack.back().first.label, + mCtx.groupStack.back().first.type) + << " read more records than available" << std::endl; +//#endif + } +} + +// WARNING: this method should be used after first calling enterGroup() +// else the method may try to dereference an element that does not exist +const GroupTypeHeader& Reader::grp(std::size_t pos) const +{ + assert (pos <= mCtx.groupStack.size()-1 && "ESM4::Reader::grp - exceeded stack depth"); + + return (*(mCtx.groupStack.end()-pos-1)).first; +} + +void Reader::skipGroupData() +{ + assert (!mCtx.groupStack.empty() && "Skipping group with an empty stack"); + + // subtract what was already read/skipped + std::uint32_t skipSize = mCtx.groupStack.back().first.groupSize - mCtx.groupStack.back().second; + + mStream->ignore(skipSize); + + // keep track of data left to read from the file + mCtx.fileRead += skipSize; + + mCtx.groupStack.back().second = mCtx.groupStack.back().first.groupSize; +} + +void Reader::skipGroup() +{ +#ifdef DEBUG_GROUPSTACK + std::string padding; // FIXME: debugging only + padding.insert(0, mCtx.groupStack.size()*2, ' '); + std::cout << padding << "Skipping record group " + << printLabel(mCtx.recordHeader.group.label, mCtx.recordHeader.group.type) << std::endl; +#endif + // subtract the size of header already read before skipping + std::uint32_t skipSize = mCtx.recordHeader.group.groupSize - (std::uint32_t)mCtx.recHeaderSize; + mStream->ignore(skipSize); + + // keep track of data left to read from the file + mCtx.fileRead += skipSize; + + // NOTE: mCtx.groupStack.back().second already has mCtx.recHeaderSize from enterGroup() + if (!mCtx.groupStack.empty()) + mCtx.groupStack.back().second += mCtx.recordHeader.group.groupSize; +} + +const CellGrid& Reader::currCellGrid() const +{ + // Maybe should throw an exception instead? + assert (mCtx.cellGridValid && "Attempt to use an invalid cell grid"); + + return mCtx.currCellGrid; +} + +// NOTE: the parameter 'files' must have the file names in the loaded order +void Reader::updateModIndices(const std::vector& files) +{ + if (files.size() >= 0xff) + throw std::runtime_error("ESM4::Reader::updateModIndices too many files"); // 0xff is reserved + + // NOTE: this map is rebuilt each time this method is called (i.e. each time a file is loaded) + // Perhaps there is an opportunity to optimize this by saving the result somewhere. + // But then, the number of files is at most around 250 so perhaps keeping it simple might be better. + + // build a lookup map + std::unordered_map fileIndex; + + for (size_t i = 0; i < files.size(); ++i) // ATTENTION: assumes current file is not included + fileIndex[Misc::StringUtils::lowerCase(files[i])] = i; + + mCtx.parentFileIndices.resize(mHeader.mMaster.size()); + for (unsigned int i = 0; i < mHeader.mMaster.size(); ++i) + { + // locate the position of the dependency in already loaded files + std::unordered_map::const_iterator it + = fileIndex.find(Misc::StringUtils::lowerCase(mHeader.mMaster[i].name)); + + if (it != fileIndex.end()) + mCtx.parentFileIndices[i] = (std::uint32_t)((it->second << 24) & 0xff000000); + else + throw std::runtime_error("ESM4::Reader::updateModIndices required dependency file not loaded"); +#if 0 + std::cout << "Master Mod: " << mCtx.header.mMaster[i].name << ", " // FIXME: debugging only + << formIdToString(mCtx.parentFileIndices[i]) << std::endl; +#endif + } + + if (!mCtx.parentFileIndices.empty() && mCtx.parentFileIndices[0] != 0) + throw std::runtime_error("ESM4::Reader::updateModIndices base modIndex is not zero"); +} + +// ModIndex adjusted formId according to master file dependencies +// (see http://www.uesp.net/wiki/Tes4Mod:FormID_Fixup) +// NOTE: need to update modindex to parentFileIndices.size() before saving +// +// FIXME: probably should add a parameter to check for mCtx.header::mOverrides +// (ACHR, LAND, NAVM, PGRE, PHZD, REFR), but not sure what exactly overrides mean +// i.e. use the modindx of its master? +// FIXME: Apparently ModIndex '00' in an ESP means the object is defined in one of its masters. +// This means we may need to search multiple times to get the correct id. +// (see https://www.uesp.net/wiki/Tes4Mod:Formid#ModIndex_Zero) +void Reader::adjustFormId(FormId& id) +{ + if (mCtx.parentFileIndices.empty()) + return; + + std::size_t index = (id >> 24) & 0xff; + + if (index < mCtx.parentFileIndices.size()) + id = mCtx.parentFileIndices[index] | (id & 0x00ffffff); + else + id = mCtx.modIndex | (id & 0x00ffffff); +} + +bool Reader::getFormId(FormId& id) +{ + if (!getExact(id)) + return false; + + adjustFormId(id); + return true; +} + +void Reader::adjustGRUPFormId() +{ + adjustFormId(mCtx.recordHeader.group.label.value); +} + +[[noreturn]] void Reader::fail(const std::string& msg) +{ + std::stringstream ss; + + ss << "ESM Error: " << msg; + ss << "\n File: " << mCtx.filename; + ss << "\n Record: " << ESM::printName(mCtx.recordHeader.record.typeId); + ss << "\n Subrecord: " << ESM::printName(mCtx.subRecordHeader.typeId); + if (mStream.get()) + ss << "\n Offset: 0x" << std::hex << mStream->tellg(); + + throw std::runtime_error(ss.str()); +} + +} diff --git a/components/esm4/reader.hpp b/components/esm4/reader.hpp new file mode 100644 index 0000000000..d4979d034a --- /dev/null +++ b/components/esm4/reader.hpp @@ -0,0 +1,298 @@ +/* + Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + +*/ +#ifndef ESM4_READER_H +#define ESM4_READER_H + +#include +#include +#include +#include + +#include "common.hpp" +#include "loadtes4.hpp" +#include "../esm/reader.hpp" + +#include + +namespace ESM4 { + // bytes read from group, updated by + // getRecordHeader() in advance + // | + // v + typedef std::vector > GroupStack; + + struct ReaderContext + { + std::string filename; // in case we need to reopen to restore the context + std::uint32_t modIndex; // the sequential position of this file in the load order: + // 0x00 reserved, 0xFF in-game (see notes below) + + // position in the vector = mod index of master files above + // value = adjusted mod index based on all the files loaded so far + std::vector parentFileIndices; + + std::size_t recHeaderSize; // normally should be already set correctly, but just in + // case the file was re-opened. default = TES5 size, + // can be reduced for TES4 by setRecHeaderSize() + + std::size_t filePos; // assume that the record header will be re-read once + // the context is restored + + // for keeping track of things + std::size_t fileRead; // number of bytes read, incl. the current record + + GroupStack groupStack; // keep track of bytes left to find when a group is done + RecordHeader recordHeader; // header of the current record or group being processed + SubRecordHeader subRecordHeader; // header of the current sub record being processed + std::uint32_t recordRead; // bytes read from the sub records, incl. the current one + + FormId currWorld; // formId of current world - for grouping CELL records + FormId currCell; // formId of current cell + // FIXME: try to get rid of these two members, seem like massive hacks + CellGrid currCellGrid; // TODO: should keep a map of cell formids + bool cellGridValid; + + ReaderContext(); + }; + + class Reader : public ESM::Reader + { + Header mHeader; // ESM4 header + + ReaderContext mCtx; + + const ToUTF8::StatelessUtf8Encoder* mEncoder; + + std::size_t mFileSize; + + Files::IStreamPtr mStream; + Files::IStreamPtr mSavedStream; // mStream is saved here while using deflated memory stream + + Files::IStreamPtr mStrings; + Files::IStreamPtr mILStrings; + Files::IStreamPtr mDLStrings; + + enum LocalizedStringType + { + Type_Strings = 0, + Type_ILStrings = 1, + Type_DLStrings = 2 + }; + + struct LStringOffset + { + LocalizedStringType type; + std::uint32_t offset; + }; + + std::map mLStringIndex; + + void buildLStringIndex(const std::string& stringFile, LocalizedStringType stringType); + + inline bool hasLocalizedStrings() const { return (mHeader.mFlags & Rec_Localized) != 0; } + + void getLocalizedStringImpl(const FormId stringId, std::string& str); + + // Close the file, resets all information. + // After calling close() the structure may be reused to load a new file. + //void close(); + + // Raw opening. Opens the file and sets everything up but doesn't parse the header. + void openRaw(Files::IStreamPtr&& stream, const std::string& filename); + + // Load ES file from a new stream, parses the header. + // Closes the currently open file first, if any. + void open(Files::IStreamPtr&& stream, const std::string& filename); + + Reader() = default; + + public: + + Reader(Files::IStreamPtr&& esmStream, const std::string& filename); + ~Reader(); + + // FIXME: should be private but ESMTool uses it + void openRaw(const std::string& filename); + + void open(const std::string& filename); + + void close() final; + + inline bool isEsm4() const final { return true; } + + inline void setEncoder(const ToUTF8::StatelessUtf8Encoder* encoder) final { mEncoder = encoder; }; + + const std::vector& getGameFiles() const final { return mHeader.mMaster; } + + inline int getRecordCount() const final { return mHeader.mData.records; } + inline const std::string getAuthor() const final { return mHeader.mAuthor; } + inline int getFormat() const final { return 0; }; // prob. not relevant for ESM4 + inline const std::string getDesc() const final { return mHeader.mDesc; } + + inline std::string getFileName() const final { return mCtx.filename; }; // not used + + inline bool hasMoreRecs() const final { return (mFileSize - mCtx.fileRead) > 0; } + + // Methods added for updating loading progress bars + inline std::size_t getFileSize() const { return mFileSize; } + inline std::size_t getFileOffset() const { return mStream->tellg(); } + + // Methods added for saving/restoring context + ReaderContext getContext(); // WARN: must be called immediately after reading the record header + + bool restoreContext(const ReaderContext& ctx); // returns the result of re-reading the header + + template + inline void get(T& t) { mStream->read((char*)&t, sizeof(T)); } + + template + bool getExact(T& t) { + mStream->read((char*)&t, sizeof(T)); + return mStream->gcount() == sizeof(T); // FIXME: try/catch block needed? + } + + // for arrays + inline bool get(void* p, std::size_t size) { + mStream->read((char*)p, size); + return mStream->gcount() == (std::streamsize)size; // FIXME: try/catch block needed? + } + + // NOTE: must be called before calling getRecordHeader() + void setRecHeaderSize(const std::size_t size); + + inline unsigned int esmVersion() const { return mHeader.mData.version.ui; } + inline unsigned int numRecords() const { return mHeader.mData.records; } + + void buildLStringIndex(); + void getLocalizedString(std::string& str); + + // Read 24 bytes of header. The caller can then decide whether to process or skip the data. + bool getRecordHeader(); + + inline const RecordHeader& hdr() const { return mCtx.recordHeader; } + + const GroupTypeHeader& grp(std::size_t pos = 0) const; + + // The object setting up this reader needs to supply the file's load order index + // so that the formId's in this file can be adjusted with the file (i.e. mod) index. + void setModIndex(std::uint32_t index) final { mCtx.modIndex = (index << 24) & 0xff000000; } + void updateModIndices(const std::vector& files); + + // Maybe should throw an exception if called when not valid? + const CellGrid& currCellGrid() const; + + inline bool hasCellGrid() const { return mCtx.cellGridValid; } + + // This is set while loading a CELL record (XCLC sub record) and invalidated + // each time loading a CELL (see clearCellGrid()) + inline void setCurrCellGrid(const CellGrid& currCell) { + mCtx.cellGridValid = true; + mCtx.currCellGrid = currCell; + } + + // FIXME: This is called each time a new CELL record is read. Rather than calling this + // methos explicitly, mCellGridValid should be set automatically somehow. + // + // Cell 2c143 is loaded immedicatly after 1bdb1 and can mistakely appear to have grid 0, 1. + inline void clearCellGrid() { mCtx.cellGridValid = false; } + + // Should be set at the beginning of a CELL load + inline void setCurrCell(FormId formId) { mCtx.currCell = formId; } + + inline FormId currCell() const { return mCtx.currCell; } + + // Should be set at the beginning of a WRLD load + inline void setCurrWorld(FormId formId) { mCtx.currWorld = formId; } + + inline FormId currWorld() const { return mCtx.currWorld; } + + // Get the data part of a record + // Note: assumes the header was read correctly and nothing else was read + void getRecordData(bool dump = false); + + // Skip the data part of a record + // Note: assumes the header was read correctly (partial skip is allowed) + void skipRecordData(); + + // Skip the remaining part of the group + // Note: assumes the header was read correctly and group was pushed onto the stack + void skipGroupData(); + + // Skip the group without pushing onto the stack + // Note: assumes the header was read correctly and group was not pushed onto the stack + // (expected to be used during development only while some groups are not yet supported) + void skipGroup(); + + // Read 6 bytes of header. The caller can then decide whether to process or skip the data. + bool getSubRecordHeader(); + + // Manally update (i.e. increase) the bytes read after SUB_XXXX + inline void updateRecordRead(std::uint32_t subSize) { mCtx.recordRead += subSize; } + + inline const SubRecordHeader& subRecordHeader() const { return mCtx.subRecordHeader; } + + // Skip the data part of a subrecord + // Note: assumes the header was read correctly and nothing else was read + void skipSubRecordData(); + + // Special for a subrecord following a XXXX subrecord + void skipSubRecordData(std::uint32_t size); + + // Get a subrecord of a particular type and data type + template + bool getSubRecord(const ESM4::SubRecordTypes type, T& t) + { + ESM4::SubRecordHeader hdr; + if (!getExact(hdr) || (hdr.typeId != type) || (hdr.dataSize != sizeof(T))) + return false; + + return get(t); + } + + // ModIndex adjusted formId according to master file dependencies + void adjustFormId(FormId& id); + + bool getFormId(FormId& id); + + void adjustGRUPFormId(); + + // Note: uses the string size from the subrecord header rather than checking null termination + bool getZString(std::string& str) { + return getStringImpl(str, mCtx.subRecordHeader.dataSize, *mStream, mEncoder, true); + } + bool getString(std::string& str) { + return getStringImpl(str, mCtx.subRecordHeader.dataSize, *mStream, mEncoder); + } + + void enterGroup(); + void exitGroupCheck(); + + // for debugging only + size_t stackSize() const { return mCtx.groupStack.size(); } + + // Used for error handling + [[noreturn]] void fail(const std::string& msg); + }; +} + +#endif // ESM4_READER_H diff --git a/components/esm4/records.hpp b/components/esm4/records.hpp new file mode 100644 index 0000000000..ae2a7a78f7 --- /dev/null +++ b/components/esm4/records.hpp @@ -0,0 +1,84 @@ +#ifndef COMPONENTS_ESM4_RECORDS_H +#define COMPONENTS_ESM4_RECORDS_H + +#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 +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/components/esm4/reference.hpp b/components/esm4/reference.hpp new file mode 100644 index 0000000000..5ac94b8519 --- /dev/null +++ b/components/esm4/reference.hpp @@ -0,0 +1,68 @@ +/* + Copyright (C) 2020 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + +*/ +#ifndef ESM4_REFERENCE_H +#define ESM4_REFERENCE_H + +#include +#include + +#include "formid.hpp" + +namespace ESM4 +{ +#pragma pack(push, 1) + struct Vector3 + { + float x; + float y; + float z; + }; + + // REFR, ACHR, ACRE + struct Placement + { + Vector3 pos; + Vector3 rot; // angles are in radian, rz applied first and rx applied last + }; + + // REFR, ACHR, ACRE + struct EnableParent + { + FormId parent; + std::uint32_t flags; //0x0001 = Set Enable State Opposite Parent, 0x0002 = Pop In + }; +#pragma pack(pop) + + struct LODReference + { + FormId baseObj; + Placement placement; + float scale; + }; +} + +#endif // ESM4_REFERENCE_H diff --git a/components/esm4/script.hpp b/components/esm4/script.hpp new file mode 100644 index 0000000000..8ba8c0025c --- /dev/null +++ b/components/esm4/script.hpp @@ -0,0 +1,384 @@ +/* + Copyright (C) 2020-2021 cc9cii + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + cc9cii cc9c@iinet.net.au + + Much of the information on the data structures are based on the information + from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by + trial & error. See http://en.uesp.net/wiki for details. + + Also see https://tes5edit.github.io/fopdoc/ for FO3/FONV specific details. + +*/ +#ifndef ESM4_SCRIPT_H +#define ESM4_SCRIPT_H + +#include +#include +#include + +namespace ESM4 +{ + enum EmotionType + { + EMO_Neutral = 0, + EMO_Anger = 1, + EMO_Disgust = 2, + EMO_Fear = 3, + EMO_Sad = 4, + EMO_Happy = 5, + EMO_Surprise = 6, + EMO_Pained = 7 // FO3/FONV + }; + + enum ConditionTypeAndFlag + { + // flag + CTF_Combine = 0x01, + CTF_RunOnTarget = 0x02, + CTF_UseGlobal = 0x04, + // condition + CTF_EqualTo = 0x00, + CTF_NotEqualTo = 0x20, + CTF_GreaterThan = 0x40, + CTF_GrThOrEqTo = 0x60, + CTF_LessThan = 0x80, + CTF_LeThOrEqTo = 0xA0 + }; + + enum FunctionIndices + { + FUN_GetDistance = 1, + FUN_GetLocked = 5, + FUN_GetPos = 6, + FUN_GetAngle = 8, + FUN_GetStartingPos = 10, + FUN_GetStartingAngle = 11, + FUN_GetSecondsPassed = 12, + FUN_GetActorValue = 14, + FUN_GetCurrentTime = 18, + FUN_GetScale = 24, + FUN_IsMoving = 25, + FUN_IsTurning = 26, + FUN_GetLineOfSight = 27, + FUN_GetIsInSameCell = 32, + FUN_GetDisabled = 35, + FUN_GetMenuMode = 36, + FUN_GetDisease = 39, + FUN_GetVampire = 40, + FUN_GetClothingValue = 41, + FUN_SameFaction = 42, + FUN_SameRace = 43, + FUN_SameSex = 44, + FUN_GetDetected = 45, + FUN_GetDead = 46, + FUN_GetItemCount = 47, + FUN_GetGold = 48, + FUN_GetSleeping = 49, + FUN_GetTalkedToPC = 50, + FUN_GetScriptVariable = 53, + FUN_GetQuestRunning = 56, + FUN_GetStage = 58, + FUN_GetStageDone = 59, + FUN_GetFactionRankDifference = 60, + FUN_GetAlarmed = 61, + FUN_IsRaining = 62, + FUN_GetAttacked = 63, + FUN_GetIsCreature = 64, + FUN_GetLockLevel = 65, + FUN_GetShouldAttack = 66, + FUN_GetInCell = 67, + FUN_GetIsClass = 68, + FUN_GetIsRace = 69, + FUN_GetIsSex = 70, + FUN_GetInFaction = 71, + FUN_GetIsID = 72, + FUN_GetFactionRank = 73, + FUN_GetGlobalValue = 74, + FUN_IsSnowing = 75, + FUN_GetDisposition = 76, + FUN_GetRandomPercent = 77, + FUN_GetQuestVariable = 79, + FUN_GetLevel = 80, + FUN_GetArmorRating = 81, + FUN_GetDeadCount = 84, + FUN_GetIsAlerted = 91, + FUN_GetPlayerControlsDisabled = 98, + FUN_GetHeadingAngle = 99, + FUN_IsWeaponOut = 101, + FUN_IsTorchOut = 102, + FUN_IsShieldOut = 103, + FUN_IsFacingUp = 106, + FUN_GetKnockedState = 107, + FUN_GetWeaponAnimType = 108, + FUN_IsWeaponSkillType = 109, + FUN_GetCurrentAIPackage = 110, + FUN_IsWaiting = 111, + FUN_IsIdlePlaying = 112, + FUN_GetMinorCrimeCount = 116, + FUN_GetMajorCrimeCount = 117, + FUN_GetActorAggroRadiusViolated = 118, + FUN_GetCrime = 122, + FUN_IsGreetingPlayer = 123, + FUN_IsGuard = 125, + FUN_HasBeenEaten = 127, + FUN_GetFatiguePercentage = 128, + FUN_GetPCIsClass = 129, + FUN_GetPCIsRace = 130, + FUN_GetPCIsSex = 131, + FUN_GetPCInFaction = 132, + FUN_SameFactionAsPC = 133, + FUN_SameRaceAsPC = 134, + FUN_SameSexAsPC = 135, + FUN_GetIsReference = 136, + FUN_IsTalking = 141, + FUN_GetWalkSpeed = 142, + FUN_GetCurrentAIProcedure = 143, + FUN_GetTrespassWarningLevel = 144, + FUN_IsTrespassing = 145, + FUN_IsInMyOwnedCell = 146, + FUN_GetWindSpeed = 147, + FUN_GetCurrentWeatherPercent = 148, + FUN_GetIsCurrentWeather = 149, + FUN_IsContinuingPackagePCNear = 150, + FUN_CanHaveFlames = 153, + FUN_HasFlames = 154, + FUN_GetOpenState = 157, + FUN_GetSitting = 159, + FUN_GetFurnitureMarkerID = 160, + FUN_GetIsCurrentPackage = 161, + FUN_IsCurrentFurnitureRef = 162, + FUN_IsCurrentFurnitureObj = 163, + FUN_GetDayofWeek = 170, + FUN_GetTalkedToPCParam = 172, + FUN_IsPCSleeping = 175, + FUN_IsPCAMurderer = 176, + FUN_GetDetectionLevel = 180, + FUN_GetEquipped = 182, + FUN_IsSwimming = 185, + FUN_GetAmountSoldStolen = 190, + FUN_GetIgnoreCrime = 192, + FUN_GetPCExpelled = 193, + FUN_GetPCFactionMurder = 195, + FUN_GetPCEnemyofFaction = 197, + FUN_GetPCFactionAttack = 199, + FUN_GetDestroyed = 203, + FUN_HasMagicEffect = 214, + FUN_GetDefaultOpen = 215, + FUN_GetAnimAction = 219, + FUN_IsSpellTarget = 223, + FUN_GetVATSMode = 224, + FUN_GetPersuasionNumber = 225, + FUN_GetSandman = 226, + FUN_GetCannibal = 227, + FUN_GetIsClassDefault = 228, + FUN_GetClassDefaultMatch = 229, + FUN_GetInCellParam = 230, + FUN_GetVatsTargetHeight = 235, + FUN_GetIsGhost = 237, + FUN_GetUnconscious = 242, + FUN_GetRestrained = 244, + FUN_GetIsUsedItem = 246, + FUN_GetIsUsedItemType = 247, + FUN_GetIsPlayableRace = 254, + FUN_GetOffersServicesNow = 255, + FUN_GetUsedItemLevel = 258, + FUN_GetUsedItemActivate = 259, + FUN_GetBarterGold = 264, + FUN_IsTimePassing = 265, + FUN_IsPleasant = 266, + FUN_IsCloudy = 267, + FUN_GetArmorRatingUpperBody = 274, + FUN_GetBaseActorValue = 277, + FUN_IsOwner = 278, + FUN_IsCellOwner = 280, + FUN_IsHorseStolen = 282, + FUN_IsLeftUp = 285, + FUN_IsSneaking = 286, + FUN_IsRunning = 287, + FUN_GetFriendHit = 288, + FUN_IsInCombat = 289, + FUN_IsInInterior = 300, + FUN_IsWaterObject = 304, + FUN_IsActorUsingATorch = 306, + FUN_IsXBox = 309, + FUN_GetInWorldspace = 310, + FUN_GetPCMiscStat = 312, + FUN_IsActorEvil = 313, + FUN_IsActorAVictim = 314, + FUN_GetTotalPersuasionNumber = 315, + FUN_GetIdleDoneOnce = 318, + FUN_GetNoRumors = 320, + FUN_WhichServiceMenu = 323, + FUN_IsRidingHorse = 327, + FUN_IsInDangerousWater = 332, + FUN_GetIgnoreFriendlyHits = 338, + FUN_IsPlayersLastRiddenHorse = 339, + FUN_IsActor = 353, + FUN_IsEssential = 354, + FUN_IsPlayerMovingIntoNewSpace = 358, + FUN_GetTimeDead = 361, + FUN_GetPlayerHasLastRiddenHorse = 362, + FUN_IsChild = 365, + FUN_GetLastPlayerAction = 367, + FUN_IsPlayerActionActive = 368, + FUN_IsTalkingActivatorActor = 370, + FUN_IsInList = 372, + FUN_GetHasNote = 382, + FUN_GetHitLocation = 391, + FUN_IsPC1stPerson = 392, + FUN_GetCauseofDeath = 397, + FUN_IsLimbGone = 398, + FUN_IsWeaponInList = 399, + FUN_HasFriendDisposition = 403, + FUN_GetVATSValue = 408, + FUN_IsKiller = 409, + FUN_IsKillerObject = 410, + FUN_GetFactionCombatReaction = 411, + FUN_Exists = 415, + FUN_GetGroupMemberCount = 416, + FUN_GetGroupTargetCount = 417, + FUN_GetObjectiveCompleted = 420, + FUN_GetObjectiveDisplayed = 421, + FUN_GetIsVoiceType = 427, + FUN_GetPlantedExplosive = 428, + FUN_IsActorTalkingThroughActivator = 430, + FUN_GetHealthPercentage = 431, + FUN_GetIsObjectType = 433, + FUN_GetDialogueEmotion = 435, + FUN_GetDialogueEmotionValue = 436, + FUN_GetIsCreatureType = 438, + FUN_GetInZone = 446, + FUN_HasPerk = 449, + FUN_GetFactionRelation = 450, + FUN_IsLastIdlePlayed = 451, + FUN_GetPlayerTeammate = 454, + FUN_GetPlayerTeammateCount = 455, + FUN_GetActorCrimePlayerEnemy = 459, + FUN_GetActorFactionPlayerEnemy = 460, + FUN_IsPlayerTagSkill = 462, + FUN_IsPlayerGrabbedRef = 464, + FUN_GetDestructionStage = 471, + FUN_GetIsAlignment = 474, + FUN_GetThreatRatio = 478, + FUN_GetIsUsedItemEquipType = 480, + FUN_GetConcussed = 489, + FUN_GetMapMarkerVisible = 492, + FUN_GetPermanentActorValue = 495, + FUN_GetKillingBlowLimb = 496, + FUN_GetWeaponHealthPerc = 500, + FUN_GetRadiationLevel = 503, + FUN_GetLastHitCritical = 510, + FUN_IsCombatTarget = 515, + FUN_GetVATSRightAreaFree = 518, + FUN_GetVATSLeftAreaFree = 519, + FUN_GetVATSBackAreaFree = 520, + FUN_GetVATSFrontAreaFree = 521, + FUN_GetIsLockBroken = 522, + FUN_IsPS3 = 523, + FUN_IsWin32 = 524, + FUN_GetVATSRightTargetVisible = 525, + FUN_GetVATSLeftTargetVisible = 526, + FUN_GetVATSBackTargetVisible = 527, + FUN_GetVATSFrontTargetVisible = 528, + FUN_IsInCriticalStage = 531, + FUN_GetXPForNextLevel = 533, + FUN_GetQuestCompleted = 546, + FUN_IsGoreDisabled = 550, + FUN_GetSpellUsageNum = 555, + FUN_GetActorsInHigh = 557, + FUN_HasLoaded3D = 558, + FUN_GetReputation = 573, + FUN_GetReputationPct = 574, + FUN_GetReputationThreshold = 575, + FUN_IsHardcore = 586, + FUN_GetForceHitReaction = 601, + FUN_ChallengeLocked = 607, + FUN_GetCasinoWinningStage = 610, + FUN_PlayerInRegion = 612, + FUN_GetChallengeCompleted = 614, + FUN_IsAlwaysHardcore = 619 + }; + +#pragma pack(push, 1) + struct TargetResponseData + { + std::uint32_t emoType; // EmotionType + std::int32_t emoValue; + std::uint32_t unknown1; + std::uint32_t responseNo; // 1 byte + padding + // below FO3/FONV + FormId sound; // when 20 bytes usually 0 but there are exceptions (FO3 INFO FormId = 0x0002241f) + std::uint32_t flags; // 1 byte + padding (0x01 = use emotion anim) + }; + + struct TargetCondition + { + std::uint32_t condition; // ConditionTypeAndFlag + padding + float comparison; // WARN: can be GLOB FormId if flag set + std::uint32_t functionIndex; + std::uint32_t param1; // FIXME: if formid needs modindex adjustment or not? + std::uint32_t param2; + std::uint32_t runOn; // 0 subject, 1 target, 2 reference, 3 combat target, 4 linked reference + // below FO3/FONV/TES5 + FormId reference; + }; + + struct ScriptHeader + { + std::uint32_t unused; + std::uint32_t refCount; + std::uint32_t compiledSize; + std::uint32_t variableCount; + std::uint16_t type; // 0 object, 1 quest, 0x100 effect + std::uint16_t flag; // 0x01 enabled + }; +#pragma pack(pop) + + struct ScriptLocalVariableData + { + // SLSD + std::uint32_t index; + std::uint32_t unknown1; + std::uint32_t unknown2; + std::uint32_t unknown3; + std::uint32_t type; + std::uint32_t unknown4; + // SCVR + std::string variableName; + + void clear() { + index = 0; + type = 0; + variableName.clear(); + } + }; + + struct ScriptDefinition + { + ScriptHeader scriptHeader; + // SDCA compiled source + std::string scriptSource; + std::vector localVarData; + std::vector localRefVarIndex; + FormId globReference; + }; +} + +#endif // ESM4_SCRIPT_H diff --git a/components/esmloader/esmdata.cpp b/components/esmloader/esmdata.cpp new file mode 100644 index 0000000000..fd7d0f622c --- /dev/null +++ b/components/esmloader/esmdata.cpp @@ -0,0 +1,77 @@ +#include "esmdata.hpp" +#include "lessbyid.hpp" +#include "record.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace EsmLoader +{ + namespace + { + template + auto returnAs(F&& f) + { + using Result = decltype(std::forward(f)(ESM::Static {})); + if constexpr (!std::is_same_v) + return Result {}; + } + + template + auto withStatic(std::string_view refId, const std::vector& values, F&& f) + { + const auto it = std::lower_bound(values.begin(), values.end(), refId, LessById {}); + + if (it == values.end() || it->mId != refId) + return returnAs(std::forward(f)); + + return std::forward(f)(*it); + } + + template + auto withStatic(std::string_view refId, ESM::RecNameInts type, const EsmData& content, F&& f) + { + switch (type) + { + case ESM::REC_ACTI: return withStatic(refId, content.mActivators, std::forward(f)); + case ESM::REC_CONT: return withStatic(refId, content.mContainers, std::forward(f)); + case ESM::REC_DOOR: return withStatic(refId, content.mDoors, std::forward(f)); + case ESM::REC_STAT: return withStatic(refId, content.mStatics, std::forward(f)); + default: break; + } + + return returnAs(std::forward(f)); + } + } + + EsmData::~EsmData() {} + + std::string_view getModel(const EsmData& content, std::string_view refId, ESM::RecNameInts type) + { + return withStatic(refId, type, content, [] (const auto& v) { return std::string_view(v.mModel); }); + } + + ESM::Variant getGameSetting(const std::vector& records, std::string_view id) + { + const std::string lower = Misc::StringUtils::lowerCase(id); + auto it = std::lower_bound(records.begin(), records.end(), lower, LessById {}); + if (it == records.end() || it->mId != lower) + throw std::runtime_error("Game settings \"" + std::string(id) + "\" is not found"); + return it->mValue; + } +} diff --git a/components/esmloader/esmdata.hpp b/components/esmloader/esmdata.hpp new file mode 100644 index 0000000000..afdcc1748d --- /dev/null +++ b/components/esmloader/esmdata.hpp @@ -0,0 +1,52 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_ESMDATA_H +#define OPENMW_COMPONENTS_ESMLOADER_ESMDATA_H + +#include + +#include +#include + +namespace ESM +{ + struct Activator; + struct Cell; + struct Container; + struct Door; + struct GameSetting; + struct Land; + struct Static; + class Variant; +} + +namespace EsmLoader +{ + struct RefIdWithType + { + std::string_view mId; + ESM::RecNameInts mType; + }; + + struct EsmData + { + std::vector mActivators; + std::vector mCells; + std::vector mContainers; + std::vector mDoors; + std::vector mGameSettings; + std::vector mLands; + std::vector mStatics; + std::vector mRefIdTypes; + + EsmData() = default; + EsmData(const EsmData&) = delete; + EsmData(EsmData&&) = default; + + ~EsmData(); + }; + + std::string_view getModel(const EsmData& content, std::string_view refId, ESM::RecNameInts type); + + ESM::Variant getGameSetting(const std::vector& records, std::string_view id); +} + +#endif diff --git a/components/esmloader/lessbyid.hpp b/components/esmloader/lessbyid.hpp new file mode 100644 index 0000000000..da835c9e39 --- /dev/null +++ b/components/esmloader/lessbyid.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_COMPONENTS_CONTENT_LESSBYID_H +#define OPENMW_COMPONENTS_CONTENT_LESSBYID_H + +#include + +namespace EsmLoader +{ + struct LessById + { + template + bool operator()(const T& lhs, const T& rhs) const + { + return lhs.mId < rhs.mId; + } + + template + bool operator()(const T& lhs, std::string_view rhs) const + { + return lhs.mId < rhs; + } + }; +} + +#endif diff --git a/components/esmloader/load.cpp b/components/esmloader/load.cpp new file mode 100644 index 0000000000..d3c170bc4c --- /dev/null +++ b/components/esmloader/load.cpp @@ -0,0 +1,369 @@ +#include "load.hpp" +#include "esmdata.hpp" +#include "lessbyid.hpp" +#include "record.hpp" + +#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 + +namespace EsmLoader +{ + namespace + { + struct GetKey + { + template + decltype(auto) operator()(const T& v) const + { + return (v.mId); + } + + const ESM::CellId& operator()(const ESM::Cell& v) const + { + return v.mCellId; + } + + std::pair operator()(const ESM::Land& v) const + { + return std::pair(v.mX, v.mY); + } + + template + decltype(auto) operator()(const Record& v) const + { + return (*this)(v.mValue); + } + }; + + struct CellRecords + { + Records mValues; + std::map mByName; + std::map, std::size_t> mByPosition; + }; + + template > + struct HasId : std::false_type {}; + + template + struct HasId> : std::true_type {}; + + template + constexpr bool hasId = HasId::value; + + template + auto loadRecord(ESM::ESMReader& reader, Records& records) + -> std::enable_if_t> + { + T record; + bool deleted = false; + record.load(reader, deleted); + Misc::StringUtils::lowerCaseInPlace(record.mId); + if (Misc::ResourceHelpers::isHiddenMarker(record.mId)) + return; + records.emplace_back(deleted, std::move(record)); + } + + template + auto loadRecord(ESM::ESMReader& reader, Records& records) + -> std::enable_if_t> + { + T record; + bool deleted = false; + record.load(reader, deleted); + records.emplace_back(deleted, std::move(record)); + } + + void loadRecord(ESM::ESMReader& reader, CellRecords& records) + { + ESM::Cell record; + bool deleted = false; + record.loadNameAndData(reader, deleted); + Misc::StringUtils::lowerCaseInPlace(record.mName); + + if ((record.mData.mFlags & ESM::Cell::Interior) != 0) + { + const auto it = records.mByName.find(record.mName); + if (it == records.mByName.end()) + { + record.loadCell(reader, true); + records.mByName.emplace_hint(it, record.mName, records.mValues.size()); + records.mValues.emplace_back(deleted, std::move(record)); + } + else + { + Record& old = records.mValues[it->second]; + old.mValue.mData = record.mData; + old.mValue.loadCell(reader, true); + } + } + else + { + const std::pair position(record.mData.mX, record.mData.mY); + const auto it = records.mByPosition.find(position); + if (it == records.mByPosition.end()) + { + record.loadCell(reader, true); + records.mByPosition.emplace_hint(it, position, records.mValues.size()); + records.mValues.emplace_back(deleted, std::move(record)); + } + else + { + Record& old = records.mValues[it->second]; + old.mValue.mData = record.mData; + old.mValue.loadCell(reader, true); + } + } + } + + struct ShallowContent + { + Records mActivators; + CellRecords mCells; + Records mContainers; + Records mDoors; + Records mGameSettings; + Records mLands; + Records mStatics; + }; + + void loadRecord(const Query& query, const ESM::NAME& name, ESM::ESMReader& reader, ShallowContent& content) + { + switch (name.toInt()) + { + case ESM::REC_ACTI: + if (query.mLoadActivators) + return loadRecord(reader, content.mActivators); + break; + case ESM::REC_CELL: + if (query.mLoadCells) + return loadRecord(reader, content.mCells); + break; + case ESM::REC_CONT: + if (query.mLoadContainers) + return loadRecord(reader, content.mContainers); + break; + case ESM::REC_DOOR: + if (query.mLoadDoors) + return loadRecord(reader, content.mDoors); + break; + case ESM::REC_GMST: + if (query.mLoadGameSettings) + return loadRecord(reader, content.mGameSettings); + break; + case ESM::REC_LAND: + if (query.mLoadLands) + return loadRecord(reader, content.mLands); + break; + case ESM::REC_STAT: + if (query.mLoadStatics) + return loadRecord(reader, content.mStatics); + break; + } + + reader.skipRecord(); + } + + void loadEsm(const Query& query, ESM::ESMReader& reader, ShallowContent& content, Loading::Listener* listener) + { + Log(Debug::Info) << "Loading ESM file " << reader.getName(); + + while (reader.hasMoreRecs()) + { + const ESM::NAME recName = reader.getRecName(); + reader.getRecHeader(); + if (reader.getRecordFlags() & ESM::FLAG_Ignored) + { + reader.skipRecord(); + continue; + } + loadRecord(query, recName, reader, content); + + if (listener != nullptr) + listener->setProgress(fileProgress * reader.getFileOffset() / reader.getFileSize()); + } + } + + ShallowContent shallowLoad(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, ESM::ReadersCache& readers, + ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) + { + ShallowContent result; + + const std::set supportedFormats { + ".esm", + ".esp", + ".omwgame", + ".omwaddon", + ".project", + }; + + for (std::size_t i = 0; i < contentFiles.size(); ++i) + { + const std::string &file = contentFiles[i]; + const std::string extension = Misc::StringUtils::lowerCase(std::filesystem::path(file).extension().string()); + + if (supportedFormats.find(extension) == supportedFormats.end()) + { + Log(Debug::Warning) << "Skipping unsupported content file: " << file; + continue; + } + + if (listener != nullptr) + { + listener->setLabel(file); + listener->setProgressRange(fileProgress); + } + + const Files::MultiDirCollection& collection = fileCollections.getCollection(extension); + + const ESM::ReadersCache::BusyItem reader = readers.get(i); + reader->setEncoder(encoder); + reader->setIndex(static_cast(i)); + reader->open(collection.getPath(file).string()); + if (query.mLoadCells) + reader->resolveParentFileIndices(readers); + + loadEsm(query, *reader, result, listener); + } + + return result; + } + + struct WithType + { + ESM::RecNameInts mType; + + template + RefIdWithType operator()(const T& v) const { return {v.mId, mType}; } + }; + + template + void addRefIdsTypes(const std::vector& values, std::vector& refIdsTypes) + { + std::transform(values.begin(), values.end(), std::back_inserter(refIdsTypes), + WithType {static_cast(T::sRecordId)}); + } + + void addRefIdsTypes(EsmData& content) + { + content.mRefIdTypes.reserve( + content.mActivators.size() + + content.mContainers.size() + + content.mDoors.size() + + content.mStatics.size() + ); + + addRefIdsTypes(content.mActivators, content.mRefIdTypes); + addRefIdsTypes(content.mContainers, content.mRefIdTypes); + addRefIdsTypes(content.mDoors, content.mRefIdTypes); + addRefIdsTypes(content.mStatics, content.mRefIdTypes); + + std::sort(content.mRefIdTypes.begin(), content.mRefIdTypes.end(), LessById {}); + } + + std::vector prepareCellRecords(Records& records) + { + std::vector result; + for (Record& v : records) + if (!v.mDeleted) + result.emplace_back(std::move(v.mValue)); + return result; + } + } + + EsmData loadEsmData(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, ESM::ReadersCache& readers, ToUTF8::Utf8Encoder* encoder, + Loading::Listener* listener) + { + Log(Debug::Info) << "Loading ESM data..."; + + ShallowContent content = shallowLoad(query, contentFiles, fileCollections, readers, encoder, listener); + + std::ostringstream loaded; + + if (query.mLoadActivators) + loaded << ' ' << content.mActivators.size() << " activators,"; + if (query.mLoadCells) + loaded << ' ' << content.mCells.mValues.size() << " cells,"; + if (query.mLoadContainers) + loaded << ' ' << content.mContainers.size() << " containers,"; + if (query.mLoadDoors) + loaded << ' ' << content.mDoors.size() << " doors,"; + if (query.mLoadGameSettings) + loaded << ' ' << content.mGameSettings.size() << " game settings,"; + if (query.mLoadLands) + loaded << ' ' << content.mLands.size() << " lands,"; + if (query.mLoadStatics) + loaded << ' ' << content.mStatics.size() << " statics,"; + + Log(Debug::Info) << "Loaded" << loaded.str(); + + EsmData result; + + if (query.mLoadActivators) + result.mActivators = prepareRecords(content.mActivators, GetKey {}); + if (query.mLoadCells) + result.mCells = prepareCellRecords(content.mCells.mValues); + if (query.mLoadContainers) + result.mContainers = prepareRecords(content.mContainers, GetKey {}); + if (query.mLoadDoors) + result.mDoors = prepareRecords(content.mDoors, GetKey {}); + if (query.mLoadGameSettings) + result.mGameSettings = prepareRecords(content.mGameSettings, GetKey {}); + if (query.mLoadLands) + result.mLands = prepareRecords(content.mLands, GetKey {}); + if (query.mLoadStatics) + result.mStatics = prepareRecords(content.mStatics, GetKey {}); + + addRefIdsTypes(result); + + std::ostringstream prepared; + + if (query.mLoadActivators) + prepared << ' ' << result.mActivators.size() << " unique activators,"; + if (query.mLoadCells) + prepared << ' ' << result.mCells.size() << " unique cells,"; + if (query.mLoadContainers) + prepared << ' ' << result.mContainers.size() << " unique containers,"; + if (query.mLoadDoors) + prepared << ' ' << result.mDoors.size() << " unique doors,"; + if (query.mLoadGameSettings) + prepared << ' ' << result.mGameSettings.size() << " unique game settings,"; + if (query.mLoadLands) + prepared << ' ' << result.mLands.size() << " unique lands,"; + if (query.mLoadStatics) + prepared << ' ' << result.mStatics.size() << " unique statics,"; + + Log(Debug::Info) << "Prepared" << prepared.str(); + + return result; + } +} diff --git a/components/esmloader/load.hpp b/components/esmloader/load.hpp new file mode 100644 index 0000000000..d7a64a1c85 --- /dev/null +++ b/components/esmloader/load.hpp @@ -0,0 +1,46 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_LOAD_H +#define OPENMW_COMPONENTS_ESMLOADER_LOAD_H + +#include + +#include +#include + +namespace ToUTF8 +{ + class Utf8Encoder; +} + +namespace Files +{ + class Collections; +} + +namespace Loading +{ + class Listener; +} + +namespace EsmLoader +{ + struct EsmData; + + inline constexpr std::size_t fileProgress = 1000; + + struct Query + { + bool mLoadActivators = false; + bool mLoadCells = false; + bool mLoadContainers = false; + bool mLoadDoors = false; + bool mLoadGameSettings = false; + bool mLoadLands = false; + bool mLoadStatics = false; + }; + + EsmData loadEsmData(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, ESM::ReadersCache& readers, + ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener = nullptr); +} + +#endif diff --git a/components/esmloader/record.hpp b/components/esmloader/record.hpp new file mode 100644 index 0000000000..ec9705e823 --- /dev/null +++ b/components/esmloader/record.hpp @@ -0,0 +1,45 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_RECORD_H +#define OPENMW_COMPONENTS_ESMLOADER_RECORD_H + +#include +#include + +#include +#include +#include + +namespace EsmLoader +{ + template + struct Record + { + bool mDeleted; + T mValue; + + template + explicit Record(bool deleted, Args&& ... args) + : mDeleted(deleted) + , mValue(std::forward(args) ...) + {} + }; + + template + using Records = std::vector>; + + template + inline std::vector prepareRecords(Records& records, const GetKey& getKey) + { + const auto greaterByKey = [&] (const auto& l, const auto& r) { return getKey(r) < getKey(l); }; + const auto equalByKey = [&] (const auto& l, const auto& r) { return getKey(l) == getKey(r); }; + std::stable_sort(records.begin(), records.end(), greaterByKey); + std::vector result; + Misc::forEachUnique(records.rbegin(), records.rend(), equalByKey, [&] (const auto& v) + { + if (!v.mDeleted) + result.emplace_back(std::move(v.mValue)); + }); + return result; + } +} + +#endif diff --git a/components/fallback/validate.cpp b/components/fallback/validate.cpp index 982c709af1..e47c6f878e 100644 --- a/components/fallback/validate.cpp +++ b/components/fallback/validate.cpp @@ -1,5 +1,8 @@ #include "validate.hpp" +#include +#include + void Fallback::validate(boost::any& v, std::vector const& tokens, FallbackMap*, int) { if (v.empty()) @@ -11,13 +14,12 @@ void Fallback::validate(boost::any& v, std::vector const& tokens, F for (const auto& token : tokens) { - std::string temp = Files::EscapeHashString::processString(token); - size_t sep = temp.find(","); - if (sep < 1 || sep == temp.length() - 1 || sep == std::string::npos) + size_t sep = token.find(','); + if (sep < 1 || sep == token.length() - 1 || sep == std::string::npos) throw boost::program_options::validation_error(boost::program_options::validation_error::invalid_option_value); - std::string key(temp.substr(0, sep)); - std::string value(temp.substr(sep + 1)); + std::string key(token.substr(0, sep)); + std::string value(token.substr(sep + 1)); map->mMap[key] = value; } diff --git a/components/fallback/validate.hpp b/components/fallback/validate.hpp index 96690f50a5..49046be6fc 100644 --- a/components/fallback/validate.hpp +++ b/components/fallback/validate.hpp @@ -1,14 +1,19 @@ #ifndef OPENMW_COMPONENTS_FALLBACK_VALIDATE_H #define OPENMW_COMPONENTS_FALLBACK_VALIDATE_H -#include - -#include +#include +#include +#include // Parses and validates a fallback map from boost program_options. // Note: for boost to pick up the validate function, you need to pull in the namespace e.g. // by using namespace Fallback; +namespace boost +{ + class any; +} + namespace Fallback { diff --git a/components/files/collections.cpp b/components/files/collections.cpp index a933eb682c..3668aeadcd 100644 --- a/components/files/collections.cpp +++ b/components/files/collections.cpp @@ -20,11 +20,12 @@ namespace Files const MultiDirCollection& Collections::getCollection(const std::string& extension) const { - MultiDirCollectionContainer::iterator iter = mCollections.find(extension); + std::string ext = Misc::StringUtils::lowerCase(extension); + auto iter = mCollections.find(ext); if (iter==mCollections.end()) { std::pair result = - mCollections.insert(std::make_pair(extension, MultiDirCollection(mDirectories, extension, mFoldCase))); + mCollections.emplace(ext, MultiDirCollection(mDirectories, ext, mFoldCase)); iter = result.first; } diff --git a/components/files/configfileparser.cpp b/components/files/configfileparser.cpp new file mode 100644 index 0000000000..3d8e06ebe7 --- /dev/null +++ b/components/files/configfileparser.cpp @@ -0,0 +1,297 @@ +// This file's contents is largely lifted from boost::program_options with only minor modification. +// Its original preamble (without updated dates) from those source files is below: + +// Copyright Vladimir Prus 2002-2004. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include "configfileparser.hpp" + +#include +#include + +namespace Files +{ + namespace + { + /** Standalone parser for config files in ini-line format. + The parser is a model of single-pass lvalue iterator, and + default constructor creates past-the-end-iterator. The typical usage is: + config_file_iterator i(is, ... set of options ...), e; + for(; i !=e; ++i) { + *i; + } + + Syntax conventions: + + - config file can not contain positional options + - '#' is comment character: it is ignored together with + the rest of the line. + - variable assignments are in the form + name '=' value. + spaces around '=' are trimmed. + - Section names are given in brackets. + + The actual option name is constructed by combining current section + name and specified option name, with dot between. If section_name + already contains dot at the end, new dot is not inserted. For example: + @verbatim + [gui.accessibility] + visual_bell=yes + @endverbatim + will result in option "gui.accessibility.visual_bell" with value + "yes" been returned. + + TODO: maybe, we should just accept a pointer to options_description + class. + */ + class common_config_file_iterator + : public boost::eof_iterator + { + public: + common_config_file_iterator() { found_eof(); } + common_config_file_iterator( + const std::set& allowed_options, + bool allow_unregistered = false); + + virtual ~common_config_file_iterator() {} + + public: // Method required by eof_iterator + + void get(); + +#if BOOST_WORKAROUND(_MSC_VER, <= 1900) + void decrement() {} + void advance(difference_type) {} +#endif + + protected: // Stubs for derived classes + + // Obtains next line from the config file + // Note: really, this design is a bit ugly + // The most clean thing would be to pass 'line_iterator' to + // constructor of this class, but to avoid templating this class + // we'd need polymorphic iterator, which does not exist yet. + virtual bool getline(std::string&) { return false; } + + protected: + /** Adds another allowed option. If the 'name' ends with + '*', then all options with the same prefix are + allowed. For example, if 'name' is 'foo*', then 'foo1' and + 'foo_bar' are allowed. */ + void add_option(const char* name); + + // Returns true if 's' is a registered option name. + bool allowed_option(const std::string& s) const; + + // That's probably too much data for iterator, since + // it will be copied, but let's not bother for now. + std::set allowed_options; + // Invariant: no element is prefix of other element. + std::set allowed_prefixes; + std::string m_prefix; + bool m_allow_unregistered = false; + }; + + common_config_file_iterator::common_config_file_iterator( + const std::set& allowed_options, + bool allow_unregistered) + : allowed_options(allowed_options), + m_allow_unregistered(allow_unregistered) + { + for (std::set::const_iterator i = allowed_options.begin(); + i != allowed_options.end(); + ++i) + { + add_option(i->c_str()); + } + } + + void common_config_file_iterator::add_option(const char* name) + { + std::string s(name); + assert(!s.empty()); + if (*s.rbegin() == '*') { + s.resize(s.size() - 1); + bool bad_prefixes(false); + // If 's' is a prefix of one of allowed suffix, then + // lower_bound will return that element. + // If some element is prefix of 's', then lower_bound will + // return the next element. + std::set::iterator i = allowed_prefixes.lower_bound(s); + if (i != allowed_prefixes.end()) { + if (i->find(s) == 0) + bad_prefixes = true; + } + if (i != allowed_prefixes.begin()) { + --i; + if (s.find(*i) == 0) + bad_prefixes = true; + } + if (bad_prefixes) + boost::throw_exception(bpo::error("options '" + std::string(name) + "' and '" + + *i + "*' will both match the same " + "arguments from the configuration file")); + allowed_prefixes.insert(s); + } + } + + std::string trim_ws(const std::string& s) + { + std::string::size_type n, n2; + n = s.find_first_not_of(" \t\r\n"); + if (n == std::string::npos) + return std::string(); + else { + n2 = s.find_last_not_of(" \t\r\n"); + return s.substr(n, n2 - n + 1); + } + } + + void common_config_file_iterator::get() + { + std::string s; + std::string::size_type n; + bool found = false; + + while (this->getline(s)) { + + // strip '#' comments and whitespace + if (s.find('#') == s.find_first_not_of(" \t\r\n")) + continue; + s = trim_ws(s); + + if (!s.empty()) { + // Handle section name + if (*s.begin() == '[' && *s.rbegin() == ']') { + m_prefix = s.substr(1, s.size() - 2); + if (*m_prefix.rbegin() != '.') + m_prefix += '.'; + } + else if ((n = s.find('=')) != std::string::npos) { + + std::string name = m_prefix + trim_ws(s.substr(0, n)); + std::string value = trim_ws(s.substr(n + 1)); + + bool registered = allowed_option(name); + if (!registered && !m_allow_unregistered) + boost::throw_exception(bpo::unknown_option(name)); + + found = true; + this->value().string_key = name; + this->value().value.clear(); + this->value().value.push_back(value); + this->value().unregistered = !registered; + this->value().original_tokens.clear(); + this->value().original_tokens.push_back(name); + this->value().original_tokens.push_back(value); + break; + + } else { + boost::throw_exception(bpo::invalid_config_file_syntax(s, bpo::invalid_syntax::unrecognized_line)); + } + } + } + if (!found) + found_eof(); + } + + bool common_config_file_iterator::allowed_option(const std::string& s) const + { + std::set::const_iterator i = allowed_options.find(s); + if (i != allowed_options.end()) + return true; + // If s is "pa" where "p" is allowed prefix then + // lower_bound should find the element after "p". + // This depends on 'allowed_prefixes' invariant. + i = allowed_prefixes.lower_bound(s); + if (i != allowed_prefixes.begin() && s.find(*--i) == 0) + return true; + return false; + } + + + + template + class basic_config_file_iterator : public Files::common_config_file_iterator { + public: + basic_config_file_iterator() + { + found_eof(); + } + + /** Creates a config file parser for the specified stream. + */ + basic_config_file_iterator(std::basic_istream& is, + const std::set& allowed_options, + bool allow_unregistered = false); + + private: // base overrides + + bool getline(std::string&); + + private: // internal data + std::shared_ptr > is; + }; + + template + basic_config_file_iterator:: + basic_config_file_iterator(std::basic_istream& is, + const std::set& allowed_options, + bool allow_unregistered) + : common_config_file_iterator(allowed_options, allow_unregistered) + { + this->is.reset(&is, bpo::detail::null_deleter()); + get(); + } + + template + bool basic_config_file_iterator::getline(std::string& s) + { + std::basic_string in; + if (std::getline(*is, in)) { + s = bpo::to_internal(in); + return true; + } else { + return false; + } + } + } + + template + bpo::basic_parsed_options + parse_config_file(std::basic_istream& is, + const bpo::options_description& desc, + bool allow_unregistered) + { + std::set allowed_options; + + const std::vector >& options = desc.options(); + for (unsigned i = 0; i < options.size(); ++i) + { + const bpo::option_description& d = *options[i]; + + if (d.long_name().empty()) + boost::throw_exception( + bpo::error("abbreviated option names are not permitted in options configuration files")); + + allowed_options.insert(d.long_name()); + } + + // Parser return char strings + bpo::parsed_options result(&desc); + copy(basic_config_file_iterator( + is, allowed_options, allow_unregistered), + basic_config_file_iterator(), + back_inserter(result.options)); + // Convert char strings into desired type. + return bpo::basic_parsed_options(result); + } + + template + bpo::basic_parsed_options + parse_config_file(std::basic_istream& is, + const bpo::options_description& desc, + bool allow_unregistered); +} diff --git a/components/files/configfileparser.hpp b/components/files/configfileparser.hpp new file mode 100644 index 0000000000..60ab27a6d4 --- /dev/null +++ b/components/files/configfileparser.hpp @@ -0,0 +1,17 @@ +#ifndef COMPONENTS_FILES_CONFIGFILEPARSER_HPP +#define COMPONENTS_FILES_CONFIGFILEPARSER_HPP + +#include + +namespace Files +{ + +namespace bpo = boost::program_options; + +template +bpo::basic_parsed_options parse_config_file(std::basic_istream&, const bpo::options_description&, + bool allow_unregistered = false); + +} + +#endif // COMPONENTS_FILES_CONFIGFILEPARSER_HPP \ No newline at end of file diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 92d35a6b65..5d21bfb6d3 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -1,16 +1,21 @@ #include "configurationmanager.hpp" #include -#include +#include #include #include +#include +#include + /** * \namespace Files */ namespace Files { +namespace bpo = boost::program_options; + static const char* const openmwCfgFile = "openmw.cfg"; #if defined(_WIN32) || defined(__WINDOWS__) @@ -20,6 +25,7 @@ static const char* const applicationName = "openmw"; #endif const char* const localToken = "?local?"; +const char* const userConfigToken = "?userconfig?"; const char* const userDataToken = "?userdata?"; const char* const globalToken = "?global?"; @@ -29,18 +35,9 @@ ConfigurationManager::ConfigurationManager(bool silent) { setupTokensMapping(); - boost::filesystem::create_directories(mFixedPath.getUserConfigPath()); - boost::filesystem::create_directories(mFixedPath.getUserDataPath()); - - mLogPath = mFixedPath.getUserConfigPath(); - + // Initialize with fixed paths, will be overridden in `readConfiguration`. + mUserDataPath = mFixedPath.getUserDataPath(); mScreenshotPath = mFixedPath.getUserDataPath() / "screenshots"; - - // probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails - boost::system::error_code dirErr; - if (!boost::filesystem::create_directories(mScreenshotPath, dirErr) && !boost::filesystem::is_directory(mScreenshotPath)) { - mScreenshotPath = mFixedPath.getUserDataPath(); - } } ConfigurationManager::~ConfigurationManager() @@ -50,41 +47,146 @@ ConfigurationManager::~ConfigurationManager() void ConfigurationManager::setupTokensMapping() { mTokensMapping.insert(std::make_pair(localToken, &FixedPath<>::getLocalPath)); + mTokensMapping.insert(std::make_pair(userConfigToken, &FixedPath<>::getUserConfigPath)); mTokensMapping.insert(std::make_pair(userDataToken, &FixedPath<>::getUserDataPath)); mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); } -void ConfigurationManager::readConfiguration(boost::program_options::variables_map& variables, - boost::program_options::options_description& description, bool quiet) +static bool hasReplaceConfig(const bpo::variables_map& variables) +{ + if (variables["replace"].empty()) + return false; + for (const std::string& var : variables["replace"].as>()) + { + if (var == "config") + return true; + } + return false; +} + +void ConfigurationManager::readConfiguration(bpo::variables_map& variables, + const bpo::options_description& description, bool quiet) { bool silent = mSilent; mSilent = quiet; - - // User config has the highest priority. - auto composingVariables = separateComposingVariables(variables, description); - loadConfig(mFixedPath.getUserConfigPath(), variables, description); - mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); - - // read either local or global config depending on type of installation - composingVariables = separateComposingVariables(variables, description); - bool loaded = loadConfig(mFixedPath.getLocalPath(), variables, description); - mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); - if (!loaded) + + std::optional config = loadConfig(mFixedPath.getLocalPath(), description); + if (config) + mActiveConfigPaths.push_back(mFixedPath.getLocalPath()); + else + { + mActiveConfigPaths.push_back(mFixedPath.getGlobalConfigPath()); + config = loadConfig(mFixedPath.getGlobalConfigPath(), description); + } + if (!config) + { + if (!quiet) + Log(Debug::Error) << "Neither local config nor global config are available."; + mSilent = silent; + return; + } + + std::stack extraConfigDirs; + addExtraConfigDirs(extraConfigDirs, variables); + if (!hasReplaceConfig(variables)) + addExtraConfigDirs(extraConfigDirs, *config); + + std::vector parsedConfigs{*std::move(config)}; + std::set alreadyParsedPaths; // needed to prevent infinite loop in case of a circular link + alreadyParsedPaths.insert(boost::filesystem::path(mActiveConfigPaths.front())); + + while (!extraConfigDirs.empty()) + { + boost::filesystem::path path = extraConfigDirs.top(); + extraConfigDirs.pop(); + if (alreadyParsedPaths.count(path) > 0) + { + if (!quiet) + Log(Debug::Warning) << "Repeated config dir: " << path; + continue; + } + alreadyParsedPaths.insert(path); + config = loadConfig(path, description); + if (config && hasReplaceConfig(*config) && parsedConfigs.size() > 1) + { + mActiveConfigPaths.resize(1); + parsedConfigs.resize(1); + Log(Debug::Info) << "Skipping previous configs except " << (mActiveConfigPaths.front() / "openmw.cfg") << + " due to replace=config in " << (path / "openmw.cfg"); + } + mActiveConfigPaths.push_back(path); + if (config) + { + addExtraConfigDirs(extraConfigDirs, *config); + parsedConfigs.push_back(*std::move(config)); + } + } + + for (auto it = parsedConfigs.rbegin(); it != parsedConfigs.rend(); ++it) { - composingVariables = separateComposingVariables(variables, description); - loadConfig(mFixedPath.getGlobalConfigPath(), variables, description); + auto composingVariables = separateComposingVariables(variables, description); + for (auto& [k, v] : *it) + { + auto variable = variables.find(k); + if (variable == variables.end()) + variables.insert({k, v}); + else if (variable->second.defaulted()) + variable->second = v; + } mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); + } + + mUserDataPath = variables["user-data"].as(); + if (mUserDataPath.empty()) + { + if (!quiet) + Log(Debug::Warning) << "Error: `user-data` is not specified"; + mUserDataPath = mFixedPath.getUserDataPath(); + } + mScreenshotPath = mUserDataPath / "screenshots"; + + boost::filesystem::create_directories(getUserConfigPath()); + boost::filesystem::create_directories(mScreenshotPath); + + // probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails + if (!boost::filesystem::is_directory(mScreenshotPath)) + mScreenshotPath = mUserDataPath; + + if (!quiet) + { + Log(Debug::Info) << "Logs dir: " << getUserConfigPath().string(); + Log(Debug::Info) << "User data dir: " << mUserDataPath.string(); + Log(Debug::Info) << "Screenshots dir: " << mScreenshotPath.string(); } mSilent = silent; } -boost::program_options::variables_map ConfigurationManager::separateComposingVariables(boost::program_options::variables_map & variables, boost::program_options::options_description& description) +void ConfigurationManager::addExtraConfigDirs(std::stack& dirs, + const bpo::variables_map& variables) const +{ + auto configIt = variables.find("config"); + if (configIt == variables.end()) + return; + Files::PathContainer newDirs = asPathContainer(configIt->second.as()); + for (auto it = newDirs.rbegin(); it != newDirs.rend(); ++it) + dirs.push(*it); +} + +void ConfigurationManager::addCommonOptions(bpo::options_description& description) +{ + description.add_options() + ("config", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "") + ->multitoken()->composing(), "additional config directories") + ("replace", bpo::value>()->default_value(std::vector(), "")->multitoken()->composing(), + "settings where the values from the current source should replace those from lower-priority sources instead of being appended") + ("user-data", bpo::value()->default_value(Files::MaybeQuotedPath(), ""), + "set user data directory (used for saves, screenshots, etc)"); +} + +bpo::variables_map separateComposingVariables(bpo::variables_map & variables, const bpo::options_description& description) { - boost::program_options::variables_map composingVariables; + bpo::variables_map composingVariables; for (auto itr = variables.begin(); itr != variables.end();) { if (description.find(itr->first, false).semantic()->is_composing()) @@ -98,8 +200,20 @@ boost::program_options::variables_map ConfigurationManager::separateComposingVar return composingVariables; } -void ConfigurationManager::mergeComposingVariables(boost::program_options::variables_map & first, boost::program_options::variables_map & second, boost::program_options::options_description& description) +void mergeComposingVariables(bpo::variables_map& first, bpo::variables_map& second, + const bpo::options_description& description) { + // There are a few places this assumes all variables are present in second, but it's never crashed in the wild, so it looks like that's guaranteed. + std::set replacedVariables; + if (description.find_nothrow("replace", false)) + { + auto replace = second["replace"]; + if (!replace.defaulted() && !replace.empty()) + { + std::vector replaceVector = replace.as>(); + replacedVariables.insert(replaceVector.begin(), replaceVector.end()); + } + } for (const auto& option : description.options()) { if (option->semantic()->is_composing()) @@ -113,25 +227,30 @@ void ConfigurationManager::mergeComposingVariables(boost::program_options::varia continue; } + if (replacedVariables.count(name) || firstPosition->second.defaulted() || firstPosition->second.empty()) + { + firstPosition->second = second[name]; + continue; + } + if (second[name].defaulted() || second[name].empty()) continue; boost::any& firstValue = firstPosition->second.value(); const boost::any& secondValue = second[name].value(); - if (firstValue.type() == typeid(Files::EscapePathContainer)) + if (firstValue.type() == typeid(Files::MaybeQuotedPathContainer)) { - auto& firstPathContainer = boost::any_cast(firstValue); - const auto& secondPathContainer = boost::any_cast(secondValue); - + auto& firstPathContainer = boost::any_cast(firstValue); + const auto& secondPathContainer = boost::any_cast(secondValue); firstPathContainer.insert(firstPathContainer.end(), secondPathContainer.begin(), secondPathContainer.end()); } - else if (firstValue.type() == typeid(Files::EscapeStringVector)) + else if (firstValue.type() == typeid(std::vector)) { - auto& firstVector = boost::any_cast(firstValue); - const auto& secondVector = boost::any_cast(secondValue); + auto& firstVector = boost::any_cast&>(firstValue); + const auto& secondVector = boost::any_cast&>(secondValue); - firstVector.mVector.insert(firstVector.mVector.end(), secondVector.mVector.begin(), secondVector.mVector.end()); + firstVector.insert(firstVector.end(), secondVector.begin(), secondVector.end()); } else if (firstValue.type() == typeid(Fallback::FallbackMap)) { @@ -146,68 +265,85 @@ void ConfigurationManager::mergeComposingVariables(boost::program_options::varia Log(Debug::Error) << "Unexpected composing variable type. Curse boost and their blasted arcane templates."; } } - } -void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, bool create) +void ConfigurationManager::processPath(boost::filesystem::path& path, const boost::filesystem::path& basePath) const { - std::string path; - for (Files::PathContainer::iterator it = dataDirs.begin(); it != dataDirs.end(); ++it) + std::string str = path.string(); + + if (str.empty() || str[0] != '?') { - path = it->string(); + if (!path.is_absolute()) + path = basePath / path; + return; + } - // Check if path contains a token - if (!path.empty() && *path.begin() == '?') + std::string::size_type pos = str.find('?', 1); + if (pos != std::string::npos && pos != 0) + { + auto tokenIt = mTokensMapping.find(str.substr(0, pos + 1)); + if (tokenIt != mTokensMapping.end()) { - std::string::size_type pos = path.find('?', 1); - if (pos != std::string::npos && pos != 0) + boost::filesystem::path tempPath(((mFixedPath).*(tokenIt->second))()); + if (pos < str.length() - 1) { - TokensMappingContainer::iterator tokenIt = mTokensMapping.find(path.substr(0, pos + 1)); - if (tokenIt != mTokensMapping.end()) - { - boost::filesystem::path tempPath(((mFixedPath).*(tokenIt->second))()); - if (pos < path.length() - 1) - { - // There is something after the token, so we should - // append it to the path - tempPath /= path.substr(pos + 1, path.length() - pos); - } - - *it = tempPath; - } - else - { - // Clean invalid / unknown token, it will be removed outside the loop - (*it).clear(); - } + // There is something after the token, so we should + // append it to the path + tempPath /= str.substr(pos + 1, str.length() - pos); } - } - if (!boost::filesystem::is_directory(*it)) + path = tempPath; + } + else { - if (create) - { - try - { - boost::filesystem::create_directories (*it); - } - catch (...) {} - - if (boost::filesystem::is_directory(*it)) - continue; - } + if (!mSilent) + Log(Debug::Warning) << "Path starts with unknown token: " << path; + path.clear(); + } + } +} - (*it).clear(); +void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, const boost::filesystem::path& basePath) const +{ + for (auto& path : dataDirs) + processPath(path, basePath); +} + +void ConfigurationManager::processPaths(boost::program_options::variables_map& variables, const boost::filesystem::path& basePath) const +{ + for (auto& [name, var] : variables) + { + if (var.defaulted()) + continue; + if (var.value().type() == typeid(MaybeQuotedPathContainer)) + { + auto& pathContainer = boost::any_cast(var.value()); + for (MaybeQuotedPath& path : pathContainer) + processPath(path, basePath); + } + else if (var.value().type() == typeid(MaybeQuotedPath)) + { + boost::filesystem::path& path = boost::any_cast(var.value()); + processPath(path, basePath); } } +} +void ConfigurationManager::filterOutNonExistingPaths(Files::PathContainer& dataDirs) const +{ dataDirs.erase(std::remove_if(dataDirs.begin(), dataDirs.end(), - std::bind(&boost::filesystem::path::empty, std::placeholders::_1)), dataDirs.end()); + [this](const boost::filesystem::path& p) + { + bool exists = boost::filesystem::is_directory(p); + if (!exists && !mSilent) + Log(Debug::Warning) << "No such dir: " << p; + return !exists; + }), + dataDirs.end()); } -bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, - boost::program_options::variables_map& variables, - boost::program_options::options_description& description) +std::optional ConfigurationManager::loadConfig( + const boost::filesystem::path& path, const bpo::options_description& description) const { boost::filesystem::path cfgFile(path); cfgFile /= std::string(openmwCfgFile); @@ -216,26 +352,19 @@ bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, if (!mSilent) Log(Debug::Info) << "Loading config file: " << cfgFile.string(); - boost::filesystem::ifstream configFileStreamUnfiltered(cfgFile); - boost::iostreams::filtering_istream configFileStream; - configFileStream.push(escape_hash_filter()); - configFileStream.push(configFileStreamUnfiltered); - if (configFileStreamUnfiltered.is_open()) - { - boost::program_options::store(boost::program_options::parse_config_file( - configFileStream, description, true), variables); + boost::filesystem::ifstream configFileStream(cfgFile); - return true; - } - else + if (configFileStream.is_open()) { - if (!mSilent) - Log(Debug::Error) << "Loading failed."; - - return false; + bpo::variables_map variables; + bpo::store(Files::parse_config_file(configFileStream, description, true), variables); + processPaths(variables, path); + return variables; } + else if (!mSilent) + Log(Debug::Error) << "Loading failed."; } - return false; + return std::nullopt; } const boost::filesystem::path& ConfigurationManager::getGlobalPath() const @@ -245,12 +374,15 @@ const boost::filesystem::path& ConfigurationManager::getGlobalPath() const const boost::filesystem::path& ConfigurationManager::getUserConfigPath() const { - return mFixedPath.getUserConfigPath(); + if (mActiveConfigPaths.empty()) + return mFixedPath.getUserConfigPath(); + else + return mActiveConfigPaths.back(); } const boost::filesystem::path& ConfigurationManager::getUserDataPath() const { - return mFixedPath.getUserDataPath(); + return mUserDataPath; } const boost::filesystem::path& ConfigurationManager::getLocalPath() const @@ -273,14 +405,50 @@ const boost::filesystem::path& ConfigurationManager::getInstallPath() const return mFixedPath.getInstallPath(); } -const boost::filesystem::path& ConfigurationManager::getLogPath() const +const boost::filesystem::path& ConfigurationManager::getScreenshotPath() const { - return mLogPath; + return mScreenshotPath; } -const boost::filesystem::path& ConfigurationManager::getScreenshotPath() const +void parseArgs(int argc, const char* const argv[], bpo::variables_map& variables, + const bpo::options_description& description) { - return mScreenshotPath; + bpo::store( + bpo::command_line_parser(argc, argv).options(description).allow_unregistered().run(), + variables + ); +} + +void parseConfig(std::istream& stream, bpo::variables_map& variables, const bpo::options_description& description) +{ + bpo::store(Files::parse_config_file(stream, description, true), variables); +} + +std::istream& operator>> (std::istream& istream, MaybeQuotedPath& MaybeQuotedPath) +{ + // If the stream starts with a double quote, read from stream using boost::filesystem::path rules, then discard anything remaining. + // This prevents boost::program_options getting upset that we've not consumed the whole stream. + // If it doesn't start with a double quote, read the whole thing verbatim + if (istream.peek() == '"') + { + istream >> static_cast(MaybeQuotedPath); + if (istream && !istream.eof() && istream.peek() != EOF) + { + std::string remainder{std::istreambuf_iterator(istream), {}}; + Log(Debug::Warning) << "Trailing data in path setting. Used '" << MaybeQuotedPath.string() << "' but '" << remainder << "' remained"; + } + } + else + { + std::string intermediate{std::istreambuf_iterator(istream), {}}; + static_cast(MaybeQuotedPath) = intermediate; + } + return istream; +} + +PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathContainer) +{ + return PathContainer(MaybeQuotedPathContainer.begin(), MaybeQuotedPathContainer.end()); } -} /* namespace Cfg */ +} /* namespace Files */ diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 0ec0a1f67d..20387c1ba5 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -2,12 +2,18 @@ #define COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP #include - -#include +#include +#include #include #include +namespace boost::program_options +{ + class options_description; + class variables_map; +} + /** * \namespace Files */ @@ -23,51 +29,82 @@ struct ConfigurationManager virtual ~ConfigurationManager(); void readConfiguration(boost::program_options::variables_map& variables, - boost::program_options::options_description& description, bool quiet=false); + const boost::program_options::options_description& description, bool quiet=false); - boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map& variables, boost::program_options::options_description& description); + void filterOutNonExistingPaths(Files::PathContainer& dataDirs) const; - void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, boost::program_options::options_description& description); - - void processPaths(Files::PathContainer& dataDirs, bool create = false); - ///< \param create Try creating the directory, if it does not exist. + // Replaces tokens (`?local?`, `?global?`, etc.) in paths. Adds `basePath` prefix for relative paths. + void processPath(boost::filesystem::path& path, const boost::filesystem::path& basePath) const; + void processPaths(Files::PathContainer& dataDirs, const boost::filesystem::path& basePath) const; + void processPaths(boost::program_options::variables_map& variables, const boost::filesystem::path& basePath) const; /**< Fixed paths */ const boost::filesystem::path& getGlobalPath() const; - const boost::filesystem::path& getUserConfigPath() const; const boost::filesystem::path& getLocalPath() const; const boost::filesystem::path& getGlobalDataPath() const; + const boost::filesystem::path& getUserConfigPath() const; const boost::filesystem::path& getUserDataPath() const; const boost::filesystem::path& getLocalDataPath() const; const boost::filesystem::path& getInstallPath() const; + const std::vector& getActiveConfigPaths() const { return mActiveConfigPaths; } const boost::filesystem::path& getCachePath() const; - const boost::filesystem::path& getLogPath() const; + const boost::filesystem::path& getLogPath() const { return getUserConfigPath(); } const boost::filesystem::path& getScreenshotPath() const; + static void addCommonOptions(boost::program_options::options_description& description); + private: typedef Files::FixedPath<> FixedPathType; typedef const boost::filesystem::path& (FixedPathType::*path_type_f)() const; typedef std::map TokensMappingContainer; - bool loadConfig(const boost::filesystem::path& path, - boost::program_options::variables_map& variables, - boost::program_options::options_description& description); + std::optional loadConfig( + const boost::filesystem::path& path, + const boost::program_options::options_description& description) const; + + void addExtraConfigDirs(std::stack& dirs, + const boost::program_options::variables_map& variables) const; void setupTokensMapping(); + std::vector mActiveConfigPaths; + FixedPathType mFixedPath; - boost::filesystem::path mLogPath; + boost::filesystem::path mUserDataPath; boost::filesystem::path mScreenshotPath; TokensMappingContainer mTokensMapping; bool mSilent; }; -} /* namespace Cfg */ + +boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map& variables, + const boost::program_options::options_description& description); + +void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, + const boost::program_options::options_description& description); + +void parseArgs(int argc, const char* const argv[], boost::program_options::variables_map& variables, + const boost::program_options::options_description& description); + +void parseConfig(std::istream& stream, boost::program_options::variables_map& variables, + const boost::program_options::options_description& description); + +class MaybeQuotedPath : public boost::filesystem::path +{ +}; + +std::istream& operator>> (std::istream& istream, MaybeQuotedPath& MaybeQuotedPath); + +typedef std::vector MaybeQuotedPathContainer; + +PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathContainer); + +} /* namespace Files */ #endif /* COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP */ diff --git a/components/files/constrainedfilestream.cpp b/components/files/constrainedfilestream.cpp index d2802218f0..a56ae01aa0 100644 --- a/components/files/constrainedfilestream.cpp +++ b/components/files/constrainedfilestream.cpp @@ -1,118 +1,9 @@ #include "constrainedfilestream.hpp" -#include -#include - -#include "lowlevelfile.hpp" - -namespace -{ -// somewhat arbitrary though 64KB buffers didn't seem to improve performance any -const size_t sBufferSize = 8192; -} - namespace Files { - class ConstrainedFileStreamBuf : public std::streambuf - { - - size_t mOrigin; - size_t mSize; - - LowLevelFile mFile; - - char mBuffer[sBufferSize]; - - public: - ConstrainedFileStreamBuf(const std::string &fname, size_t start, size_t length) - { - mFile.open (fname.c_str ()); - mSize = length != 0xFFFFFFFF ? length : mFile.size () - start; - - if (start != 0) - mFile.seek(start); - - setg(0,0,0); - - mOrigin = start; - } - - int_type underflow() override - { - if(gptr() == egptr()) - { - size_t toRead = std::min((mOrigin+mSize)-(mFile.tell()), sBufferSize); - // Read in the next chunk of data, and set the read pointers on success - // Failure will throw exception in LowLevelFile - size_t got = mFile.read(mBuffer, toRead); - setg(&mBuffer[0], &mBuffer[0], &mBuffer[0]+got); - } - if(gptr() == egptr()) - return traits_type::eof(); - - return traits_type::to_int_type(*gptr()); - } - - pos_type seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) override - { - if((mode&std::ios_base::out) || !(mode&std::ios_base::in)) - return traits_type::eof(); - - // new file position, relative to mOrigin - size_t newPos; - switch (whence) - { - case std::ios_base::beg: - newPos = offset; - break; - case std::ios_base::cur: - newPos = (mFile.tell() - mOrigin - (egptr() - gptr())) + offset; - break; - case std::ios_base::end: - newPos = mSize + offset; - break; - default: - return traits_type::eof(); - } - - if (newPos > mSize) - return traits_type::eof(); - - mFile.seek(mOrigin+newPos); - - // Clear read pointers so underflow() gets called on the next read attempt. - setg(0, 0, 0); - - return newPos; - } - - pos_type seekpos(pos_type pos, std::ios_base::openmode mode) override - { - if((mode&std::ios_base::out) || !(mode&std::ios_base::in)) - return traits_type::eof(); - - if ((size_t)pos > mSize) - return traits_type::eof(); - - mFile.seek(mOrigin + pos); - - // Clear read pointers so underflow() gets called on the next read attempt. - setg(0, 0, 0); - return pos; - } - - }; - - ConstrainedFileStream::ConstrainedFileStream(std::unique_ptr buf) - : std::istream(buf.get()) - , mBuf(std::move(buf)) - { - } - - IStreamPtr openConstrainedFileStream(const char *filename, - size_t start, size_t length) + IStreamPtr openConstrainedFileStream(const std::string& filename, std::size_t start, std::size_t length) { - auto buf = std::unique_ptr(new ConstrainedFileStreamBuf(filename, start, length)); - return IStreamPtr(new ConstrainedFileStream(std::move(buf))); + return std::make_unique(std::make_unique(filename, start, length)); } } diff --git a/components/files/constrainedfilestream.hpp b/components/files/constrainedfilestream.hpp index bf67c7b973..f00bd3a488 100644 --- a/components/files/constrainedfilestream.hpp +++ b/components/files/constrainedfilestream.hpp @@ -1,26 +1,21 @@ #ifndef OPENMW_CONSTRAINEDFILESTREAM_H #define OPENMW_CONSTRAINEDFILESTREAM_H -#include -#include +#include "constrainedfilestreambuf.hpp" +#include "streamwithbuffer.hpp" +#include "istreamptr.hpp" + +#include +#include namespace Files { /// A file stream constrained to a specific region in the file, specified by the 'start' and 'length' parameters. -class ConstrainedFileStream : public std::istream -{ -public: - ConstrainedFileStream(std::unique_ptr buf); - virtual ~ConstrainedFileStream() {}; - -private: - std::unique_ptr mBuf; -}; - -typedef std::shared_ptr IStreamPtr; +using ConstrainedFileStream = StreamWithBuffer; -IStreamPtr openConstrainedFileStream(const char *filename, size_t start=0, size_t length=0xFFFFFFFF); +IStreamPtr openConstrainedFileStream(const std::string& filename, std::size_t start = 0, + std::size_t length = std::numeric_limits::max()); } diff --git a/components/files/constrainedfilestreambuf.cpp b/components/files/constrainedfilestreambuf.cpp new file mode 100644 index 0000000000..5939207193 --- /dev/null +++ b/components/files/constrainedfilestreambuf.cpp @@ -0,0 +1,85 @@ +#include "constrainedfilestreambuf.hpp" + +#include +#include + +namespace Files +{ + namespace File = Platform::File; + + ConstrainedFileStreamBuf::ConstrainedFileStreamBuf(const std::string& fname, std::size_t start, std::size_t length) + : mOrigin(start) + { + mFile = File::open(fname.c_str()); + mSize = length != std::numeric_limits::max() ? length : File::size(mFile) - start; + + if (start != 0) + File::seek(mFile, start); + + setg(nullptr, nullptr, nullptr); + } + + std::streambuf::int_type ConstrainedFileStreamBuf::underflow() + { + if (gptr() == egptr()) + { + const std::size_t toRead = std::min((mOrigin + mSize) - (File::tell(mFile)), sizeof(mBuffer)); + // Read in the next chunk of data, and set the read pointers on success + // Failure will throw exception. + const std::size_t got = File::read(mFile, mBuffer, toRead); + setg(&mBuffer[0], &mBuffer[0], &mBuffer[0] + got); + } + if (gptr() == egptr()) + return traits_type::eof(); + + return traits_type::to_int_type(*gptr()); + } + + std::streambuf::pos_type ConstrainedFileStreamBuf::seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) + { + if ((mode & std::ios_base::out) || !(mode & std::ios_base::in)) + return traits_type::eof(); + + // new file position, relative to mOrigin + size_t newPos; + switch (whence) + { + case std::ios_base::beg: + newPos = offset; + break; + case std::ios_base::cur: + newPos = (File::tell(mFile) - mOrigin - (egptr() - gptr())) + offset; + break; + case std::ios_base::end: + newPos = mSize + offset; + break; + default: + return traits_type::eof(); + } + + if (newPos > mSize) + return traits_type::eof(); + + File::seek(mFile, mOrigin + newPos); + + // Clear read pointers so underflow() gets called on the next read attempt. + setg(nullptr, nullptr, nullptr); + + return newPos; + } + + std::streambuf::pos_type ConstrainedFileStreamBuf::seekpos(pos_type pos, std::ios_base::openmode mode) + { + if ((mode & std::ios_base::out) || !(mode & std::ios_base::in)) + return traits_type::eof(); + + if (static_cast(pos) > mSize) + return traits_type::eof(); + + File::seek(mFile, mOrigin + pos); + + // Clear read pointers so underflow() gets called on the next read attempt. + setg(nullptr, nullptr, nullptr); + return pos; + } +} diff --git a/components/files/constrainedfilestreambuf.hpp b/components/files/constrainedfilestreambuf.hpp new file mode 100644 index 0000000000..bb9d6ca89e --- /dev/null +++ b/components/files/constrainedfilestreambuf.hpp @@ -0,0 +1,30 @@ +#ifndef OPENMW_CONSTRAINEDFILESTREAMBUF_H +#define OPENMW_CONSTRAINEDFILESTREAMBUF_H + +#include + +#include + +namespace Files +{ + /// A file streambuf constrained to a specific region in the file, specified by the 'start' and 'length' parameters. + class ConstrainedFileStreamBuf final : public std::streambuf + { + public: + ConstrainedFileStreamBuf(const std::string& fname, std::size_t start, std::size_t length); + + int_type underflow() final; + + pos_type seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) final; + + pos_type seekpos(pos_type pos, std::ios_base::openmode mode) final; + + private: + std::size_t mOrigin; + std::size_t mSize; + Platform::File::ScopedHandle mFile; + char mBuffer[8192]{ 0 }; + }; +} + +#endif diff --git a/components/files/escape.cpp b/components/files/escape.cpp deleted file mode 100644 index 8b11504d34..0000000000 --- a/components/files/escape.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "escape.hpp" - -#include - -namespace Files -{ - const int escape_hash_filter::sEscape = '@'; - const int escape_hash_filter::sEscapeIdentifier = 'a'; - const int escape_hash_filter::sHashIdentifier = 'h'; - - escape_hash_filter::escape_hash_filter() : mSeenNonWhitespace(false), mFinishLine(false) - { - } - - escape_hash_filter::~escape_hash_filter() - { - } - - unescape_hash_filter::unescape_hash_filter() : expectingIdentifier(false) - { - } - - unescape_hash_filter::~unescape_hash_filter() - { - } - - std::string EscapeHashString::processString(const std::string & str) - { - std::string temp = str; - - static const char hash[] = { escape_hash_filter::sEscape, escape_hash_filter::sHashIdentifier }; - Misc::StringUtils::replaceAll(temp, hash, "#", 2, 1); - - static const char escape[] = { escape_hash_filter::sEscape, escape_hash_filter::sEscapeIdentifier }; - Misc::StringUtils::replaceAll(temp, escape, "@", 2, 1); - - return temp; - } - - EscapeHashString::EscapeHashString() : mData() - { - } - - EscapeHashString::EscapeHashString(const std::string & str) : mData(EscapeHashString::processString(str)) - { - } - - EscapeHashString::EscapeHashString(const std::string & str, size_t pos, size_t len) : mData(EscapeHashString::processString(str), pos, len) - { - } - - EscapeHashString::EscapeHashString(const char * s) : mData(EscapeHashString::processString(std::string(s))) - { - } - - EscapeHashString::EscapeHashString(const char * s, size_t n) : mData(EscapeHashString::processString(std::string(s)), 0, n) - { - } - - EscapeHashString::EscapeHashString(size_t n, char c) : mData(n, c) - { - } - - template - EscapeHashString::EscapeHashString(InputIterator first, InputIterator last) : mData(EscapeHashString::processString(std::string(first, last))) - { - } - - std::string EscapeHashString::toStdString() const - { - return std::string(mData); - } - - std::istream & operator>> (std::istream & is, EscapeHashString & eHS) - { - std::string temp; - is >> temp; - eHS = EscapeHashString(temp); - return is; - } - - std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS) - { - os << eHS.mData; - return os; - } - - EscapeStringVector::EscapeStringVector() : mVector() - { - } - - EscapeStringVector::~EscapeStringVector() - { - } - - std::vector EscapeStringVector::toStdStringVector() const - { - std::vector temp = std::vector(); - for (std::vector::const_iterator it = mVector.begin(); it != mVector.end(); ++it) - { - temp.push_back(it->toStdString()); - } - return temp; - } - - // boost program options validation - - void validate(boost::any &v, const std::vector &tokens, Files::EscapeHashString * eHS, int a) - { - boost::program_options::validators::check_first_occurrence(v); - - if (v.empty()) - v = boost::any(EscapeHashString(boost::program_options::validators::get_single_string(tokens))); - } - - void validate(boost::any &v, const std::vector &tokens, EscapeStringVector *, int) - { - if (v.empty()) - v = boost::any(EscapeStringVector()); - - EscapeStringVector * eSV = boost::any_cast(&v); - - for (std::vector::const_iterator it = tokens.begin(); it != tokens.end(); ++it) - eSV->mVector.emplace_back(*it); - } - - PathContainer EscapePath::toPathContainer(const EscapePathContainer & escapePathContainer) - { - PathContainer temp; - for (EscapePathContainer::const_iterator it = escapePathContainer.begin(); it != escapePathContainer.end(); ++it) - temp.push_back(it->mPath); - return temp; - } - - std::istream & operator>> (std::istream & istream, EscapePath & escapePath) - { - boost::iostreams::filtering_istream filteredStream; - filteredStream.push(unescape_hash_filter()); - filteredStream.push(istream); - - filteredStream >> escapePath.mPath; - - return istream; - } -} diff --git a/components/files/escape.hpp b/components/files/escape.hpp deleted file mode 100644 index d01bd8d980..0000000000 --- a/components/files/escape.hpp +++ /dev/null @@ -1,191 +0,0 @@ -#ifndef COMPONENTS_FILES_ESCAPE_HPP -#define COMPONENTS_FILES_ESCAPE_HPP - -#include - -#include - -#include -#include -#include - -/** - * \namespace Files - */ -namespace Files -{ - /** - * \struct escape_hash_filter - */ - struct escape_hash_filter : public boost::iostreams::input_filter - { - static const int sEscape; - static const int sHashIdentifier; - static const int sEscapeIdentifier; - - escape_hash_filter(); - virtual ~escape_hash_filter(); - - template int get(Source & src); - - private: - std::queue mNext; - - bool mSeenNonWhitespace; - bool mFinishLine; - }; - - template - int escape_hash_filter::get(Source & src) - { - if (mNext.empty()) - { - int character = boost::iostreams::get(src); - if (character == boost::iostreams::WOULD_BLOCK) - { - mNext.push(character); - } - else if (character == EOF) - { - mSeenNonWhitespace = false; - mFinishLine = false; - mNext.push(character); - } - else if (character == '\n') - { - mSeenNonWhitespace = false; - mFinishLine = false; - mNext.push(character); - } - else if (mFinishLine) - { - mNext.push(character); - } - else if (character == '#') - { - if (mSeenNonWhitespace) - { - mNext.push(sEscape); - mNext.push(sHashIdentifier); - } - else - { - //it's fine being interpreted by Boost as a comment, and so is anything afterwards - mNext.push(character); - mFinishLine = true; - } - } - else if (character == sEscape) - { - mNext.push(sEscape); - mNext.push(sEscapeIdentifier); - } - else - { - mNext.push(character); - } - if (!mSeenNonWhitespace && !isspace(character)) - mSeenNonWhitespace = true; - } - int retval = mNext.front(); - mNext.pop(); - return retval; - } - - struct unescape_hash_filter : public boost::iostreams::input_filter - { - unescape_hash_filter(); - virtual ~unescape_hash_filter(); - - template int get(Source & src); - - private: - bool expectingIdentifier; - }; - - template - int unescape_hash_filter::get(Source & src) - { - int character; - if (!expectingIdentifier) - character = boost::iostreams::get(src); - else - { - character = escape_hash_filter::sEscape; - expectingIdentifier = false; - } - if (character == escape_hash_filter::sEscape) - { - int nextChar = boost::iostreams::get(src); - int intended; - if (nextChar == escape_hash_filter::sEscapeIdentifier) - intended = escape_hash_filter::sEscape; - else if (nextChar == escape_hash_filter::sHashIdentifier) - intended = '#'; - else if (nextChar == boost::iostreams::WOULD_BLOCK) - { - expectingIdentifier = true; - intended = nextChar; - } - else - intended = '?'; - return intended; - } - else - return character; - } - - /** - * \class EscapeHashString - */ - class EscapeHashString - { - private: - std::string mData; - public: - static std::string processString(const std::string & str); - - EscapeHashString(); - EscapeHashString(const std::string & str); - EscapeHashString(const std::string & str, size_t pos, size_t len = std::string::npos); - EscapeHashString(const char * s); - EscapeHashString(const char * s, size_t n); - EscapeHashString(size_t n, char c); - template - EscapeHashString(InputIterator first, InputIterator last); - - std::string toStdString() const; - - friend std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS); - }; - - std::istream & operator>> (std::istream & is, EscapeHashString & eHS); - - struct EscapeStringVector - { - std::vector mVector; - - EscapeStringVector(); - virtual ~EscapeStringVector(); - - std::vector toStdStringVector() const; - }; - - //boost program options validation - - void validate(boost::any &v, const std::vector &tokens, Files::EscapeHashString * eHS, int a); - - void validate(boost::any &v, const std::vector &tokens, EscapeStringVector *, int); - - struct EscapePath - { - boost::filesystem::path mPath; - - static PathContainer toPathContainer(const std::vector & escapePathContainer); - }; - - typedef std::vector EscapePathContainer; - - std::istream & operator>> (std::istream & istream, EscapePath & escapePath); -} /* namespace Files */ -#endif /* COMPONENTS_FILES_ESCAPE_HPP */ diff --git a/components/files/hash.cpp b/components/files/hash.cpp new file mode 100644 index 0000000000..b11ecbf838 --- /dev/null +++ b/components/files/hash.cpp @@ -0,0 +1,41 @@ +#include "hash.hpp" + +#include + +#include +#include +#include +#include + +namespace Files +{ + std::array getHash(const std::string& fileName, std::istream& stream) + { + std::array hash {0, 0}; + try + { + const auto start = stream.tellg(); + const auto exceptions = stream.exceptions(); + stream.exceptions(std::ios_base::badbit); + while (stream) + { + std::array value; + stream.read(value.data(), value.size()); + const std::streamsize read = stream.gcount(); + if (read == 0) + break; + std::array blockHash {0, 0}; + MurmurHash3_x64_128(value.data(), static_cast(read), hash.data(), blockHash.data()); + hash = blockHash; + } + stream.clear(); + stream.exceptions(exceptions); + stream.seekg(start); + } + catch (const std::exception& e) + { + throw std::runtime_error("Error while reading \"" + fileName + "\" to get hash: " + std::string(e.what())); + } + return hash; + } +} diff --git a/components/files/hash.hpp b/components/files/hash.hpp new file mode 100644 index 0000000000..a39961e4d5 --- /dev/null +++ b/components/files/hash.hpp @@ -0,0 +1,14 @@ +#ifndef COMPONENTS_FILES_HASH_H +#define COMPONENTS_FILES_HASH_H + +#include +#include +#include +#include + +namespace Files +{ + std::array getHash(const std::string& fileName, std::istream& stream); +} + +#endif diff --git a/components/files/istreamptr.hpp b/components/files/istreamptr.hpp new file mode 100644 index 0000000000..6dee8b7a60 --- /dev/null +++ b/components/files/istreamptr.hpp @@ -0,0 +1,12 @@ +#ifndef OPENMW_COMPONENTS_FILES_ISTREAMPTR_H +#define OPENMW_COMPONENTS_FILES_ISTREAMPTR_H + +#include +#include + +namespace Files +{ + using IStreamPtr = std::unique_ptr; +} + +#endif diff --git a/components/files/linuxpath.cpp b/components/files/linuxpath.cpp index c3dead2962..7f659e10ea 100644 --- a/components/files/linuxpath.cpp +++ b/components/files/linuxpath.cpp @@ -80,14 +80,17 @@ boost::filesystem::path LinuxPath::getGlobalConfigPath() const boost::filesystem::path LinuxPath::getLocalPath() const { boost::filesystem::path localPath("./"); - std::string binPath(pathconf(".", _PC_PATH_MAX), '\0'); const char *statusPaths[] = {"/proc/self/exe", "/proc/self/file", "/proc/curproc/exe", "/proc/curproc/file"}; for(const char *path : statusPaths) { - if (readlink(path, &binPath[0], binPath.size()) != -1) + boost::filesystem::path statusPath(path); + if (!boost::filesystem::exists(statusPath)) continue; + + statusPath = boost::filesystem::read_symlink(statusPath); + if (!boost::filesystem::is_empty(statusPath)) { - localPath = boost::filesystem::path(binPath).parent_path() / "/"; + localPath = statusPath.parent_path() / "/"; break; } } diff --git a/components/files/lowlevelfile.cpp b/components/files/lowlevelfile.cpp deleted file mode 100644 index 424527b9d9..0000000000 --- a/components/files/lowlevelfile.cpp +++ /dev/null @@ -1,328 +0,0 @@ -#include "lowlevelfile.hpp" - -#include -#include -#include - -#if FILE_API == FILE_API_POSIX -#include -#include -#include -#include -#include -#endif - -#if FILE_API == FILE_API_STDIO -/* - * - * Implementation of LowLevelFile methods using c stdio - * - */ - -LowLevelFile::LowLevelFile () -{ - mHandle = nullptr; -} - -LowLevelFile::~LowLevelFile () -{ - if (mHandle != nullptr) - fclose (mHandle); -} - -void LowLevelFile::open (char const * filename) -{ - assert (mHandle == nullptr); - - mHandle = fopen (filename, "rb"); - - if (mHandle == nullptr) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading."; - throw std::runtime_error (os.str ()); - } -} - -void LowLevelFile::close () -{ - assert (mHandle != nullptr); - - fclose (mHandle); - - mHandle = nullptr; -} - -size_t LowLevelFile::size () -{ - assert (mHandle != nullptr); - - long oldPosition = ftell (mHandle); - - if (oldPosition == -1) - throw std::runtime_error ("A query operation on a file failed."); - - if (fseek (mHandle, 0, SEEK_END) != 0) - throw std::runtime_error ("A query operation on a file failed."); - - long Size = ftell (mHandle); - - if (Size == -1) - throw std::runtime_error ("A query operation on a file failed."); - - if (fseek (mHandle, oldPosition, SEEK_SET) != 0) - throw std::runtime_error ("A query operation on a file failed."); - - return size_t (Size); -} - -void LowLevelFile::seek (size_t Position) -{ - assert (mHandle != nullptr); - - if (fseek (mHandle, Position, SEEK_SET) != 0) - throw std::runtime_error ("A seek operation on a file failed."); -} - -size_t LowLevelFile::tell () -{ - assert (mHandle != nullptr); - - long Position = ftell (mHandle); - - if (Position == -1) - throw std::runtime_error ("A query operation on a file failed."); - - return size_t (Position); -} - -size_t LowLevelFile::read (void * data, size_t size) -{ - assert (mHandle != nullptr); - - int amount = fread (data, 1, size, mHandle); - - if (amount == 0 && ferror (mHandle)) - throw std::runtime_error ("A read operation on a file failed."); - - return amount; -} - -#elif FILE_API == FILE_API_POSIX -/* - * - * Implementation of LowLevelFile methods using posix IO calls - * - */ - -LowLevelFile::LowLevelFile () -{ - mHandle = -1; -} - -LowLevelFile::~LowLevelFile () -{ - if (mHandle != -1) - ::close (mHandle); -} - -void LowLevelFile::open (char const * filename) -{ - assert (mHandle == -1); - -#ifdef O_BINARY - static const int openFlags = O_RDONLY | O_BINARY; -#else - static const int openFlags = O_RDONLY; -#endif - - mHandle = ::open (filename, openFlags, 0); - - if (mHandle == -1) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading: " << strerror(errno); - throw std::runtime_error (os.str ()); - } -} - -void LowLevelFile::close () -{ - assert (mHandle != -1); - - ::close (mHandle); - - mHandle = -1; -} - -size_t LowLevelFile::size () -{ - assert (mHandle != -1); - - size_t oldPosition = ::lseek (mHandle, 0, SEEK_CUR); - - if (oldPosition == size_t (-1)) - { - std::ostringstream os; - os << "An lseek() call failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } - - size_t Size = ::lseek (mHandle, 0, SEEK_END); - - if (Size == size_t (-1)) - { - std::ostringstream os; - os << "An lseek() call failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } - - if (lseek (mHandle, oldPosition, SEEK_SET) == -1) - { - std::ostringstream os; - os << "An lseek() call failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } - - return Size; -} - -void LowLevelFile::seek (size_t Position) -{ - assert (mHandle != -1); - - if (::lseek (mHandle, Position, SEEK_SET) == -1) - { - std::ostringstream os; - os << "An lseek() call failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } -} - -size_t LowLevelFile::tell () -{ - assert (mHandle != -1); - - size_t Position = ::lseek (mHandle, 0, SEEK_CUR); - - if (Position == size_t (-1)) - { - std::ostringstream os; - os << "An lseek() call failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } - - return Position; -} - -size_t LowLevelFile::read (void * data, size_t size) -{ - assert (mHandle != -1); - - int amount = ::read (mHandle, data, size); - - if (amount == -1) - { - std::ostringstream os; - os << "An attempt to read " << size << "bytes failed:" << strerror(errno); - throw std::runtime_error (os.str ()); - } - - return amount; -} - -#elif FILE_API == FILE_API_WIN32 - -#include -/* - * - * Implementation of LowLevelFile methods using Win32 API calls - * - */ - -LowLevelFile::LowLevelFile () -{ - mHandle = INVALID_HANDLE_VALUE; -} - -LowLevelFile::~LowLevelFile () -{ - if (mHandle != INVALID_HANDLE_VALUE) - CloseHandle (mHandle); -} - -void LowLevelFile::open (char const * filename) -{ - assert (mHandle == INVALID_HANDLE_VALUE); - - std::wstring wname = boost::locale::conv::utf_to_utf(filename); - HANDLE handle = CreateFileW (wname.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); - - if (handle == INVALID_HANDLE_VALUE) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading: " << GetLastError(); - throw std::runtime_error (os.str ()); - } - - mHandle = handle; -} - -void LowLevelFile::close () -{ - assert (mHandle != INVALID_HANDLE_VALUE); - - CloseHandle (mHandle); - - mHandle = INVALID_HANDLE_VALUE; -} - -size_t LowLevelFile::size () -{ - assert (mHandle != INVALID_HANDLE_VALUE); - - BY_HANDLE_FILE_INFORMATION info; - - if (!GetFileInformationByHandle (mHandle, &info)) - throw std::runtime_error ("A query operation on a file failed."); - - if (info.nFileSizeHigh != 0) - throw std::runtime_error ("Files greater that 4GB are not supported."); - - return info.nFileSizeLow; -} - -void LowLevelFile::seek (size_t Position) -{ - assert (mHandle != INVALID_HANDLE_VALUE); - - if (SetFilePointer (mHandle, Position, nullptr, SEEK_SET) == INVALID_SET_FILE_POINTER) - if (GetLastError () != NO_ERROR) - throw std::runtime_error ("A seek operation on a file failed."); -} - -size_t LowLevelFile::tell () -{ - assert (mHandle != INVALID_HANDLE_VALUE); - - DWORD value = SetFilePointer (mHandle, 0, nullptr, SEEK_CUR); - - if (value == INVALID_SET_FILE_POINTER && GetLastError () != NO_ERROR) - throw std::runtime_error ("A query operation on a file failed."); - - return value; -} - -size_t LowLevelFile::read (void * data, size_t size) -{ - assert (mHandle != INVALID_HANDLE_VALUE); - - DWORD read; - - if (!ReadFile (mHandle, data, size, &read, nullptr)) - throw std::runtime_error ("A read operation on a file failed."); - - return read; -} - -#endif diff --git a/components/files/lowlevelfile.hpp b/components/files/lowlevelfile.hpp deleted file mode 100644 index b2634d8c76..0000000000 --- a/components/files/lowlevelfile.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef COMPONENTS_FILES_LOWLEVELFILE_HPP -#define COMPONENTS_FILES_LOWLEVELFILE_HPP - -#include - -#define FILE_API_STDIO 0 -#define FILE_API_POSIX 1 -#define FILE_API_WIN32 2 - -#if defined(__linux) || defined(__unix) || defined(__posix) -#define FILE_API FILE_API_POSIX -#elif defined(_WIN32) -#define FILE_API FILE_API_WIN32 -#else -#define FILE_API FILE_API_STDIO -#endif - -#if FILE_API == FILE_API_STDIO -#include -#elif FILE_API == FILE_API_POSIX -#elif FILE_API == FILE_API_WIN32 -#include -#else -#error Unsupported File API -#endif - -class LowLevelFile -{ -public: - - LowLevelFile (); - ~LowLevelFile (); - - void open (char const * filename); - void close (); - - size_t size (); - - void seek (size_t Position); - size_t tell (); - - size_t read (void * data, size_t size); - -private: -#if FILE_API == FILE_API_STDIO - FILE* mHandle; -#elif FILE_API == FILE_API_POSIX - int mHandle; -#elif FILE_API == FILE_API_WIN32 - HANDLE mHandle; -#endif -}; - -#endif diff --git a/components/files/multidircollection.cpp b/components/files/multidircollection.cpp index 98e25fcc86..a052b3016e 100644 --- a/components/files/multidircollection.cpp +++ b/components/files/multidircollection.cpp @@ -16,22 +16,7 @@ namespace Files { if (mStrict) return left==right; - - std::size_t len = left.length(); - - if (len!=right.length()) - return false; - - for (std::size_t i=0; ir) - return false; - } - - return left.length() +#include + +#if defined(_WIN32) || defined(__WINDOWS__) +#include +#endif + +namespace Files +{ + std::unique_ptr openBinaryInputFileStream(const std::string& path) + { +#if defined(_WIN32) || defined(__WINDOWS__) + std::wstring wpath = boost::locale::conv::utf_to_utf(path); + auto stream = std::make_unique(wpath, std::ios::binary); +#else + auto stream = std::make_unique(path, std::ios::binary); +#endif + if (!stream->is_open()) + throw std::runtime_error("Failed to open '" + path + "' for reading: " + std::strerror(errno)); + stream->exceptions(std::ios::badbit); + return stream; + } +} diff --git a/components/files/openfile.hpp b/components/files/openfile.hpp new file mode 100644 index 0000000000..3cecf7bac1 --- /dev/null +++ b/components/files/openfile.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_COMPONENTS_FILES_OPENFILE_H +#define OPENMW_COMPONENTS_FILES_OPENFILE_H + +#include +#include +#include + +namespace Files +{ + std::unique_ptr openBinaryInputFileStream(const std::string& path); +} + +#endif diff --git a/components/files/streamwithbuffer.hpp b/components/files/streamwithbuffer.hpp new file mode 100644 index 0000000000..dfd1a04376 --- /dev/null +++ b/components/files/streamwithbuffer.hpp @@ -0,0 +1,23 @@ +#ifndef OPENMW_COMPONENTS_FILES_STREAMWITHBUFFER_H +#define OPENMW_COMPONENTS_FILES_STREAMWITHBUFFER_H + +#include +#include + +namespace Files +{ + template + class StreamWithBuffer final : public std::istream + { + public: + explicit StreamWithBuffer(std::unique_ptr&& buffer) + : std::istream(buffer.get()) + , mBuffer(std::move(buffer)) + {} + + private: + std::unique_ptr mBuffer; + }; +} + +#endif diff --git a/components/files/windowspath.cpp b/components/files/windowspath.cpp index 92d1a9ff09..8a17acec9d 100644 --- a/components/files/windowspath.cpp +++ b/components/files/windowspath.cpp @@ -4,10 +4,16 @@ #include +#define FAR +#define NEAR + #include #include #include +#undef NEAR +#undef FAR + #include namespace bconv = boost::locale::conv; diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 605d552435..ffa1593318 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -1,13 +1,14 @@ #include "fontloader.hpp" #include +#include +#include #include #include #include -#include #include #include #include @@ -15,8 +16,11 @@ #include +#include + #include +#include #include #include @@ -25,8 +29,10 @@ namespace { - unsigned long utf8ToUnicode(const std::string& utf8) + unsigned long utf8ToUnicode(std::string_view utf8) { + if (utf8.empty()) + return 0; size_t i = 0; unsigned long unicode; size_t numbytes; @@ -75,69 +81,69 @@ namespace return unicode; } - // getUtf8, aka the worst function ever written. - // This includes various hacks for dealing with Morrowind's .fnt files that are *mostly* + /// This is a hack for Polish font + unsigned char mapUtf8Char(unsigned char c) { + switch(c){ + case 0x80: return 0xc6; + case 0x81: return 0x9c; + case 0x82: return 0xe6; + case 0x83: return 0xb3; + case 0x84: return 0xf1; + case 0x85: return 0xb9; + case 0x86: return 0xbf; + case 0x87: return 0x9f; + case 0x88: return 0xea; + case 0x89: return 0xea; + case 0x8a: return 0x00; // not contained in win1250 + case 0x8b: return 0x00; // not contained in win1250 + case 0x8c: return 0x8f; + case 0x8d: return 0xaf; + case 0x8e: return 0xa5; + case 0x8f: return 0x8c; + case 0x90: return 0xca; + case 0x93: return 0xa3; + case 0x94: return 0xf6; + case 0x95: return 0xf3; + case 0x96: return 0xaf; + case 0x97: return 0x8f; + case 0x99: return 0xd3; + case 0x9a: return 0xd1; + case 0x9c: return 0x00; // not contained in win1250 + case 0xa0: return 0xb9; + case 0xa1: return 0xaf; + case 0xa2: return 0xf3; + case 0xa3: return 0xbf; + case 0xa4: return 0x00; // not contained in win1250 + case 0xe1: return 0x8c; + case 0xe3: return 0x00; // not contained in win1250 + case 0xf5: return 0x00; // not contained in win1250 + default: return c; + } + } + + // getUnicode includes various hacks for dealing with Morrowind's .fnt files that are *mostly* // in the expected win12XX encoding, but also have randomly swapped characters sometimes. // Looks like the Morrowind developers found standard encodings too boring and threw in some twists for fun. - std::string getUtf8 (unsigned char c, ToUTF8::Utf8Encoder& encoder, ToUTF8::FromType encoding) + unsigned long getUnicode(unsigned char c, ToUTF8::Utf8Encoder& encoder, ToUTF8::FromType encoding) { - if (encoding == ToUTF8::WINDOWS_1250) + if (encoding == ToUTF8::WINDOWS_1250) // Hack for polish font { - // Hacks for polish font - unsigned char win1250; - std::map conv; - conv[0x80] = 0xc6; - conv[0x81] = 0x9c; - conv[0x82] = 0xe6; - conv[0x83] = 0xb3; - conv[0x84] = 0xf1; - conv[0x85] = 0xb9; - conv[0x86] = 0xbf; - conv[0x87] = 0x9f; - conv[0x88] = 0xea; - conv[0x89] = 0xea; - conv[0x8a] = 0x0; // not contained in win1250 - conv[0x8b] = 0x0; // not contained in win1250 - conv[0x8c] = 0x8f; - conv[0x8d] = 0xaf; - conv[0x8e] = 0xa5; - conv[0x8f] = 0x8c; - conv[0x90] = 0xca; - conv[0x93] = 0xa3; - conv[0x94] = 0xf6; - conv[0x95] = 0xf3; - conv[0x96] = 0xaf; - conv[0x97] = 0x8f; - conv[0x99] = 0xd3; - conv[0x9a] = 0xd1; - conv[0x9c] = 0x0; // not contained in win1250 - conv[0xa0] = 0xb9; - conv[0xa1] = 0xaf; - conv[0xa2] = 0xf3; - conv[0xa3] = 0xbf; - conv[0xa4] = 0x0; // not contained in win1250 - conv[0xe1] = 0x8c; - // Can't remember if this was supposed to read 0xe2, or is it just an extraneous copypaste? - //conv[0xe1] = 0x8c; - conv[0xe3] = 0x0; // not contained in win1250 - conv[0xf5] = 0x0; // not contained in win1250 - - if (conv.find(c) != conv.end()) - win1250 = conv[c]; - else - win1250 = c; - return encoder.getUtf8(std::string(1, win1250)); + const std::array str {static_cast(mapUtf8Char(c)), '\0'}; + return utf8ToUnicode(encoder.getUtf8(std::string_view(str.data(), 1))); } else - return encoder.getUtf8(std::string(1, c)); + { + const std::array str {static_cast(c), '\0'}; + return utf8ToUnicode(encoder.getUtf8(std::string_view(str.data(), 1))); + } } - void fail (Files::IStreamPtr file, const std::string& fileName, const std::string& message) + [[noreturn]] void fail(std::istream& stream, const std::string& fileName, const std::string& message) { std::stringstream error; error << "Font loading error: " << message; error << "\n File: " << fileName; - error << "\n Offset: 0x" << std::hex << file->tellg(); + error << "\n Offset: 0x" << std::hex << stream.tellg(); throw std::runtime_error(error.str()); } @@ -146,19 +152,16 @@ namespace namespace Gui { - FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath) + FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor) : mVFS(vfs) - , mUserDataPath(userDataPath) - , mFontHeight(16) + , mFontHeight(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)) + , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) mEncoding = ToUTF8::CP437; else mEncoding = encoding; - int fontSize = Settings::Manager::getInt("font size", "GUI"); - mFontHeight = std::min(std::max(12, fontSize), 20); - MyGUI::ResourceManager::getInstance().unregisterLoadXmlDelegate("Resource"); MyGUI::ResourceManager::getInstance().registerLoadXmlDelegate("Resource") = MyGUI::newDelegate(this, &FontLoader::loadFontFromXml); } @@ -193,26 +196,12 @@ namespace Gui mFonts.clear(); } - void FontLoader::loadBitmapFonts(bool exportToFile) + void FontLoader::loadBitmapFonts() { - const std::map& index = mVFS->getIndex(); - - std::string pattern = "Fonts/"; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& path : mVFS->getRecursiveDirectoryIterator("Fonts/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".fnt") == 0) - loadBitmapFont(name, exportToFile); - } - else - break; - ++found; + if (Misc::getFileExtension(path) == "fnt") + loadBitmapFont(path); } } @@ -225,16 +214,17 @@ namespace Gui return; } - const std::string cfg = dataManager->getDataPath(""); - const std::string fontFile = mUserDataPath + "/" + "Fonts" + "/" + "openmw_font.xml"; - if (!boost::filesystem::exists(fontFile)) - return; + std::string oldDataPath = dataManager->getDataPath(""); + dataManager->setResourcePath("fonts"); - dataManager->setResourcePath(mUserDataPath + "/" + "Fonts"); - MyGUI::ResourceManager::getInstance().load("openmw_font.xml"); - dataManager->setResourcePath(cfg); - } + for (const auto& path : mVFS->getRecursiveDirectoryIterator("Fonts/")) + { + if (Misc::getFileExtension(path) == "omwfont") + MyGUI::ResourceManager::getInstance().load(std::string(Misc::getFileName(path))); + } + dataManager->setResourcePath(oldDataPath); + } typedef struct { @@ -256,40 +246,40 @@ namespace Gui float ascent; } GlyphInfo; - void FontLoader::loadBitmapFont(const std::string &fileName, bool exportToFile) + void FontLoader::loadBitmapFont(const std::string &fileName) { Files::IStreamPtr file = mVFS->get(fileName); float fontSize; file->read((char*)&fontSize, sizeof(fontSize)); if (!file->good()) - fail(file, fileName, "File too small to be a valid font"); + fail(*file, fileName, "File too small to be a valid font"); int one; file->read((char*)&one, sizeof(one)); if (!file->good()) - fail(file, fileName, "File too small to be a valid font"); + fail(*file, fileName, "File too small to be a valid font"); if (one != 1) - fail(file, fileName, "Unexpected value"); + fail(*file, fileName, "Unexpected value"); file->read((char*)&one, sizeof(one)); if (!file->good()) - fail(file, fileName, "File too small to be a valid font"); + fail(*file, fileName, "File too small to be a valid font"); if (one != 1) - fail(file, fileName, "Unexpected value"); + fail(*file, fileName, "Unexpected value"); char name_[284]; file->read(name_, sizeof(name_)); if (!file->good()) - fail(file, fileName, "File too small to be a valid font"); + fail(*file, fileName, "File too small to be a valid font"); std::string name(name_); GlyphInfo data[256]; file->read((char*)data, sizeof(data)); if (!file->good()) - fail(file, fileName, "File too small to be a valid font"); + fail(*file, fileName, "File too small to be a valid font"); file.reset(); @@ -303,36 +293,19 @@ namespace Gui bitmapFile->read((char*)&height, sizeof(int)); if (!bitmapFile->good()) - fail(bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); + fail(*bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); if (width <= 0 || height <= 0) - fail(bitmapFile, bitmapFilename, "Width and height must be positive"); + fail(*bitmapFile, bitmapFilename, "Width and height must be positive"); std::vector textureData; textureData.resize(width*height*4); bitmapFile->read(&textureData[0], width*height*4); if (!bitmapFile->good()) - fail(bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); + fail(*bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); bitmapFile.reset(); - std::string resourceName; - if (name.size() >= 5 && Misc::StringUtils::ciEqual(name.substr(0, 5), "magic")) - resourceName = "Magic Cards"; - else if (name.size() >= 7 && Misc::StringUtils::ciEqual(name.substr(0, 7), "century")) - resourceName = "Century Gothic"; - else if (name.size() >= 7 && Misc::StringUtils::ciEqual(name.substr(0, 7), "daedric")) - resourceName = "Daedric"; - - if (exportToFile) - { - osg::ref_ptr image = new osg::Image; - image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); - assert (image->isDataContiguous()); - memcpy(image->data(), &textureData[0], textureData.size()); - - Log(Debug::Info) << "Writing " << resourceName + ".png"; - osgDB::writeImageFile(*image, resourceName + ".png"); - } + std::string resourceName = name; // Register the font with MyGUI MyGUI::ResourceManualFont* font = static_cast( @@ -357,7 +330,9 @@ namespace Gui // We need to emulate loading from XML because the data members are private as of mygui 3.2.0 MyGUI::xml::Document xmlDocument; MyGUI::xml::ElementPtr root = xmlDocument.createRoot("ResourceManualFont"); - root->addAttribute("name", resourceName); + + std::string baseName(Misc::stemFile(fileName)); + root->addAttribute("name", getInternalFontName(baseName)); MyGUI::xml::ElementPtr defaultHeight = root->createChild("Property"); defaultHeight->addAttribute("key", "DefaultHeight"); @@ -375,7 +350,7 @@ namespace Gui float h = data[i].bottom_left.y*height - y1; ToUTF8::Utf8Encoder encoder(mEncoding); - unsigned long unicodeVal = utf8ToUnicode(getUtf8(i, encoder, mEncoding)); + unsigned long unicodeVal = getUnicode(i, encoder, mEncoding); MyGUI::xml::ElementPtr code = codes->createChild("Code"); code->addAttribute("index", unicodeVal); @@ -508,21 +483,13 @@ namespace Gui cursorCode->addAttribute("size", "0 0"); } - if (exportToFile) - { - Log(Debug::Info) << "Writing " << resourceName + ".xml"; - xmlDocument.createDeclaration(); - xmlDocument.save(resourceName + ".xml"); - } - font->deserialization(root, MyGUI::Version(3,2,0)); - // Setup "book" version of font as fallback if we will not use TrueType fonts MyGUI::ResourceManualFont* bookFont = static_cast( MyGUI::FactoryManager::getInstance().createObject("Resource", "ResourceManualFont")); mFonts.push_back(bookFont); bookFont->deserialization(root, MyGUI::Version(3,2,0)); - bookFont->setResourceName("Journalbook " + resourceName); + bookFont->setResourceName("Journalbook " + getInternalFontName(baseName)); // Remove automatically registered fonts for (std::vector::iterator it = mFonts.begin(); it != mFonts.end();) @@ -566,10 +533,7 @@ namespace Gui // to allow to configure font size via config file, without need to edit XML files. // Also we should take UI scaling factor in account. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); - - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - resolution *= uiScale; + resolution = std::clamp(resolution, 48, 960) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); @@ -578,6 +542,8 @@ namespace Gui MyGUI::xml::ElementPtr sizeNode = resourceNode->createChild("Property"); sizeNode->addAttribute("key", "Size"); sizeNode->addAttribute("value", std::to_string(mFontHeight)); + + resourceNode->setAttribute("name", getInternalFontName(name)); } else if (Misc::StringUtils::ciEqual(type, "ResourceSkin") || Misc::StringUtils::ciEqual(type, "AutoSizedResourceSkin")) @@ -593,7 +559,7 @@ namespace Gui if (createCopy) { - MyGUI::xml::ElementPtr copy = _node->createCopy(); + std::unique_ptr copy{_node->createCopy()}; MyGUI::xml::ElementEnumerator copyFont = copy->getElementEnumerator(); while (copyFont.next("Resource")) @@ -611,7 +577,7 @@ namespace Gui // setup separate fonts with different Resolution to fit these windows. // These fonts have an internal prefix. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); + resolution = std::clamp(resolution, 48, 960); float currentX = Settings::Manager::getInt("resolution x", "Video"); float currentY = Settings::Manager::getInt("resolution y", "Video"); @@ -625,11 +591,11 @@ namespace Gui resolutionNode->addAttribute("key", "Resolution"); resolutionNode->addAttribute("value", std::to_string(resolution)); - copyFont->setAttribute("name", "Journalbook " + name); + copyFont->setAttribute("name", "Journalbook " + getInternalFontName(name)); } } - MyGUI::ResourceManager::getInstance().loadFromXmlNode(copy, _file, _version); + MyGUI::ResourceManager::getInstance().loadFromXmlNode(copy.get(), _file, _version); } } @@ -637,4 +603,36 @@ namespace Gui { return mFontHeight; } + + std::string FontLoader::getInternalFontName(const std::string& name) + { + const std::string lowerName = Misc::StringUtils::lowerCase(name); + + if (lowerName == Misc::StringUtils::lowerCase(Fallback::Map::getString("Fonts_Font_0"))) + return "DefaultFont"; + if (lowerName == Misc::StringUtils::lowerCase(Fallback::Map::getString("Fonts_Font_2"))) + return "ScrollFont"; + if (lowerName == "dejavusansmono") + return "MonoFont"; // We need to use a TrueType monospace font to display debug texts properly. + + // Use our TrueType fonts as a fallback. + if (!MyGUI::ResourceManager::getInstance().isExist("DefaultFont") && name == "pelagiad") + return "DefaultFont"; + if (!MyGUI::ResourceManager::getInstance().isExist("ScrollFont") && name == "ayembedt") + return "ScrollFont"; + + return name; + } + + std::string FontLoader::getFontForFace(const std::string& face) + { + const std::string lowerFace = Misc::StringUtils::lowerCase(face); + + if (lowerFace == "magic cards") + return "DefaultFont"; + if (lowerFace == "daedric") + return "ScrollFont"; + + return face; + } } diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp index 94b0225016..1f1137614e 100644 --- a/components/fontloader/fontloader.hpp +++ b/components/fontloader/fontloader.hpp @@ -1,8 +1,6 @@ #ifndef OPENMW_COMPONENTS_FONTLOADER_H #define OPENMW_COMPONENTS_FONTLOADER_H -#include "boost/filesystem/operations.hpp" - #include #include @@ -27,28 +25,30 @@ namespace Gui class FontLoader { public: - FontLoader (ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath); + FontLoader (ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor); ~FontLoader(); - /// @param exportToFile export the converted fonts (Images and XML with glyph metrics) to files? - void loadBitmapFonts (bool exportToFile); + void loadBitmapFonts (); void loadTrueTypeFonts (); void loadFontFromXml(MyGUI::xml::ElementPtr _node, const std::string& _file, MyGUI::Version _version); int getFontHeight(); + static std::string getFontForFace(const std::string& face); + private: ToUTF8::FromType mEncoding; const VFS::Manager* mVFS; - std::string mUserDataPath; int mFontHeight; + float mScalingFactor; std::vector mTextures; std::vector mFonts; - /// @param exportToFile export the converted font (Image and XML with glyph metrics) to files? - void loadBitmapFont (const std::string& fileName, bool exportToFile); + std::string getInternalFontName(const std::string& name); + + void loadBitmapFont (const std::string& fileName); FontLoader(const FontLoader&); void operator=(const FontLoader&); diff --git a/components/fx/lexer.cpp b/components/fx/lexer.cpp new file mode 100644 index 0000000000..d416dd692d --- /dev/null +++ b/components/fx/lexer.cpp @@ -0,0 +1,301 @@ +#include "lexer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "types.hpp" + +namespace fx +{ + namespace Lexer + { + Lexer::Lexer(std::string_view buffer) + : mHead(buffer.data()) + , mTail(mHead + buffer.length()) + , mAbsolutePos(0) + , mColumn(0) + , mLine(0) + , mBuffer(buffer) + , mLastToken(Eof{}) + { } + + Token Lexer::next() + { + if (mLookahead) + { + auto token = *mLookahead; + drop(); + return token; + } + + mLastToken = scanToken(); + + return mLastToken; + } + + Token Lexer::peek() + { + if (!mLookahead) + mLookahead = scanToken(); + + return *mLookahead; + } + + void Lexer::drop() + { + mLookahead = std::nullopt; + } + + std::optional Lexer::jump() + { + bool multi = false; + bool single = false; + auto start = mHead; + std::size_t level = 1; + + mLastJumpBlock.line = mLine; + + if (head() == '}') + { + mLastJumpBlock.content = {}; + return mLastJumpBlock.content; + } + + for (; mHead != mTail; advance()) + { + if (head() == '\n') + { + mLine++; + mColumn = 0; + if (single) + { + single = false; + continue; + } + } + else if (multi && head() == '*' && peekChar('/')) + { + multi = false; + advance(); + continue; + } + else if (multi || single) + { + continue; + } + else if (head() == '/' && peekChar('/')) + { + single = true; + advance(); + continue; + } + else if (head() == '/' && peekChar('*')) + { + multi = true; + advance(); + continue; + } + + if (head() == '{') + level++; + else if (head() == '}') + level--; + + if (level == 0) + { + mHead--; + auto sv = std::string_view{start, static_cast(mHead + 1 - start)}; + mLastJumpBlock.content = sv; + return sv; + } + } + + mLastJumpBlock = {}; + return std::nullopt; + } + + Lexer::Block Lexer::getLastJumpBlock() const + { + return mLastJumpBlock; + } + + [[noreturn]] void Lexer::error(const std::string& msg) + { + throw LexerException(Misc::StringUtils::format("Line %zu Col %zu. %s", mLine + 1, mColumn, msg)); + } + + void Lexer::advance() + { + mAbsolutePos++; + mHead++; + mColumn++; + } + + char Lexer::head() + { + return *mHead; + } + + bool Lexer::peekChar(char c) + { + if (mHead == mTail) + return false; + return *(mHead + 1) == c; + } + + Token Lexer::scanToken() + { + while (true) + { + if (mHead == mTail) + return {Eof{}}; + + if (head() == '\n') + { + mLine++; + mColumn = 0; + } + + if (!std::isspace(head())) + break; + + advance(); + } + + if (head() == '\"') + return scanStringLiteral(); + + if (std::isalpha(head())) + return scanLiteral(); + + if (std::isdigit(head()) || head() == '.' || head() == '-') + return scanNumber(); + + switch(head()) + { + case '=': + advance(); + return {Equal{}}; + case '{': + advance(); + return {Open_bracket{}}; + case '}': + advance(); + return {Close_bracket{}}; + case '(': + advance(); + return {Open_Parenthesis{}}; + case ')': + advance(); + return {Close_Parenthesis{}}; + case '\"': + advance(); + return {Quote{}}; + case ':': + advance(); + return {Colon{}}; + case ';': + advance(); + return {SemiColon{}}; + case '|': + advance(); + return {VBar{}}; + case ',': + advance(); + return {Comma{}}; + default: + error(Misc::StringUtils::format("unexpected token <%c>", head())); + } + } + + Token Lexer::scanLiteral() + { + auto start = mHead; + advance(); + + while (mHead != mTail && (std::isalnum(head()) || head() == '_')) + advance(); + + std::string_view value{start, static_cast(mHead - start)}; + + if (value == "shared") return Shared{}; + if (value == "technique") return Technique{}; + if (value == "main_pass") return Main_Pass{}; + if (value == "render_target") return Render_Target{}; + if (value == "vertex") return Vertex{}; + if (value == "fragment") return Fragment{}; + if (value == "compute") return Compute{}; + if (value == "sampler_1d") return Sampler_1D{}; + if (value == "sampler_2d") return Sampler_2D{}; + if (value == "sampler_3d") return Sampler_3D{}; + if (value == "uniform_bool") return Uniform_Bool{}; + if (value == "uniform_float") return Uniform_Float{}; + if (value == "uniform_int") return Uniform_Int{}; + if (value == "uniform_vec2") return Uniform_Vec2{}; + if (value == "uniform_vec3") return Uniform_Vec3{}; + if (value == "uniform_vec4") return Uniform_Vec4{}; + if (value == "true") return True{}; + if (value == "false") return False{}; + if (value == "vec2") return Vec2{}; + if (value == "vec3") return Vec3{}; + if (value == "vec4") return Vec4{}; + + return Literal{value}; + } + + Token Lexer::scanStringLiteral() + { + advance(); // consume quote + auto start = mHead; + + bool terminated = false; + + for (; mHead != mTail; advance()) + { + if (head() == '\"') + { + terminated = true; + advance(); + break; + } + } + + if (!terminated) + error("unterminated string"); + + return String{{start, static_cast(mHead - start - 1)}}; + } + + Token Lexer::scanNumber() + { + double buffer; + + char* endPtr; + buffer = std::strtod(mHead, &endPtr); + + if (endPtr == nullptr) + error("critical error while parsing number"); + + const char* tmp = mHead; + mHead = endPtr; + + for (; tmp != endPtr; ++tmp) + { + if ((*tmp == '.')) + return Float{static_cast(buffer)}; + } + + return Integer{static_cast(buffer)}; + } + } +} diff --git a/components/fx/lexer.hpp b/components/fx/lexer.hpp new file mode 100644 index 0000000000..e24239399c --- /dev/null +++ b/components/fx/lexer.hpp @@ -0,0 +1,75 @@ +#ifndef OPENMW_COMPONENTS_FX_LEXER_H +#define OPENMW_COMPONENTS_FX_LEXER_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "lexer_types.hpp" + +namespace fx +{ + namespace Lexer + { + struct LexerException : std::runtime_error + { + LexerException(const std::string& message) : std::runtime_error(message) {} + LexerException(const char* message) : std::runtime_error(message) {} + }; + + class Lexer + { + public: + struct Block + { + int line; + std::string_view content; + }; + + Lexer(std::string_view buffer); + Lexer() = delete; + + Token next(); + Token peek(); + + // Jump ahead to next uncommented closing bracket at level zero. Assumes the head is at an opening bracket. + // Returns the contents of the block excluding the brackets and places cursor at closing bracket. + std::optional jump(); + + Block getLastJumpBlock() const; + + [[noreturn]] void error(const std::string& msg); + + private: + void drop(); + void advance(); + char head(); + bool peekChar(char c); + + Token scanToken(); + Token scanLiteral(); + Token scanStringLiteral(); + Token scanNumber(); + + const char* mHead; + const char* mTail; + std::size_t mAbsolutePos; + std::size_t mColumn; + std::size_t mLine; + std::string_view mBuffer; + Token mLastToken; + std::optional mLookahead; + + Block mLastJumpBlock; + }; + } +} + +#endif diff --git a/components/fx/lexer_types.hpp b/components/fx/lexer_types.hpp new file mode 100644 index 0000000000..0d81c483b7 --- /dev/null +++ b/components/fx/lexer_types.hpp @@ -0,0 +1,56 @@ +#ifndef OPENMW_COMPONENTS_FX_LEXER_TYPES_H +#define OPENMW_COMPONENTS_FX_LEXER_TYPES_H + +#include +#include + +namespace fx +{ + namespace Lexer + { + struct Float { inline static constexpr std::string_view repr = "float"; float value = 0.0;}; + struct Integer { inline static constexpr std::string_view repr = "integer"; int value = 0;}; + struct Boolean { inline static constexpr std::string_view repr = "boolean"; bool value = false;}; + struct Literal { inline static constexpr std::string_view repr = "literal"; std::string_view value;}; + struct String { inline static constexpr std::string_view repr = "string"; std::string_view value;}; + struct Shared { inline static constexpr std::string_view repr = "shared"; }; + struct Vertex { inline static constexpr std::string_view repr = "vertex"; }; + struct Fragment { inline static constexpr std::string_view repr = "fragment"; }; + struct Compute { inline static constexpr std::string_view repr = "compute"; }; + struct Technique { inline static constexpr std::string_view repr = "technique"; }; + struct Main_Pass { inline static constexpr std::string_view repr = "main_pass"; }; + struct Render_Target { inline static constexpr std::string_view repr = "render_target"; }; + struct Sampler_1D { inline static constexpr std::string_view repr = "sampler_1d"; }; + struct Sampler_2D { inline static constexpr std::string_view repr = "sampler_2d"; }; + struct Sampler_3D { inline static constexpr std::string_view repr = "sampler_3d"; }; + struct Uniform_Bool { inline static constexpr std::string_view repr = "uniform_bool"; }; + struct Uniform_Float { inline static constexpr std::string_view repr = "uniform_float"; }; + struct Uniform_Int { inline static constexpr std::string_view repr = "uniform_int"; }; + struct Uniform_Vec2 { inline static constexpr std::string_view repr = "uniform_vec2"; }; + struct Uniform_Vec3 { inline static constexpr std::string_view repr = "uniform_vec3"; }; + struct Uniform_Vec4 { inline static constexpr std::string_view repr = "uniform_vec4"; }; + struct Eof { inline static constexpr std::string_view repr = "eof"; }; + struct Equal { inline static constexpr std::string_view repr = "equal"; }; + struct Open_bracket { inline static constexpr std::string_view repr = "open_bracket"; }; + struct Close_bracket { inline static constexpr std::string_view repr = "close_bracket"; }; + struct Open_Parenthesis { inline static constexpr std::string_view repr = "open_parenthesis"; }; + struct Close_Parenthesis{ inline static constexpr std::string_view repr = "close_parenthesis"; }; + struct Quote { inline static constexpr std::string_view repr = "quote"; }; + struct SemiColon { inline static constexpr std::string_view repr = "semicolon"; }; + struct Comma { inline static constexpr std::string_view repr = "comma"; }; + struct VBar { inline static constexpr std::string_view repr = "vbar"; }; + struct Colon { inline static constexpr std::string_view repr = "colon"; }; + struct True { inline static constexpr std::string_view repr = "true"; }; + struct False { inline static constexpr std::string_view repr = "false"; }; + struct Vec2 { inline static constexpr std::string_view repr = "vec2"; }; + struct Vec3 { inline static constexpr std::string_view repr = "vec3"; }; + struct Vec4 { inline static constexpr std::string_view repr = "vec4"; }; + + using Token = std::variant; + } +} + +#endif \ No newline at end of file diff --git a/components/fx/parse_constants.hpp b/components/fx/parse_constants.hpp new file mode 100644 index 0000000000..18d32ee53a --- /dev/null +++ b/components/fx/parse_constants.hpp @@ -0,0 +1,133 @@ +#ifndef OPENMW_COMPONENTS_FX_PARSE_CONSTANTS_H +#define OPENMW_COMPONENTS_FX_PARSE_CONSTANTS_H + +#include +#include + +#include +#include +#include +#include + +#include + +#include "technique.hpp" + +namespace fx +{ + namespace constants + { + constexpr std::array, 6> TechniqueFlag = {{ + {"disable_interiors" , Technique::Flag_Disable_Interiors}, + {"disable_exteriors" , Technique::Flag_Disable_Exteriors}, + {"disable_underwater" , Technique::Flag_Disable_Underwater}, + {"disable_abovewater" , Technique::Flag_Disable_Abovewater}, + {"disable_sunglare" , Technique::Flag_Disable_SunGlare}, + {"hidden" , Technique::Flag_Hidden} + }}; + + constexpr std::array, 6> SourceFormat = {{ + {"red" , GL_RED}, + {"rg" , GL_RG}, + {"rgb" , GL_RGB}, + {"bgr" , GL_BGR}, + {"rgba", GL_RGBA}, + {"bgra", GL_BGRA}, + }}; + + constexpr std::array, 9> SourceType = {{ + {"byte" , GL_BYTE}, + {"unsigned_byte" , GL_UNSIGNED_BYTE}, + {"short" , GL_SHORT}, + {"unsigned_short" , GL_UNSIGNED_SHORT}, + {"int" , GL_INT}, + {"unsigned_int" , GL_UNSIGNED_INT}, + {"unsigned_int_24_8", GL_UNSIGNED_INT_24_8}, + {"float" , GL_FLOAT}, + {"double" , GL_DOUBLE}, + }}; + + constexpr std::array, 16> InternalFormat = {{ + {"red" , GL_RED}, + {"r16f" , GL_R16F}, + {"r32f" , GL_R32F}, + {"rg" , GL_RG}, + {"rg16f" , GL_RG16F}, + {"rg32f" , GL_RG32F}, + {"rgb" , GL_RGB}, + {"rgb16f" , GL_RGB16F}, + {"rgb32f" , GL_RGB32F}, + {"rgba" , GL_RGBA}, + {"rgba16f" , GL_RGBA16F}, + {"rgba32f" , GL_RGBA32F}, + {"depth_component16" , GL_DEPTH_COMPONENT16}, + {"depth_component24" , GL_DEPTH_COMPONENT24}, + {"depth_component32" , GL_DEPTH_COMPONENT32}, + {"depth_component32f", GL_DEPTH_COMPONENT32F} + }}; + + constexpr std::array, 13> Compression = {{ + {"auto" , osg::Texture::USE_USER_DEFINED_FORMAT}, + {"arb" , osg::Texture::USE_ARB_COMPRESSION}, + {"s3tc_dxt1" , osg::Texture::USE_S3TC_DXT1_COMPRESSION}, + {"s3tc_dxt3" , osg::Texture::USE_S3TC_DXT3_COMPRESSION}, + {"s3tc_dxt5" , osg::Texture::USE_S3TC_DXT5_COMPRESSION}, + {"pvrtc_2bpp" , osg::Texture::USE_PVRTC_2BPP_COMPRESSION}, + {"pvrtc_4bpp" , osg::Texture::USE_PVRTC_4BPP_COMPRESSION}, + {"etc" , osg::Texture::USE_ETC_COMPRESSION}, + {"etc2" , osg::Texture::USE_ETC2_COMPRESSION}, + {"rgtc1" , osg::Texture::USE_RGTC1_COMPRESSION}, + {"rgtc2" , osg::Texture::USE_RGTC2_COMPRESSION}, + {"s3tc_dxt1c" , osg::Texture::USE_S3TC_DXT1c_COMPRESSION}, + {"s3tc_dxt1a" , osg::Texture::USE_S3TC_DXT1a_COMPRESSION} + }}; + + constexpr std::array, 6> WrapMode = {{ + {"clamp" , osg::Texture::CLAMP}, + {"clamp_to_edge" , osg::Texture::CLAMP_TO_EDGE}, + {"clamp_to_border", osg::Texture::CLAMP_TO_BORDER}, + {"repeat" , osg::Texture::REPEAT}, + {"mirror" , osg::Texture::MIRROR} + }}; + + constexpr std::array, 6> FilterMode = {{ + {"linear" , osg::Texture::LINEAR}, + {"linear_mipmap_linear" , osg::Texture::LINEAR_MIPMAP_LINEAR}, + {"linear_mipmap_nearest" , osg::Texture::LINEAR_MIPMAP_NEAREST}, + {"nearest" , osg::Texture::NEAREST}, + {"nearest_mipmap_linear" , osg::Texture::NEAREST_MIPMAP_LINEAR}, + {"nearest_mipmap_nearest", osg::Texture::NEAREST_MIPMAP_NEAREST} + }}; + + constexpr std::array, 15> BlendFunc = {{ + {"dst_alpha" , osg::BlendFunc::DST_ALPHA}, + {"dst_color" , osg::BlendFunc::DST_COLOR}, + {"one" , osg::BlendFunc::ONE}, + {"one_minus_dst_alpha" , osg::BlendFunc::ONE_MINUS_DST_ALPHA}, + {"one_minus_dst_color" , osg::BlendFunc::ONE_MINUS_DST_COLOR}, + {"one_minus_src_alpha" , osg::BlendFunc::ONE_MINUS_SRC_ALPHA}, + {"one_minus_src_color" , osg::BlendFunc::ONE_MINUS_SRC_COLOR}, + {"src_alpha" , osg::BlendFunc::SRC_ALPHA}, + {"src_alpha_saturate" , osg::BlendFunc::SRC_ALPHA_SATURATE}, + {"src_color" , osg::BlendFunc::SRC_COLOR}, + {"constant_color" , osg::BlendFunc::CONSTANT_COLOR}, + {"one_minus_constant_color" , osg::BlendFunc::ONE_MINUS_CONSTANT_COLOR}, + {"constant_alpha" , osg::BlendFunc::CONSTANT_ALPHA}, + {"one_minus_constant_alpha" , osg::BlendFunc::ONE_MINUS_CONSTANT_ALPHA}, + {"zero" , osg::BlendFunc::ZERO} + }}; + + constexpr std::array, 8> BlendEquation = {{ + {"rgba_min" , osg::BlendEquation::RGBA_MIN}, + {"rgba_max" , osg::BlendEquation::RGBA_MAX}, + {"alpha_min" , osg::BlendEquation::ALPHA_MIN}, + {"alpha_max" , osg::BlendEquation::ALPHA_MAX}, + {"logic_op" , osg::BlendEquation::LOGIC_OP}, + {"add" , osg::BlendEquation::FUNC_ADD}, + {"subtract" , osg::BlendEquation::FUNC_SUBTRACT}, + {"reverse_subtract" , osg::BlendEquation::FUNC_REVERSE_SUBTRACT} + }}; + } +} + +#endif \ No newline at end of file diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp new file mode 100644 index 0000000000..b32dd2e0b1 --- /dev/null +++ b/components/fx/pass.cpp @@ -0,0 +1,323 @@ +#include "pass.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "technique.hpp" +#include "stateupdater.hpp" + +namespace +{ + constexpr char s_DefaultVertex[] = R"GLSL( +#if OMW_USE_BINDINGS + omw_In vec2 omw_Vertex; +#endif +omw_Out vec2 omw_TexCoord; + +void main() +{ + omw_Position = vec4(omw_Vertex.xy, 0.0, 1.0); + omw_TexCoord = omw_Position.xy * 0.5 + 0.5; +})GLSL"; + + constexpr char s_DefaultVertexMultiview[] = R"GLSL( +layout(num_views = 2) in; +#if OMW_USE_BINDINGS + omw_In vec2 omw_Vertex; +#endif +omw_Out vec2 omw_TexCoord; + +void main() +{ + omw_Position = vec4(omw_Vertex.xy, 0.0, 1.0); + omw_TexCoord = omw_Position.xy * 0.5 + 0.5; +})GLSL"; + +} + +namespace fx +{ + Pass::Pass(Pass::Type type, Pass::Order order, bool ubo) + : mCompiled(false) + , mType(type) + , mOrder(order) + , mLegacyGLSL(true) + , mUBO(ubo) + { + } + + std::string Pass::getPassHeader(Technique& technique, std::string_view preamble, bool fragOut) + { + std::string header = R"GLSL( +#version @version @profile +@extensions + +@uboStruct + +#define OMW_REVERSE_Z @reverseZ +#define OMW_RADIAL_FOG @radialFog +#define OMW_HDR @hdr +#define OMW_NORMALS @normals +#define OMW_USE_BINDINGS @useBindings +#define OMW_MULTIVIEW @multiview +#define omw_In @in +#define omw_Out @out +#define omw_Position @position +#define omw_Texture1D @texture1D +#define omw_Texture2D @texture2D +#define omw_Texture3D @texture3D +#define omw_Vertex @vertex +#define omw_FragColor @fragColor + +@fragBinding + +uniform @builtinSampler omw_SamplerLastShader; +uniform @builtinSampler omw_SamplerLastPass; +uniform @builtinSampler omw_SamplerDepth; +uniform @builtinSampler omw_SamplerNormals; + +uniform vec4 omw_PointLights[@pointLightCount]; +uniform int omw_PointLightsCount; + +#if OMW_MULTIVIEW +uniform mat4 projectionMatrixMultiView[2]; +uniform mat4 invProjectionMatrixMultiView[2]; +#endif + +int omw_GetPointLightCount() +{ + return omw_PointLightsCount; +} + +vec3 omw_GetPointLightWorldPos(int index) +{ + return omw_PointLights[(index * 3)].xyz; +} + +vec3 omw_GetPointLightDiffuse(int index) +{ + return omw_PointLights[(index * 3) + 1].xyz; +} + +vec3 omw_GetPointLightAttenuation(int index) +{ + return omw_PointLights[(index * 3) + 2].xyz; +} + +float omw_GetPointLightRadius(int index) +{ + return omw_PointLights[(index * 3) + 2].w; +} + +#if @ubo + layout(std140) uniform _data { _omw_data omw; }; +#else + uniform _omw_data omw; +#endif + + +mat4 omw_ProjectionMatrix() +{ +#if OMW_MULTIVIEW + return projectionMatrixMultiView[gl_ViewID_OVR]; +#else + return omw.projectionMatrix; +#endif +} + +mat4 omw_InvProjectionMatrix() +{ +#if OMW_MULTIVIEW + return invProjectionMatrixMultiView[gl_ViewID_OVR]; +#else + return omw.invProjectionMatrix; +#endif +} + + float omw_GetDepth(vec2 uv) + { +#if OMW_MULTIVIEW + float depth = omw_Texture2D(omw_SamplerDepth, vec3(uv, gl_ViewID_OVR)).r; +#else + float depth = omw_Texture2D(omw_SamplerDepth, uv).r; +#endif +#if OMW_REVERSE_Z + return 1.0 - depth; +#else + return depth; +#endif + } + + vec4 omw_GetLastShader(vec2 uv) + { +#if OMW_MULTIVIEW + return omw_Texture2D(omw_SamplerLastShader, vec3(uv, gl_ViewID_OVR)); +#else + return omw_Texture2D(omw_SamplerLastShader, uv); +#endif + } + + vec4 omw_GetLastPass(vec2 uv) + { +#if OMW_MULTIVIEW + return omw_Texture2D(omw_SamplerLastPass, vec3(uv, gl_ViewID_OVR)); +#else + return omw_Texture2D(omw_SamplerLastPass, uv); +#endif + } + + vec3 omw_GetNormals(vec2 uv) + { +#if OMW_MULTIVIEW + return omw_Texture2D(omw_SamplerNormals, vec3(uv, gl_ViewID_OVR)).rgb * 2.0 - 1.0; +#else + return omw_Texture2D(omw_SamplerNormals, uv).rgb * 2.0 - 1.0; +#endif + } + +#if OMW_HDR + uniform sampler2D omw_EyeAdaptation; +#endif + + float omw_GetEyeAdaptation() + { +#if OMW_HDR + return omw_Texture2D(omw_EyeAdaptation, vec2(0.5, 0.5)).r; +#else + return 1.0; +#endif + } +)GLSL"; + + std::stringstream extBlock; + for (const auto& extension : technique.getGLSLExtensions()) + extBlock << "#ifdef " << extension << '\n' << "\t#extension " << extension << ": enable" << '\n' << "#endif" << '\n'; + + const std::vector> defines = { + {"@pointLightCount", std::to_string(SceneUtil::PPLightBuffer::sMaxPPLightsArraySize)}, + {"@version", std::to_string(technique.getGLSLVersion())}, + {"@multiview", Stereo::getMultiview() ? "1" : "0"}, + {"@builtinSampler", Stereo::getMultiview() ? "sampler2DArray" : "sampler2D"}, + {"@profile", technique.getGLSLProfile()}, + {"@extensions", extBlock.str()}, + {"@uboStruct", StateUpdater::getStructDefinition()}, + {"@ubo", mUBO ? "1" : "0"}, + {"@normals", technique.getNormals() ? "1" : "0"}, + {"@reverseZ", SceneUtil::AutoDepth::isReversed() ? "1" : "0"}, + {"@radialFog", Settings::Manager::getBool("radial fog", "Fog") ? "1" : "0"}, + {"@hdr", technique.getHDR() ? "1" : "0"}, + {"@in", mLegacyGLSL ? "varying" : "in"}, + {"@out", mLegacyGLSL ? "varying" : "out"}, + {"@position", "gl_Position"}, + {"@texture1D", mLegacyGLSL ? "texture1D" : "texture"}, + {"@texture2D", mLegacyGLSL ? "texture2D" : "texture"}, + {"@texture3D", mLegacyGLSL ? "texture3D" : "texture"}, + {"@vertex", mLegacyGLSL ? "gl_Vertex" : "_omw_Vertex"}, + {"@fragColor", mLegacyGLSL ? "gl_FragColor" : "_omw_FragColor"}, + {"@useBindings", mLegacyGLSL ? "0" : "1"}, + {"@fragBinding", mLegacyGLSL ? "" : "out vec4 omw_FragColor;"} + }; + + for (const auto& [define, value]: defines) + for (size_t pos = header.find(define); pos != std::string::npos; pos = header.find(define)) + header.replace(pos, define.size(), value); + + for (const auto& target : mRenderTargets) + header.append("uniform sampler2D " + std::string(target) + ";"); + + for (auto& uniform : technique.getUniformMap()) + if (auto glsl = uniform->getGLSL()) + header.append(glsl.value()); + + header.append(preamble); + + return header; + } + + void Pass::prepareStateSet(osg::StateSet* stateSet, const std::string& name) const + { + osg::ref_ptr program = new osg::Program; + if (mType == Type::Pixel) + { + program->addShader(new osg::Shader(*mVertex)); + program->addShader(new osg::Shader(*mFragment)); + } + else if (mType == Type::Compute) + { + program->addShader(new osg::Shader(*mCompute)); + } + + if (mUBO) + program->addBindUniformBlock("_data", static_cast(Resource::SceneManager::UBOBinding::PostProcessor)); + + program->setName(name); + + if (!mLegacyGLSL) + { + program->addBindFragDataLocation("_omw_FragColor", 0); + program->addBindAttribLocation("_omw_Vertex", 0); + } + + stateSet->setAttribute(program); + + if (mBlendSource && mBlendDest) + stateSet->setAttributeAndModes(new osg::BlendFunc(mBlendSource.value(), mBlendDest.value())); + + if (mBlendEq) + stateSet->setAttributeAndModes(new osg::BlendEquation(mBlendEq.value())); + + if (mClearColor) + stateSet->setAttributeAndModes(new SceneUtil::ClearColor(mClearColor.value(), GL_COLOR_BUFFER_BIT)); + } + + void Pass::dirty() + { + mVertex = nullptr; + mFragment = nullptr; + mCompute = nullptr; + mCompiled = false; + } + + void Pass::compile(Technique& technique, std::string_view preamble) + { + if (mCompiled) + return; + + mLegacyGLSL = technique.getGLSLVersion() != 330; + + if (mType == Type::Pixel) + { + if (!mVertex) + mVertex = new osg::Shader(osg::Shader::VERTEX, Stereo::getMultiview() ? s_DefaultVertexMultiview : s_DefaultVertex); + + mVertex->setShaderSource(getPassHeader(technique, preamble).append(mVertex->getShaderSource())); + mFragment->setShaderSource(getPassHeader(technique, preamble, true).append(mFragment->getShaderSource())); + + mVertex->setName(mName); + mFragment->setName(mName); + } + else if (mType == Type::Compute) + { + mCompute->setShaderSource(getPassHeader(technique, preamble).append(mCompute->getShaderSource())); + mCompute->setName(mName); + } + + mCompiled = true; + } + +} diff --git a/components/fx/pass.hpp b/components/fx/pass.hpp new file mode 100644 index 0000000000..829fb716cf --- /dev/null +++ b/components/fx/pass.hpp @@ -0,0 +1,81 @@ +#ifndef OPENMW_COMPONENTS_FX_PASS_H +#define OPENMW_COMPONENTS_FX_PASS_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace fx +{ + class Technique; + + class Pass + { + public: + + enum class Order + { + Forward, + Post + }; + + enum class Type + { + None, + Pixel, + Compute + }; + + friend class Technique; + + Pass(Type type=Type::Pixel, Order order=Order::Post, bool ubo = false); + + void compile(Technique& technique, std::string_view preamble); + + std::string getTarget() const { return mTarget; } + + const std::array& getRenderTargets() const { return mRenderTargets; } + + void prepareStateSet(osg::StateSet* stateSet, const std::string& name) const; + + std::string getName() const { return mName; } + + void dirty(); + + private: + std::string getPassHeader(Technique& technique, std::string_view preamble, bool fragOut = false); + + bool mCompiled; + + osg::ref_ptr mVertex; + osg::ref_ptr mFragment; + osg::ref_ptr mCompute; + + Type mType; + Order mOrder; + std::string mName; + bool mLegacyGLSL; + bool mUBO; + + std::array mRenderTargets; + + std::string mTarget; + std::optional mClearColor; + + std::optional mBlendSource; + std::optional mBlendDest; + std::optional mBlendEq; + }; +} + +#endif diff --git a/components/fx/stateupdater.cpp b/components/fx/stateupdater.cpp new file mode 100644 index 0000000000..00034b54f0 --- /dev/null +++ b/components/fx/stateupdater.cpp @@ -0,0 +1,65 @@ +#include "stateupdater.hpp" + +#include +#include + +#include +#include + +namespace fx +{ + StateUpdater::StateUpdater(bool useUBO) : mUseUBO(useUBO) {} + + void StateUpdater::setDefaults(osg::StateSet* stateset) + { + if (mUseUBO) + { + osg::ref_ptr ubo = new osg::UniformBufferObject; + + osg::ref_ptr> data = new osg::BufferTemplate(); + data->setBufferObject(ubo); + + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::PostProcessor), data, 0, mData.getGPUSize()); + + stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); + } + else + { + const auto createUniform = [&] (const auto& v) { + using T = std::decay_t; + std::string name = "omw." + std::string(T::sName); + stateset->addUniform(new osg::Uniform(name.c_str(), mData.get())); + }; + + std::apply([&] (const auto& ... v) { (createUniform(v) , ...); }, mData.getData()); + } + } + + void StateUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* nv) + { + if (mUseUBO) + { + osg::UniformBufferBinding* ubb = dynamic_cast(stateset->getAttribute(osg::StateAttribute::UNIFORMBUFFERBINDING, static_cast(Resource::SceneManager::UBOBinding::PostProcessor))); + if (!ubb) + throw std::runtime_error("StateUpdater::apply: failed to get an UniformBufferBinding!"); + + auto& dest = static_cast*>(ubb->getBufferData())->getData(); + mData.copyTo(dest); + + ubb->getBufferData()->dirty(); + } + else + { + const auto setUniform = [&] (const auto& v) { + using T = std::decay_t; + std::string name = "omw." + std::string(T::sName); + stateset->getUniform(name)->set(mData.get()); + }; + + std::apply([&] (const auto& ... v) { (setUniform(v) , ...); }, mData.getData()); + } + + if (mPointLightBuffer) + mPointLightBuffer->applyUniforms(nv->getTraversalNumber(), stateset); + } +} diff --git a/components/fx/stateupdater.hpp b/components/fx/stateupdater.hpp new file mode 100644 index 0000000000..b2beb8d576 --- /dev/null +++ b/components/fx/stateupdater.hpp @@ -0,0 +1,202 @@ +#ifndef OPENMW_COMPONENTS_FX_STATEUPDATER_H +#define OPENMW_COMPONENTS_FX_STATEUPDATER_H + +#include + +#include +#include +#include + +namespace fx +{ + class StateUpdater : public SceneUtil::StateSetUpdater + { + public: + StateUpdater(bool useUBO); + + void setProjectionMatrix(const osg::Matrixf& matrix) + { + mData.get() = matrix; + mData.get() = osg::Matrixf::inverse(matrix); + } + + void setViewMatrix(const osg::Matrixf& matrix) + { + mData.get() = matrix; + mData.get() = osg::Matrixf::inverse(matrix); + } + + void setPrevViewMatrix(const osg::Matrixf& matrix) { mData.get() = matrix;} + + void setEyePos(const osg::Vec3f& pos) { mData.get() = osg::Vec4f(pos, 0.f); } + + void setEyeVec(const osg::Vec3f& vec) { mData.get() = osg::Vec4f(vec, 0.f); } + + void setFogColor(const osg::Vec4f& color) { mData.get() = color; } + + void setSunColor(const osg::Vec4f& color) { mData.get() = color; } + + void setSunPos(const osg::Vec4f& pos, bool night) + { + mData.get() = pos; + + if (night) + mData.get().z() *= -1.f; + } + + void setResolution(const osg::Vec2f& size) + { + mData.get() = size; + mData.get() = {1.f / size.x(), 1.f / size.y()}; + } + + void setSunVis(float vis) + { + mData.get() = vis; + } + + void setFogRange(float near, float far) + { + mData.get() = near; + mData.get() = far; + } + + void setNearFar(float near, float far) + { + mData.get() = near; + mData.get() = far; + } + + void setIsUnderwater(bool underwater) { mData.get() = underwater; } + + void setIsInterior(bool interior) { mData.get() = interior; } + + void setFov(float fov) { mData.get() = fov; } + + void setGameHour(float hour) { mData.get() = hour; } + + void setWeatherId(int id) { mData.get() = id; } + + void setNextWeatherId(int id) { mData.get() = id; } + + void setWaterHeight(float height) { mData.get() = height; } + + void setSimulationTime(float time) { mData.get() = time; } + + void setDeltaSimulationTime(float time) { mData.get() = time; } + + void setWindSpeed(float speed) { mData.get() = speed; } + + void setWeatherTransition(float transition) { mData.get() = transition > 0 ? 1 - transition : 0; } + + void bindPointLights(std::shared_ptr buffer) + { + mPointLightBuffer = buffer; + } + + static std::string getStructDefinition() + { + static std::string definition = UniformData::getDefinition("_omw_data"); + return definition; + } + + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override; + + private: + struct ProjectionMatrix : std140::Mat4 { static constexpr std::string_view sName = "projectionMatrix"; }; + + struct InvProjectionMatrix : std140::Mat4 { static constexpr std::string_view sName = "invProjectionMatrix"; }; + + struct ViewMatrix : std140::Mat4 { static constexpr std::string_view sName = "viewMatrix"; }; + + struct PrevViewMatrix : std140::Mat4 { static constexpr std::string_view sName = "prevViewMatrix"; }; + + struct InvViewMatrix : std140::Mat4 { static constexpr std::string_view sName = "invViewMatrix"; }; + + struct EyePos : std140::Vec4 { static constexpr std::string_view sName = "eyePos"; }; + + struct EyeVec : std140::Vec4 { static constexpr std::string_view sName = "eyeVec"; }; + + struct FogColor : std140::Vec4 { static constexpr std::string_view sName = "fogColor"; }; + + struct SunColor : std140::Vec4 { static constexpr std::string_view sName = "sunColor"; }; + + struct SunPos : std140::Vec4 { static constexpr std::string_view sName = "sunPos"; }; + + struct Resolution : std140::Vec2 { static constexpr std::string_view sName = "resolution"; }; + + struct RcpResolution : std140::Vec2 { static constexpr std::string_view sName = "rcpResolution"; }; + + struct FogNear : std140::Float { static constexpr std::string_view sName = "fogNear"; }; + + struct FogFar : std140::Float { static constexpr std::string_view sName = "fogFar"; }; + + struct Near : std140::Float { static constexpr std::string_view sName = "near"; }; + + struct Far : std140::Float { static constexpr std::string_view sName = "far"; }; + + struct Fov : std140::Float { static constexpr std::string_view sName = "fov"; }; + + struct GameHour : std140::Float { static constexpr std::string_view sName = "gameHour"; }; + + struct SunVis : std140::Float { static constexpr std::string_view sName = "sunVis"; }; + + struct WaterHeight : std140::Float { static constexpr std::string_view sName = "waterHeight"; }; + + struct SimulationTime : std140::Float { static constexpr std::string_view sName = "simulationTime"; }; + + struct DeltaSimulationTime : std140::Float { static constexpr std::string_view sName = "deltaSimulationTime"; }; + + struct WindSpeed : std140::Float { static constexpr std::string_view sName = "windSpeed"; }; + + struct WeatherTransition : std140::Float { static constexpr std::string_view sName = "weatherTransition"; }; + + struct WeatherID : std140::Int { static constexpr std::string_view sName = "weatherID"; }; + + struct NextWeatherID : std140::Int { static constexpr std::string_view sName = "nextWeatherID"; }; + + struct IsUnderwater : std140::Bool { static constexpr std::string_view sName = "isUnderwater"; }; + + struct IsInterior : std140::Bool { static constexpr std::string_view sName = "isInterior"; }; + + using UniformData = std140::UBO< + ProjectionMatrix, + InvProjectionMatrix, + ViewMatrix, + PrevViewMatrix, + InvViewMatrix, + EyePos, + EyeVec, + FogColor, + SunColor, + SunPos, + Resolution, + RcpResolution, + FogNear, + FogFar, + Near, + Far, + Fov, + GameHour, + SunVis, + WaterHeight, + SimulationTime, + DeltaSimulationTime, + WindSpeed, + WeatherTransition, + WeatherID, + NextWeatherID, + IsUnderwater, + IsInterior + >; + + UniformData mData; + bool mUseUBO; + + std::shared_ptr mPointLightBuffer; + }; +} + +#endif diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp new file mode 100644 index 0000000000..eaa74e3e3b --- /dev/null +++ b/components/fx/technique.cpp @@ -0,0 +1,1091 @@ +#include "technique.hpp" + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "parse_constants.hpp" + +namespace +{ + struct ProxyTextureData + { + osg::Texture::WrapMode wrap_s = osg::Texture::CLAMP_TO_EDGE; + osg::Texture::WrapMode wrap_t = osg::Texture::CLAMP_TO_EDGE; + osg::Texture::WrapMode wrap_r = osg::Texture::CLAMP_TO_EDGE; + osg::Texture::FilterMode min_filter = osg::Texture::LINEAR_MIPMAP_LINEAR; + osg::Texture::FilterMode mag_filter =osg::Texture::LINEAR; + osg::Texture::InternalFormatMode compression = osg::Texture::USE_IMAGE_DATA_FORMAT; + std::optional source_format; + std::optional source_type; + std::optional internal_format; + }; +} + +namespace fx +{ + Technique::Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, const std::string& name, int width, int height, bool ubo, bool supportsNormals) + : mName(name) + , mFileName((std::filesystem::path(Technique::sSubdir) / (mName + Technique::sExt)).string()) + , mLastModificationTime(std::filesystem::file_time_type()) + , mWidth(width) + , mHeight(height) + , mVFS(vfs) + , mImageManager(imageManager) + , mUBO(ubo) + , mSupportsNormals(supportsNormals) + { + clear(); + } + + void Technique::clear() + { + mTextures.clear(); + mStatus = Status::Uncompiled; + mValid = false; + mHDR = false; + mNormals = false; + mLights = false; + mEnabled = true; + mPassMap.clear(); + mPasses.clear(); + mPassKeys.clear(); + mDefinedUniforms.clear(); + mRenderTargets.clear(); + mMainTemplate = nullptr; + mLastAppliedType = Pass::Type::None; + mFlags = 0; + mShared.clear(); + mAuthor = {}; + mDescription = {}; + mVersion = {}; + mGLSLExtensions.clear(); + mGLSLVersion = mUBO ? 330 : 120; + mGLSLProfile.clear(); + mDynamic = false; + } + + std::string Technique::getBlockWithLineDirective() + { + auto block = mLexer->getLastJumpBlock(); + std::string content = std::string(block.content); + + content = "\n#line " + std::to_string(block.line + 1) + "\n" + std::string(block.content) + "\n"; + return content; + } + + Technique::UniformMap::iterator Technique::findUniform(const std::string& name) + { + return std::find_if(mDefinedUniforms.begin(), mDefinedUniforms.end(), [&name](const auto& uniform) + { + return uniform->mName == name; + }); + } + + bool Technique::compile() + { + clear(); + + if (!mVFS.exists(mFileName)) + { + Log(Debug::Error) << "Could not load technique, file does not exist '" << mFileName << "'"; + + mStatus = Status::File_Not_exists; + return false; + } + + try + { + std::string source(std::istreambuf_iterator(*mVFS.get(getFileName())), {}); + + parse(std::move(source)); + + if (mPassKeys.empty()) + error("no pass list found, ensure you define one in a 'technique' block"); + + int swaps = 0; + + for (auto name : mPassKeys) + { + auto it = mPassMap.find(name); + + if (it == mPassMap.end()) + error(Misc::StringUtils::format("pass '%s' was found in the pass list, but there was no matching 'fragment', 'vertex' or 'compute' block", std::string(name))); + + if (mLastAppliedType != Pass::Type::None && mLastAppliedType != it->second->mType) + { + swaps++; + if (swaps == 2) + Log(Debug::Warning) << "compute and pixel shaders are being swapped multiple times in shader chain, this can lead to serious performance drain."; + } + else + mLastAppliedType = it->second->mType; + + if (Stereo::getMultiview()) + { + mGLSLExtensions.insert("GL_OVR_multiview"); + mGLSLExtensions.insert("GL_OVR_multiview2"); + mGLSLExtensions.insert("GL_EXT_texture_array"); + } + + it->second->compile(*this, mShared); + + if (!it->second->mTarget.empty()) + { + auto rtIt = mRenderTargets.find(it->second->mTarget); + if (rtIt == mRenderTargets.end()) + error(Misc::StringUtils::format("target '%s' not defined", std::string(it->second->mTarget))); + } + + mPasses.emplace_back(it->second); + } + + if (mPasses.empty()) + error("invalid pass list, no passes defined for technique"); + + mValid = true; + } + catch(const std::runtime_error& e) + { + clear(); + mStatus = Status::Parse_Error; + + mLastError = "Failed parsing technique '" + getName() + "' " + e.what();; + Log(Debug::Error) << mLastError; + } + + return mValid; + } + + std::string Technique::getName() const + { + return mName; + } + + std::string Technique::getFileName() const + { + return mFileName; + } + + bool Technique::setLastModificationTime(std::filesystem::file_time_type timeStamp) + { + const bool isDirty = timeStamp != mLastModificationTime; + mLastModificationTime = timeStamp; + return isDirty; + } + + [[noreturn]] void Technique::error(const std::string& msg) + { + mLexer->error(msg); + } + + template<> + void Technique::parseBlockImp() + { + if (!mLexer->jump()) + error(Misc::StringUtils::format("unterminated 'shared' block, expected closing brackets")); + + if (!mShared.empty()) + error("repeated 'shared' block, only one allowed per technique file"); + + mShared = getBlockWithLineDirective(); + } + + template<> + void Technique::parseBlockImp() + { + if (!mPassKeys.empty()) + error("exactly one 'technique' block can appear per file"); + + while (!isNext() && !isNext()) + { + expect(); + + auto key = std::get(mToken).value; + + expect(); + + if (key == "passes") + mPassKeys = parseLiteralList(); + else if (key == "version") + mVersion = parseString(); + else if (key == "description") + mDescription = parseString(); + else if (key == "author") + mAuthor = parseString(); + else if (key == "glsl_version") + { + int version = parseInteger(); + if (mUBO && version > 330) + mGLSLVersion = version; + } + else if (key == "flags") + mFlags = parseFlags(); + else if (key == "hdr") + mHDR = parseBool(); + else if (key == "pass_normals") + mNormals = parseBool() && mSupportsNormals; + else if (key == "pass_lights") + mLights = parseBool(); + else if (key == "glsl_profile") + { + expect(); + mGLSLProfile = std::string(std::get(mToken).value); + } + else if (key == "glsl_extensions") + { + for (const auto& ext : parseLiteralList()) + mGLSLExtensions.emplace(ext); + } + else if (key == "dynamic") + mDynamic = parseBool(); + else + error(Misc::StringUtils::format("unexpected key '%s'", std::string{key})); + + expect(); + } + + if (mPassKeys.empty()) + error("pass list in 'technique' block cannot be empty."); + } + + template<> + void Technique::parseBlockImp() + { + if (mMainTemplate) + error("duplicate 'main_pass' block"); + + if (mName != "main") + error("'main_pass' block can only be defined in the 'main.omwfx' technique file"); + + mMainTemplate = new osg::Texture2D; + + mMainTemplate->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + mMainTemplate->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + + while (!isNext() && !isNext()) + { + expect(); + + auto key = std::get(mToken).value; + + expect(); + + if (key == "wrap_s") + mMainTemplate->setWrap(osg::Texture::WRAP_S, parseWrapMode()); + else if (key == "wrap_t") + mMainTemplate->setWrap(osg::Texture::WRAP_T, parseWrapMode()); + // Skip depth attachments for main scene, as some engine settings rely on specific depth formats. + // Allowing this to be overriden will cause confusion. + else if (key == "internal_format") + mMainTemplate->setInternalFormat(parseInternalFormat()); + else if (key == "source_type") + mMainTemplate->setSourceType(parseSourceType()); + else if (key == "source_format") + mMainTemplate->setSourceFormat(parseSourceFormat()); + else + error(Misc::StringUtils::format("unexpected key '%s'", std::string(key))); + + expect(); + } + } + + template<> + void Technique::parseBlockImp() + { + if (mRenderTargets.count(mBlockName)) + error(Misc::StringUtils::format("redeclaration of render target '%s'", std::string(mBlockName))); + + fx::Types::RenderTarget rt; + rt.mTarget->setTextureSize(mWidth, mHeight); + rt.mTarget->setSourceFormat(GL_RGB); + rt.mTarget->setInternalFormat(GL_RGB); + rt.mTarget->setSourceType(GL_UNSIGNED_BYTE); + + while (!isNext() && !isNext()) + { + expect(); + + auto key = std::get(mToken).value; + + expect(); + + if (key == "min_filter") + rt.mTarget->setFilter(osg::Texture2D::MIN_FILTER, parseFilterMode()); + else if (key == "mag_filter") + rt.mTarget->setFilter(osg::Texture2D::MAG_FILTER, parseFilterMode()); + else if (key == "wrap_s") + rt.mTarget->setWrap(osg::Texture2D::WRAP_S, parseWrapMode()); + else if (key == "wrap_t") + rt.mTarget->setWrap(osg::Texture2D::WRAP_T, parseWrapMode()); + else if (key == "width_ratio") + rt.mSize.mWidthRatio = parseFloat(); + else if (key == "height_ratio") + rt.mSize.mHeightRatio = parseFloat(); + else if (key == "width") + rt.mSize.mWidth = parseInteger(); + else if (key == "height") + rt.mSize.mHeight = parseInteger(); + else if (key == "internal_format") + rt.mTarget->setInternalFormat(parseInternalFormat()); + else if (key == "source_type") + rt.mTarget->setSourceType(parseSourceType()); + else if (key == "source_format") + rt.mTarget->setSourceFormat(parseSourceFormat()); + else if (key == "mipmaps") + rt.mMipMap = parseBool(); + else + error(Misc::StringUtils::format("unexpected key '%s'", std::string(key))); + + expect(); + } + + mRenderTargets.emplace(mBlockName, std::move(rt)); + } + + template<> + void Technique::parseBlockImp() + { + if (!mLexer->jump()) + error(Misc::StringUtils::format("unterminated 'vertex' block, expected closing brackets")); + + auto& pass = mPassMap[mBlockName]; + + if (!pass) + pass = std::make_shared(); + + pass->mName = mBlockName; + + if (pass->mCompute) + error(Misc::StringUtils::format("'compute' block already defined. Usage is ambiguous.")); + else if (!pass->mVertex) + pass->mVertex = new osg::Shader(osg::Shader::VERTEX, getBlockWithLineDirective()); + else + error(Misc::StringUtils::format("duplicate vertex shader for block '%s'", std::string(mBlockName))); + + pass->mType = Pass::Type::Pixel; + } + + template<> + void Technique::parseBlockImp() + { + if (!mLexer->jump()) + error(Misc::StringUtils::format("unterminated 'fragment' block, expected closing brackets")); + + auto& pass = mPassMap[mBlockName]; + + if (!pass) + pass = std::make_shared(); + + pass->mUBO = mUBO; + pass->mName = mBlockName; + + if (pass->mCompute) + error(Misc::StringUtils::format("'compute' block already defined. Usage is ambiguous.")); + else if (!pass->mFragment) + pass->mFragment = new osg::Shader(osg::Shader::FRAGMENT, getBlockWithLineDirective()); + else + error(Misc::StringUtils::format("duplicate vertex shader for block '%s'", std::string(mBlockName))); + + pass->mType = Pass::Type::Pixel; + } + + template<> + void Technique::parseBlockImp() + { + if (!mLexer->jump()) + error(Misc::StringUtils::format("unterminated 'compute' block, expected closing brackets")); + + auto& pass = mPassMap[mBlockName]; + + if (!pass) + pass = std::make_shared(); + + pass->mName = mBlockName; + + if (pass->mFragment) + error(Misc::StringUtils::format("'fragment' block already defined. Usage is ambiguous.")); + else if (pass->mVertex) + error(Misc::StringUtils::format("'vertex' block already defined. Usage is ambiguous.")); + else if (!pass->mFragment) + pass->mCompute = new osg::Shader(osg::Shader::COMPUTE, getBlockWithLineDirective()); + else + error(Misc::StringUtils::format("duplicate vertex shader for block '%s'", std::string(mBlockName))); + + pass->mType = Pass::Type::Compute; + } + + template + void Technique::parseSampler() + { + if (findUniform(std::string(mBlockName)) != mDefinedUniforms.end()) + error(Misc::StringUtils::format("redeclaration of uniform '%s'", std::string(mBlockName))); + + ProxyTextureData proxy; + osg::ref_ptr sampler; + + constexpr bool is1D = std::is_same_v; + constexpr bool is3D = std::is_same_v; + + Types::SamplerType type; + + while (!isNext() && !isNext()) + { + expect(); + + auto key = asLiteral(); + + expect(); + + if (!is1D && key == "min_filter") + proxy.min_filter = parseFilterMode(); + else if (!is1D && key == "mag_filter") + proxy.mag_filter = parseFilterMode(); + else if (key == "wrap_s") + proxy.wrap_s = parseWrapMode(); + else if (key == "wrap_t") + proxy.wrap_t = parseWrapMode(); + else if (is3D && key == "wrap_r") + proxy.wrap_r = parseWrapMode(); + else if (key == "compression") + proxy.compression = parseCompression(); + else if (key == "source_type") + proxy.source_type = parseSourceType(); + else if (key == "source_format") + proxy.source_format = parseSourceFormat(); + else if (key == "internal_format") + proxy.internal_format = parseInternalFormat(); + else if (key == "source") + { + expect(); + auto image = mImageManager.getImage(std::string{std::get(mToken).value}, is3D); + if constexpr (is1D) + { + type = Types::SamplerType::Texture_1D; + sampler = new osg::Texture1D(image); + } + else if constexpr (is3D) + { + type = Types::SamplerType::Texture_3D; + sampler = new osg::Texture3D(image); + } + else + { + type = Types::SamplerType::Texture_2D; + sampler = new osg::Texture2D(image); + } + } + else + error(Misc::StringUtils::format("unexpected key '%s'", std::string{key})); + + expect(); + } + if (!sampler) + error(Misc::StringUtils::format("%s '%s' requires a filename", std::string(T::repr), std::string{mBlockName})); + + if (!is1D) + { + sampler->setFilter(osg::Texture::MIN_FILTER, proxy.min_filter); + sampler->setFilter(osg::Texture::MAG_FILTER, proxy.mag_filter); + } + if (is3D) + sampler->setWrap(osg::Texture::WRAP_R, proxy.wrap_r); + sampler->setWrap(osg::Texture::WRAP_S, proxy.wrap_s); + sampler->setWrap(osg::Texture::WRAP_T, proxy.wrap_t); + sampler->setInternalFormatMode(proxy.compression); + if (proxy.internal_format.has_value()) + sampler->setInternalFormat(proxy.internal_format.value()); + if (proxy.source_type.has_value()) + sampler->setSourceType(proxy.source_type.value()); + if (proxy.internal_format.has_value()) + sampler->setSourceFormat(proxy.internal_format.value()); + sampler->setName(std::string{mBlockName}); + sampler->setResizeNonPowerOfTwoHint(false); + + mTextures.emplace_back(sampler); + + std::shared_ptr uniform = std::make_shared(); + uniform->mSamplerType = type; + uniform->mName = std::string(mBlockName); + mDefinedUniforms.emplace_back(std::move(uniform)); + } + + template + void Technique::parseUniform() + { + if (findUniform(std::string(mBlockName)) != mDefinedUniforms.end()) + error(Misc::StringUtils::format("redeclaration of uniform '%s'", std::string(mBlockName))); + + std::shared_ptr uniform = std::make_shared(); + Types::Uniform data = Types::Uniform(); + + while (!isNext() && !isNext()) + { + expect(); + + auto key = asLiteral(); + + expect("error parsing config for uniform block"); + + constexpr bool isVec = std::is_same_v || std::is_same_v || std::is_same_v; + constexpr bool isFloat = std::is_same_v; + constexpr bool isInt = std::is_same_v; + constexpr bool isBool = std::is_same_v; + + static_assert(isVec || isFloat || isInt || isBool, "Unsupported type"); + + if (key == "default") + { + if constexpr (isVec) + data.mDefault = parseVec(); + else if constexpr (isFloat) + data.mDefault = parseFloat(); + else if constexpr (isInt) + data.mDefault = parseInteger(); + else if constexpr (isBool) + data.mDefault = parseBool(); + } + else if (key == "size") + { + if constexpr (isBool) + error("bool arrays currently unsupported"); + + int size = parseInteger(); + if (size > 1) + data.mArray = std::vector(size); + } + else if (key == "min") + { + if constexpr (isVec) + data.mMin = parseVec(); + else if constexpr (isFloat) + data.mMin = parseFloat(); + else if constexpr (isInt) + data.mMin = parseInteger(); + else if constexpr (isBool) + data.mMin = parseBool(); + } + else if (key == "max") + { + if constexpr (isVec) + data.mMax = parseVec(); + else if constexpr (isFloat) + data.mMax = parseFloat(); + else if constexpr (isInt) + data.mMax = parseInteger(); + else if constexpr (isBool) + data.mMax = parseBool(); + } + else if (key == "step") + uniform->mStep = parseFloat(); + else if (key == "static") + uniform->mStatic = parseBool(); + else if (key == "description") + { + expect(); + uniform->mDescription = std::get(mToken).value; + } + else if (key == "header") + { + expect(); + uniform->mHeader = std::get(mToken).value; + } + else if (key == "display_name") + { + expect(); + uniform->mDisplayName = std::get(mToken).value; + } + else + error(Misc::StringUtils::format("unexpected key '%s'", std::string{key})); + + expect(); + } + + if (data.isArray()) + uniform->mStatic = false; + + uniform->mName = std::string(mBlockName); + uniform->mData = data; + uniform->mTechniqueName = mName; + + if (data.mArray) + { + if constexpr (!std::is_same_v) + { + if (auto cached = Settings::ShaderManager::get().getValue>(mName, uniform->mName)) + uniform->setValue(cached.value()); + } + } + else if (auto cached = Settings::ShaderManager::get().getValue(mName, uniform->mName)) + { + uniform->setValue(cached.value()); + } + + mDefinedUniforms.emplace_back(std::move(uniform)); + } + + template<> + void Technique::parseBlockImp() + { + parseSampler(); + } + + template<> + void Technique::parseBlockImp() + { + parseSampler(); + } + + template<> + void Technique::parseBlockImp() + { + parseSampler(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template<> + void Technique::parseBlockImp() + { + parseUniform(); + } + + template + void Technique::expect(const std::string& err) + { + mToken = mLexer->next(); + if (!std::holds_alternative(mToken)) + { + if (err.empty()) + error(Misc::StringUtils::format("Expected %s", std::string(T::repr))); + else + error(Misc::StringUtils::format("%s. Expected %s", err, std::string(T::repr))); + } + } + + template + void Technique::expect(const std::string& err) + { + mToken = mLexer->next(); + if (!std::holds_alternative(mToken) && !std::holds_alternative(mToken)) + { + if (err.empty()) + error(Misc::StringUtils::format("%s. Expected %s or %s", err, std::string(T::repr), std::string(T2::repr))); + else + error(Misc::StringUtils::format("Expected %s or %s", std::string(T::repr), std::string(T2::repr))); + } + } + + template + bool Technique::isNext() + { + return std::holds_alternative(mLexer->peek()); + } + + void Technique::parse(std::string&& buffer) + { + mBuffer = std::move(buffer); + Misc::StringUtils::replaceAll(mBuffer, "\r\n", "\n"); + mLexer = std::make_unique(mBuffer); + + for (auto t = mLexer->next(); !std::holds_alternative(t); t = mLexer->next()) + { + std::visit([this](auto&& arg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) + parseBlock(false); + else if constexpr (std::is_same_v) + parseBlock(false); + else if constexpr (std::is_same_v) + parseBlock(false); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else if constexpr (std::is_same_v) + parseBlock(); + else + error("invalid top level block"); + } + , t); + } + } + + template + void Technique::parseBlock(bool named) + { + mBlockName = T::repr; + + if (named) + { + expect("name is required for preceeding block decleration"); + + mBlockName = std::get(mToken).value; + + if (isNext()) + parseBlockHeader(); + } + + expect(); + + parseBlockImp(); + + expect(); + } + + template + std::vector Technique::parseLiteralList() + { + std::vector data; + + while (!isNext()) + { + expect(); + + data.emplace_back(std::get(mToken).value); + + if (!isNext()) + break; + + mLexer->next(); + } + + return data; + } + + void Technique::parseBlockHeader() + { + expect(); + + if (isNext()) + { + mLexer->next(); + return; + } + + auto& pass = mPassMap[mBlockName]; + + if (!pass) + pass = std::make_shared(); + + bool clear = true; + osg::Vec4f clearColor = {1,1,1,1}; + + while (!isNext()) + { + expect("invalid key in block header"); + + std::string_view key = std::get(mToken).value; + + expect(); + + if (key == "target") + { + expect(); + pass->mTarget = std::get(mToken).value; + } + else if (key == "rt1") + { + expect(); + pass->mRenderTargets[0] = std::get(mToken).value; + } + else if (key == "rt2") + { + expect(); + pass->mRenderTargets[1] = std::get(mToken).value; + } + else if (key == "rt3") + { + expect(); + pass->mRenderTargets[2] =std::get(mToken).value; + } + else if (key == "blend") + { + expect(); + osg::BlendEquation::Equation blendEq = parseBlendEquation(); + expect(); + osg::BlendFunc::BlendFuncMode blendSrc = parseBlendFuncMode(); + expect(); + osg::BlendFunc::BlendFuncMode blendDest = parseBlendFuncMode(); + expect(); + + pass->mBlendSource = blendSrc; + pass->mBlendDest = blendDest; + if (blendEq != osg::BlendEquation::FUNC_ADD) + pass->mBlendEq = blendEq; + } + else if (key == "clear") + clear = parseBool(); + else if (key == "clear_color") + clearColor = parseVec(); + else + error(Misc::StringUtils::format("unrecognized key '%s' in block header", std::string(key))); + + mToken = mLexer->next(); + + if (std::holds_alternative(mToken)) + { + if (std::holds_alternative(mLexer->peek())) + error(Misc::StringUtils::format("leading comma in '%s' is not allowed", std::string(mBlockName))); + else + continue; + } + + if (std::holds_alternative(mToken)) + return; + } + + if (clear) + pass->mClearColor = clearColor; + + error("malformed block header"); + } + + std::string_view Technique::asLiteral() const + { + return std::get(mToken).value; + } + + FlagsType Technique::parseFlags() + { + auto parseBit = [this] (std::string_view term) { + for (const auto& [identifer, bit]: constants::TechniqueFlag) + { + if (Misc::StringUtils::ciEqual(term, identifer)) + return bit; + } + error(Misc::StringUtils::format("unrecognized flag '%s'", std::string(term))); + }; + + FlagsType flag = 0; + for (const auto& bit : parseLiteralList()) + flag |= parseBit(bit); + + return flag; + } + + osg::Texture::FilterMode Technique::parseFilterMode() + { + expect(); + + for (const auto& [identifer, mode]: constants::FilterMode) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized filter mode '%s'", std::string{asLiteral()})); + } + + osg::Texture::WrapMode Technique::parseWrapMode() + { + expect(); + + for (const auto& [identifer, mode]: constants::WrapMode) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized wrap mode '%s'", std::string{asLiteral()})); + } + + osg::Texture::InternalFormatMode Technique::parseCompression() + { + expect(); + + for (const auto& [identifer, mode]: constants::Compression) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized compression '%s'", std::string{asLiteral()})); + } + + int Technique::parseInternalFormat() + { + expect(); + + for (const auto& [identifer, mode]: constants::InternalFormat) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized internal format '%s'", std::string{asLiteral()})); + } + + int Technique::parseSourceType() + { + expect(); + + for (const auto& [identifer, mode]: constants::SourceType) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized source type '%s'", std::string{asLiteral()})); + } + + int Technique::parseSourceFormat() + { + expect(); + + for (const auto& [identifer, mode]: constants::SourceFormat) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized source format '%s'", std::string{asLiteral()})); + } + + osg::BlendEquation::Equation Technique::parseBlendEquation() + { + expect(); + + for (const auto& [identifer, mode]: constants::BlendEquation) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized blend equation '%s'", std::string{asLiteral()})); + } + + osg::BlendFunc::BlendFuncMode Technique::parseBlendFuncMode() + { + expect(); + + for (const auto& [identifer, mode]: constants::BlendFunc) + { + if (asLiteral() == identifer) + return mode; + } + + error(Misc::StringUtils::format("unrecognized blend function '%s'", std::string{asLiteral()})); + } + + bool Technique::parseBool() + { + mToken = mLexer->next(); + + if (std::holds_alternative(mToken)) + return true; + if (std::holds_alternative(mToken)) + return false; + + error("expected 'true' or 'false' as boolean value"); + } + + std::string_view Technique::parseString() + { + expect(); + + return std::get(mToken).value; + } + + float Technique::parseFloat() + { + mToken = mLexer->next(); + + if (std::holds_alternative(mToken)) + return std::get(mToken).value; + if (std::holds_alternative(mToken)) + return static_cast(std::get(mToken).value); + + error("expected float value"); + } + + int Technique::parseInteger() + { + expect(); + + return std::get(mToken).value; + } + + template + OSGVec Technique::parseVec() + { + expect(); + expect(); + + OSGVec value; + + for (int i = 0; i < OSGVec::num_components; ++i) + { + value[i] = parseFloat(); + + if (i < OSGVec::num_components - 1) + expect(); + } + + expect("check definition of the vector"); + + return value; + } +} diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp new file mode 100644 index 0000000000..1cb4e3e743 --- /dev/null +++ b/components/fx/technique.hpp @@ -0,0 +1,320 @@ +#ifndef OPENMW_COMPONENTS_FX_TECHNIQUE_H +#define OPENMW_COMPONENTS_FX_TECHNIQUE_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pass.hpp" +#include "lexer.hpp" +#include "types.hpp" + +namespace Resource +{ + class ImageManager; +} + +namespace VFS +{ + class Manager; +} + +namespace fx +{ + using FlagsType = size_t; + + struct DispatchNode + { + DispatchNode() = default; + + DispatchNode(const DispatchNode& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY) + : mHandle(other.mHandle) + , mFlags(other.mFlags) + , mRootStateSet(other.mRootStateSet) + { + mPasses.reserve(other.mPasses.size()); + + for (const auto& subpass : other.mPasses) + mPasses.emplace_back(subpass, copyOp); + } + + struct SubPass { + SubPass() = default; + + osg::ref_ptr mStateSet = new osg::StateSet; + osg::ref_ptr mRenderTarget; + osg::ref_ptr mRenderTexture; + bool mResolve = false; + + SubPass(const SubPass& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY) + : mStateSet(new osg::StateSet(*other.mStateSet, copyOp)) + , mResolve(other.mResolve) + { + if (other.mRenderTarget) + mRenderTarget = new osg::FrameBufferObject(*other.mRenderTarget, copyOp); + if (other.mRenderTexture) + mRenderTexture = new osg::Texture2D(*other.mRenderTexture, copyOp); + } + }; + + void compile() + { + for (auto rit = mPasses.rbegin(); rit != mPasses.rend(); ++rit) + { + if (!rit->mRenderTarget) + { + rit->mResolve = true; + break; + } + } + } + + // not safe to read/write in draw thread + std::shared_ptr mHandle = nullptr; + + FlagsType mFlags = 0; + + std::vector mPasses; + + osg::ref_ptr mRootStateSet = new osg::StateSet; + }; + + using DispatchArray = std::vector; + + class Technique + { + public: + using PassList = std::vector>; + using TexList = std::vector>; + + using UniformMap = std::vector>; + using RenderTargetMap = std::unordered_map; + + inline static std::string sExt = ".omwfx"; + inline static std::string sSubdir = "shaders"; + + enum class Status + { + Success, + Uncompiled, + File_Not_exists, + Parse_Error + }; + + static constexpr FlagsType Flag_Disable_Interiors = (1 << 0); + static constexpr FlagsType Flag_Disable_Exteriors = (1 << 1); + static constexpr FlagsType Flag_Disable_Underwater = (1 << 2); + static constexpr FlagsType Flag_Disable_Abovewater = (1 << 3); + static constexpr FlagsType Flag_Disable_SunGlare = (1 << 4); + static constexpr FlagsType Flag_Hidden = (1 << 5); + + Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, const std::string& name, int width, int height, bool ubo, bool supportsNormals); + + bool compile(); + + std::string getName() const; + + std::string getFileName() const; + + bool setLastModificationTime(std::filesystem::file_time_type timeStamp); + + bool isValid() const { return mValid; } + + bool getHDR() const { return mHDR; } + + bool getNormals() const { return mNormals && mSupportsNormals; } + + bool getLights() const { return mLights; } + + const PassList& getPasses() { return mPasses; } + + const TexList& getTextures() const { return mTextures; } + + Status getStatus() const { return mStatus; } + + std::string_view getAuthor() const { return mAuthor; } + + std::string_view getDescription() const { return mDescription; } + + std::string_view getVersion() const { return mVersion; } + + int getGLSLVersion() const { return mGLSLVersion; } + + std::string getGLSLProfile() const { return mGLSLProfile; } + + const std::unordered_set& getGLSLExtensions() const { return mGLSLExtensions; } + + osg::ref_ptr getMainTemplate() const { return mMainTemplate; } + + FlagsType getFlags() const { return mFlags; } + + bool getHidden() const { return mFlags & Flag_Hidden; } + + UniformMap& getUniformMap() { return mDefinedUniforms; } + + RenderTargetMap& getRenderTargetsMap() { return mRenderTargets; } + + std::string getLastError() const { return mLastError; } + + UniformMap::iterator findUniform(const std::string& name); + + bool getDynamic() const { return mDynamic; } + + void setLocked(bool locked) { mLocked = locked; } + bool getLocked() const { return mLocked; } + + private: + [[noreturn]] void error(const std::string& msg); + + void clear(); + + std::string_view asLiteral() const; + + template + void expect(const std::string& err=""); + + template + void expect(const std::string& err=""); + + template + bool isNext(); + + void parse(std::string&& buffer); + + template + void parseUniform(); + + template + void parseSampler(); + + template + void parseBlock(bool named=true); + + template + void parseBlockImp() {} + + void parseBlockHeader(); + + bool parseBool(); + + std::string_view parseString(); + + float parseFloat(); + + int parseInteger(); + + int parseInternalFormat(); + + int parseSourceType(); + + int parseSourceFormat(); + + osg::BlendEquation::Equation parseBlendEquation(); + + osg::BlendFunc::BlendFuncMode parseBlendFuncMode(); + + osg::Texture::WrapMode parseWrapMode(); + + osg::Texture::InternalFormatMode parseCompression(); + + FlagsType parseFlags(); + + osg::Texture::FilterMode parseFilterMode(); + + template + std::vector parseLiteralList(); + + template + OSGVec parseVec(); + + std::string getBlockWithLineDirective(); + + std::unique_ptr mLexer; + Lexer::Token mToken; + + std::string mShared; + std::string mName; + std::string mFileName; + std::string_view mBlockName; + std::string_view mAuthor; + std::string_view mDescription; + std::string_view mVersion; + + std::unordered_set mGLSLExtensions; + int mGLSLVersion; + std::string mGLSLProfile; + + FlagsType mFlags; + + Status mStatus; + + bool mEnabled; + + std::filesystem::file_time_type mLastModificationTime; + bool mValid; + bool mHDR; + bool mNormals; + bool mLights; + int mWidth; + int mHeight; + + osg::ref_ptr mMainTemplate; + RenderTargetMap mRenderTargets; + + TexList mTextures; + PassList mPasses; + + std::unordered_map> mPassMap; + std::vector mPassKeys; + + Pass::Type mLastAppliedType; + + UniformMap mDefinedUniforms; + + const VFS::Manager& mVFS; + Resource::ImageManager& mImageManager; + bool mUBO; + bool mSupportsNormals; + + std::string mBuffer; + + std::string mLastError; + + bool mDynamic = false; + bool mLocked = false; + }; + + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); + template<> void Technique::parseBlockImp(); +} + +#endif diff --git a/components/fx/types.hpp b/components/fx/types.hpp new file mode 100644 index 0000000000..ba809cdd9a --- /dev/null +++ b/components/fx/types.hpp @@ -0,0 +1,305 @@ +#ifndef OPENMW_COMPONENTS_FX_TYPES_H +#define OPENMW_COMPONENTS_FX_TYPES_H + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "pass.hpp" + +namespace fx +{ + namespace Types + { + struct SizeProxy + { + std::optional mWidthRatio; + std::optional mHeightRatio; + std::optional mWidth; + std::optional mHeight; + + std::tuple get(int width, int height) const + { + int scaledWidth = width; + int scaledHeight = height; + + if (mWidthRatio) + scaledWidth = width * mWidthRatio.value(); + else if (mWidth) + scaledWidth = mWidth.value(); + + if (mHeightRatio > 0.f) + scaledHeight = height * mHeightRatio.value(); + else if (mHeight) + scaledHeight = mHeight.value(); + + return std::make_tuple(scaledWidth, scaledHeight); + } + }; + + struct RenderTarget + { + osg::ref_ptr mTarget = new osg::Texture2D; + SizeProxy mSize; + bool mMipMap = false; + }; + + template + struct Uniform + { + std::optional mValue; + std::optional> mArray; + + T mDefault = {}; + T mMin = std::numeric_limits::lowest(); + T mMax = std::numeric_limits::max(); + + using value_type = T; + + bool isArray() const + { + return mArray.has_value(); + } + + const std::vector& getArray() const + { + return *mArray; + } + + T getValue() const + { + return mValue.value_or(mDefault); + } + }; + + using Uniform_t = std::variant< + Uniform, + Uniform, + Uniform, + Uniform, + Uniform, + Uniform + >; + + enum SamplerType + { + Texture_1D, + Texture_2D, + Texture_3D + }; + + struct UniformBase + { + std::string mName; + std::string mDisplayName; + std::string mHeader; + std::string mTechniqueName; + std::string mDescription; + + bool mStatic = true; + std::optional mSamplerType = std::nullopt; + double mStep = 1.0; + + Uniform_t mData; + + template + T getValue() const + { + auto value = Settings::ShaderManager::get().getValue(mTechniqueName, mName); + + return value.value_or(std::get>(mData).getValue()); + } + + size_t getNumElements() const + { + return std::visit([&](auto&& arg) { ;return arg.isArray() ? arg.getArray().size() : 1; }, mData); + } + + template + T getMin() const + { + return std::get>(mData).mMin; + } + + template + T getMax() const + { + return std::get>(mData).mMax; + } + + template + T getDefault() const + { + return std::get>(mData).mDefault; + } + + template + void setValue(const T& value) + { + std::visit([&, value](auto&& arg){ + using U = typename std::decay_t::value_type; + + if constexpr (std::is_same_v) + { + arg.mValue = value; + + Settings::ShaderManager::get().setValue(mTechniqueName, mName, value); + } + else + { + Log(Debug::Warning) << "Attempting to set uniform '" << mName << "' with wrong type"; + } + }, mData); + } + + template + void setValue(const std::vector& value) + { + std::visit([&, value](auto&& arg) { + using U = typename std::decay_t::value_type; + + if (!arg.isArray() || arg.getArray().size() != value.size()) + { + Log(Debug::Error) << "Attempting to set uniform array '" << mName << "' with mismatching array sizes"; + return; + } + + if constexpr (std::is_same_v) + { + arg.mArray = value; + Settings::ShaderManager::get().setValue(mTechniqueName, mName, value); + } + else + Log(Debug::Warning) << "Attempting to set uniform array '" << mName << "' with wrong type"; + }, mData); + } + + void setUniform(osg::Uniform* uniform) + { + auto type = getType(); + if (!type || type.value() != uniform->getType()) + return; + + std::visit([&](auto&& arg) + { + if (arg.isArray()) + { + for (size_t i = 0; i < arg.getArray().size(); ++i) + uniform->setElement(i, arg.getArray()[i]); + uniform->dirty(); + } + else + uniform->set(arg.getValue()); + }, mData); + } + + std::optional getType() const + { + return std::visit([](auto&& arg) -> std::optional { + using T = typename std::decay_t::value_type; + + if constexpr (std::is_same_v) + return osg::Uniform::FLOAT_VEC2; + else if constexpr (std::is_same_v) + return osg::Uniform::FLOAT_VEC3; + else if constexpr (std::is_same_v) + return osg::Uniform::FLOAT_VEC4; + else if constexpr (std::is_same_v) + return osg::Uniform::FLOAT; + else if constexpr (std::is_same_v) + return osg::Uniform::INT; + else if constexpr (std::is_same_v) + return osg::Uniform::BOOL; + + return std::nullopt; + }, mData); + } + + std::optional getGLSL() + { + if (mSamplerType) + { + switch (mSamplerType.value()) + { + case Texture_1D: + return Misc::StringUtils::format("uniform sampler1D %s;", mName); + case Texture_2D: + return Misc::StringUtils::format("uniform sampler2D %s;", mName); + case Texture_3D: + return Misc::StringUtils::format("uniform sampler3D %s;", mName); + } + } + + return std::visit([&](auto&& arg) -> std::optional { + using T = typename std::decay_t::value_type; + + auto value = arg.getValue(); + + const bool useUniform = arg.isArray() || (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug || mStatic == false); + const std::string uname = arg.isArray() ? Misc::StringUtils::format("%s[%zu]", mName, arg.getArray().size()) : mName; + + if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform vec2 %s;", uname); + + return Misc::StringUtils::format("const vec2 %s=vec2(%f,%f);", mName, value[0], value[1]); + } + else if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform vec3 %s;", uname); + + return Misc::StringUtils::format("const vec3 %s=vec3(%f,%f,%f);", mName, value[0], value[1], value[2]); + } + else if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform vec4 %s;", uname); + + return Misc::StringUtils::format("const vec4 %s=vec4(%f,%f,%f,%f);", mName, value[0], value[1], value[2], value[3]); + } + else if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform float %s;", uname); + + return Misc::StringUtils::format("const float %s=%f;", mName, value); + } + else if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform int %s;", uname); + + return Misc::StringUtils::format("const int %s=%i;", mName, value); + } + else if constexpr (std::is_same_v) + { + if (useUniform) + return Misc::StringUtils::format("uniform bool %s;", uname); + + return Misc::StringUtils::format("const bool %s=%s;", mName, value ? "true" : "false"); + } + + return std::nullopt; + + }, mData); + } + + }; + } +} + +#endif diff --git a/components/fx/widgets.cpp b/components/fx/widgets.cpp new file mode 100644 index 0000000000..749f0a1c6a --- /dev/null +++ b/components/fx/widgets.cpp @@ -0,0 +1,166 @@ +#include "widgets.hpp" + +#include + +namespace +{ + template + void createVectorWidget(const std::shared_ptr& uniform, MyGUI::Widget* client, fx::Widgets::UniformBase* base) + { + int height = client->getHeight(); + base->setSize(base->getSize().width, (base->getSize().height - height) + (height * T::num_components)); + client->setSize(client->getSize().width, height * T::num_components); + + for (int i = 0; i < T::num_components; ++i) + { + auto* widget = client->createWidget("MW_ValueEditNumber", {0, height * i, client->getWidth(), height}, MyGUI::Align::Default); + widget->setData(uniform, static_cast(i)); + base->addItem(widget); + } + } +} + +namespace fx +{ + namespace Widgets + { + void EditBool::setValue(bool value) + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + mCheckbutton->setCaptionWithReplacing(value ? "#{sOn}" : "#{sOff}"); + mFill->setVisible(value); + + uniform->setValue(value); + } + + void EditBool::setValueFromUniform() + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + setValue(uniform->template getValue()); + } + + void EditBool::toDefault() + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + setValue(uniform->getDefault()); + } + + void EditBool::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mCheckbutton, "Checkbutton"); + assignWidget(mFill, "Fill"); + + mCheckbutton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditBool::notifyMouseButtonClick); + } + + + void EditBool::notifyMouseButtonClick(MyGUI::Widget* sender) + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + setValue(!uniform->getValue()); + } + + void UniformBase::init(const std::shared_ptr& uniform) + { + mLabel->setCaption(uniform->mDisplayName.empty() ? uniform->mName : uniform->mDisplayName); + + if (uniform->mDescription.empty()) + { + mLabel->setUserString("ToolTipType", ""); + } + else + { + mLabel->setUserString("ToolTipType", "Layout"); + mLabel->setUserString("ToolTipLayout", "TextToolTip"); + mLabel->setUserString("Caption_Text", uniform->mDescription); + } + + std::visit([this, &uniform](auto&& arg) { + using T = typename std::decay_t::value_type; + + if constexpr (std::is_same_v) + { + createVectorWidget(uniform, mClient, this); + } + else if constexpr (std::is_same_v) + { + createVectorWidget(uniform, mClient, this); + } + else if constexpr (std::is_same_v) + { + createVectorWidget(uniform, mClient, this); + } + else if constexpr (std::is_same_v) + { + auto* widget = mClient->createWidget("MW_ValueEditNumber", {0, 0, mClient->getWidth(), mClient->getHeight()}, MyGUI::Align::Stretch); + widget->setData(uniform); + mBases.emplace_back(widget); + } + else if constexpr (std::is_same_v) + { + auto* widget = mClient->createWidget("MW_ValueEditNumber", {0, 0, mClient->getWidth(), mClient->getHeight()}, MyGUI::Align::Stretch); + widget->setData(uniform); + mBases.emplace_back(widget); + } + else if constexpr (std::is_same_v) + { + auto* widget = mClient->createWidget("MW_ValueEditBool", {0, 0, mClient->getWidth(), mClient->getHeight()}, MyGUI::Align::Stretch); + widget->setData(uniform); + mBases.emplace_back(widget); + } + + mReset->eventMouseButtonClick += MyGUI::newDelegate(this, &UniformBase::notifyResetClicked); + + for (EditBase* base : mBases) + base->setValueFromUniform(); + + }, uniform->mData); + } + + void UniformBase::addItem(EditBase* item) + { + mBases.emplace_back(item); + } + + void UniformBase::toDefault() + { + for (EditBase* base : mBases) + { + if (base) + base->toDefault(); + } + } + + void UniformBase::notifyResetClicked(MyGUI::Widget* sender) + { + toDefault(); + } + + void UniformBase::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mReset, "Reset"); + assignWidget(mLabel, "Label"); + assignWidget(mClient, "Client"); + } + } +} \ No newline at end of file diff --git a/components/fx/widgets.hpp b/components/fx/widgets.hpp new file mode 100644 index 0000000000..061bd2d959 --- /dev/null +++ b/components/fx/widgets.hpp @@ -0,0 +1,280 @@ +#ifndef OPENMW_COMPONENTS_FX_WIDGETS_H +#define OPENMW_COMPONENTS_FX_WIDGETS_H + +#include +#include +#include + +#include +#include +#include + +#include + +#include "technique.hpp" +#include "types.hpp" + +namespace Gui +{ + class AutoSizedTextBox; + class AutoSizedButton; +} + +namespace fx +{ + namespace Widgets + { + enum Index + { + None = -1, + Zero = 0, + One = 1, + Two = 2, + Three = 3 + }; + + class EditBase + { + public: + virtual ~EditBase() = default; + + void setData(const std::shared_ptr& uniform, Index index = None) + { + mUniform = uniform; + mIndex = index; + } + + virtual void setValueFromUniform() = 0; + + virtual void toDefault() = 0; + + protected: + std::weak_ptr mUniform; + Index mIndex; + }; + + class EditBool : public EditBase, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(EditBool) + + public: + void setValue(bool value); + void setValueFromUniform() override; + void toDefault() override; + + private: + void initialiseOverride() override; + void notifyMouseButtonClick(MyGUI::Widget* sender); + + MyGUI::Button* mCheckbutton{nullptr}; + MyGUI::Widget* mFill{nullptr}; + }; + + template + class EditNumber : public EditBase, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(EditNumber) + + public: + void setValue(T value) + { + mValue = value; + if constexpr (std::is_floating_point_v) + mValueLabel->setCaption(Misc::StringUtils::format("%.3f", mValue)); + else + mValueLabel->setCaption(std::to_string(mValue)); + + float range = 0.f; + float min = 0.f; + + if (auto uniform = mUniform.lock()) + { + if constexpr (std::is_fundamental_v) + { + uniform->template setValue(mValue); + range = uniform->template getMax() - uniform->template getMin(); + min = uniform->template getMin(); + } + else + { + UType uvalue = uniform->template getValue(); + uvalue[mIndex] = mValue; + uniform->template setValue(uvalue); + range = uniform->template getMax()[mIndex] - uniform->template getMin()[mIndex]; + min = uniform->template getMin()[mIndex]; + } + } + + float fill = (range == 0.f) ? 1.f : (mValue - min) / range; + mFill->setRealSize(fill, 1.0); + } + + void setValueFromUniform() override + { + if (auto uniform = mUniform.lock()) + { + T value; + + if constexpr (std::is_fundamental_v) + value = uniform->template getValue(); + else + value = uniform->template getValue()[mIndex]; + + setValue(value); + } + } + + void toDefault() override + { + if (auto uniform = mUniform.lock()) + { + if constexpr (std::is_fundamental_v) + setValue(uniform->template getDefault()); + else + setValue(uniform->template getDefault()[mIndex]); + } + } + + private: + + void initialiseOverride() override + { + Base::initialiseOverride(); + + assignWidget(mDragger, "Dragger"); + assignWidget(mValueLabel, "Value"); + assignWidget(mButtonIncrease, "ButtonIncrease"); + assignWidget(mButtonDecrease, "ButtonDecrease"); + assignWidget(mFill, "Fill"); + + mButtonIncrease->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNumber::notifyButtonClicked); + mButtonDecrease->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNumber::notifyButtonClicked); + + mDragger->eventMouseButtonPressed += MyGUI::newDelegate(this, &EditNumber::notifyMouseButtonPressed); + mDragger->eventMouseDrag += MyGUI::newDelegate(this, &EditNumber::notifyMouseButtonDragged); + mDragger->eventMouseWheel += MyGUI::newDelegate(this, &EditNumber::notifyMouseWheel); + } + + void notifyMouseWheel(MyGUI::Widget* sender, int rel) + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + if (rel > 0) + increment(uniform->mStep); + else + increment(-uniform->mStep); + } + + void notifyMouseButtonDragged(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + int delta = left - mLastPointerX; + + // allow finer tuning when shift is pressed + constexpr double scaling = 20.0; + T step = MyGUI::InputManager::getInstance().isShiftPressed() ? uniform->mStep / scaling : uniform->mStep; + + if (step == 0) + { + if constexpr (std::is_integral_v) + step = 1; + else + step = uniform->mStep; + } + + if (delta > 0) + increment(step); + else if (delta < 0) + increment(-step); + + mLastPointerX = left; + } + + void notifyMouseButtonPressed(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + mLastPointerX = left; + } + + void increment(T step) + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + if constexpr (std::is_fundamental_v) + setValue(std::clamp(uniform->template getValue() + step, uniform->template getMin(), uniform->template getMax())); + else + setValue(std::clamp(uniform->template getValue()[mIndex] + step, uniform->template getMin()[mIndex], uniform->template getMax()[mIndex])); + } + + void notifyButtonClicked(MyGUI::Widget* sender) + { + auto uniform = mUniform.lock(); + + if (!uniform) + return; + + if (sender == mButtonDecrease) + increment(-uniform->mStep); + else if (sender == mButtonIncrease) + increment(uniform->mStep); + } + + MyGUI::Button* mButtonDecrease{nullptr}; + MyGUI::Button* mButtonIncrease{nullptr}; + MyGUI::Widget* mDragger{nullptr}; + MyGUI::Widget* mFill{nullptr}; + MyGUI::TextBox* mValueLabel{nullptr}; + T mValue{}; + + int mLastPointerX{0}; + }; + + class EditNumberFloat4 : public EditNumber { MYGUI_RTTI_DERIVED(EditNumberFloat4) }; + class EditNumberFloat3 : public EditNumber { MYGUI_RTTI_DERIVED(EditNumberFloat3) }; + class EditNumberFloat2 : public EditNumber { MYGUI_RTTI_DERIVED(EditNumberFloat2) }; + class EditNumberFloat : public EditNumber { MYGUI_RTTI_DERIVED(EditNumberFloat) }; + class EditNumberInt : public EditNumber { MYGUI_RTTI_DERIVED(EditNumberInt) }; + + class UniformBase final : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(UniformBase) + + public: + void init(const std::shared_ptr& uniform); + + void toDefault(); + + void addItem(EditBase* item); + + Gui::AutoSizedTextBox* getLabel() { return mLabel; } + + private: + + void notifyResetClicked(MyGUI::Widget* sender); + + void initialiseOverride() override; + + Gui::AutoSizedButton* mReset{nullptr}; + Gui::AutoSizedTextBox* mLabel{nullptr}; + MyGUI::Widget* mClient{nullptr}; + std::vector mBases; + }; + } +} + +#endif diff --git a/components/interpreter/context.hpp b/components/interpreter/context.hpp index 862018bdc0..df97bcf232 100644 --- a/components/interpreter/context.hpp +++ b/components/interpreter/context.hpp @@ -2,6 +2,7 @@ #define INTERPRETER_CONTEXT_H_INCLUDED #include +#include #include namespace Interpreter @@ -12,6 +13,8 @@ namespace Interpreter virtual ~Context() {} + virtual std::string getTarget() const = 0; + virtual int getLocalShort (int index) const = 0; virtual int getLocalLong (int index) const = 0; @@ -35,23 +38,23 @@ namespace Interpreter virtual void report (const std::string& message) = 0; - virtual int getGlobalShort (const std::string& name) const = 0; + virtual int getGlobalShort(std::string_view name) const = 0; - virtual int getGlobalLong (const std::string& name) const = 0; + virtual int getGlobalLong(std::string_view name) const = 0; - virtual float getGlobalFloat (const std::string& name) const = 0; + virtual float getGlobalFloat(std::string_view name) const = 0; - virtual void setGlobalShort (const std::string& name, int value) = 0; + virtual void setGlobalShort(std::string_view name, int value) = 0; - virtual void setGlobalLong (const std::string& name, int value) = 0; + virtual void setGlobalLong(std::string_view name, int value) = 0; - virtual void setGlobalFloat (const std::string& name, float value) = 0; + virtual void setGlobalFloat(std::string_view name, float value) = 0; virtual std::vector getGlobals () const = 0; - virtual char getGlobalType (const std::string& name) const = 0; + virtual char getGlobalType(std::string_view name) const = 0; - virtual std::string getActionBinding(const std::string& action) const = 0; + virtual std::string getActionBinding(std::string_view action) const = 0; virtual std::string getActorName() const = 0; @@ -77,17 +80,17 @@ namespace Interpreter virtual std::string getCurrentCellName() const = 0; - virtual int getMemberShort (const std::string& id, const std::string& name, bool global) const = 0; + virtual int getMemberShort(std::string_view id, std::string_view name, bool global) const = 0; - virtual int getMemberLong (const std::string& id, const std::string& name, bool global) const = 0; + virtual int getMemberLong(std::string_view id, std::string_view name, bool global) const = 0; - virtual float getMemberFloat (const std::string& id, const std::string& name, bool global) const = 0; + virtual float getMemberFloat(std::string_view id, std::string_view name, bool global) const = 0; - virtual void setMemberShort (const std::string& id, const std::string& name, int value, bool global) = 0; + virtual void setMemberShort(std::string_view id, std::string_view name, int value, bool global) = 0; - virtual void setMemberLong (const std::string& id, const std::string& name, int value, bool global) = 0; + virtual void setMemberLong(std::string_view id, std::string_view name, int value, bool global) = 0; - virtual void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) + virtual void setMemberFloat(std::string_view id, std::string_view name, float value, bool global) = 0; }; } diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp index 0ceed80d53..164e7b2126 100644 --- a/components/interpreter/defines.cpp +++ b/components/interpreter/defines.cpp @@ -26,7 +26,7 @@ namespace Interpreter{ return a.length() > b.length(); } - std::string fixDefinesReal(std::string text, bool dialogue, Context& context) + static std::string fixDefinesReal(const std::string& text, bool dialogue, Context& context) { unsigned int start = 0; std::ostringstream retval; @@ -172,11 +172,11 @@ namespace Interpreter{ for(unsigned int j = 0; j < globals.size(); j++){ if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name - temp = text.substr(i+1, globals[j].length()); - transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + temp = Misc::StringUtils::lowerCase(text.substr(i+1, globals[j].length())); } - if((found = check(temp, globals[j], &i, &start))){ + found = check(temp, globals[j], &i, &start); + if(found){ char type = context.getGlobalType(globals[j]); switch(type){ diff --git a/components/interpreter/docs/vmformat.txt b/components/interpreter/docs/vmformat.txt index b5c9cf0ae0..7eac8b26e0 100644 --- a/components/interpreter/docs/vmformat.txt +++ b/components/interpreter/docs/vmformat.txt @@ -77,7 +77,7 @@ op 15: div (integer) stack[1] by stack[0], pop twice, push result op 16: div (float) stack[1] by stack[0], pop twice, push result op 17: convert stack[1] from integer to float op 18: convert stack[1] from float to integer -op 19: take square root of stack[0] (float) +opcode 19 unused op 20: return op 21: replace stack[0] with local short stack[0] op 22: replace stack[0] with local long stack[0] diff --git a/components/interpreter/installopcodes.cpp b/components/interpreter/installopcodes.cpp index afee36bc28..d5e9bb0cc5 100644 --- a/components/interpreter/installopcodes.cpp +++ b/components/interpreter/installopcodes.cpp @@ -11,90 +11,77 @@ namespace Interpreter { - void installOpcodes (Interpreter& interpreter) + void installOpcodes(Interpreter& interpreter) { // generic - interpreter.installSegment0 (0, new OpPushInt); - interpreter.installSegment5 (3, new OpIntToFloat); - interpreter.installSegment5 (6, new OpFloatToInt); - interpreter.installSegment5 (7, new OpNegateInt); - interpreter.installSegment5 (8, new OpNegateFloat); - interpreter.installSegment5 (17, new OpIntToFloat1); - interpreter.installSegment5 (18, new OpFloatToInt1); + interpreter.installSegment0(0); + interpreter.installSegment5(3); + interpreter.installSegment5(6); + interpreter.installSegment5(7); + interpreter.installSegment5(8); + interpreter.installSegment5(17); + interpreter.installSegment5(18); // local variables, global variables & literals - interpreter.installSegment5 (0, new OpStoreLocalShort); - interpreter.installSegment5 (1, new OpStoreLocalLong); - interpreter.installSegment5 (2, new OpStoreLocalFloat); - interpreter.installSegment5 (4, new OpFetchIntLiteral); - interpreter.installSegment5 (5, new OpFetchFloatLiteral); - interpreter.installSegment5 (21, new OpFetchLocalShort); - interpreter.installSegment5 (22, new OpFetchLocalLong); - interpreter.installSegment5 (23, new OpFetchLocalFloat); - interpreter.installSegment5 (39, new OpStoreGlobalShort); - interpreter.installSegment5 (40, new OpStoreGlobalLong); - interpreter.installSegment5 (41, new OpStoreGlobalFloat); - interpreter.installSegment5 (42, new OpFetchGlobalShort); - interpreter.installSegment5 (43, new OpFetchGlobalLong); - interpreter.installSegment5 (44, new OpFetchGlobalFloat); - interpreter.installSegment5 (59, new OpStoreMemberShort (false)); - interpreter.installSegment5 (60, new OpStoreMemberLong (false)); - interpreter.installSegment5 (61, new OpStoreMemberFloat (false)); - interpreter.installSegment5 (62, new OpFetchMemberShort (false)); - interpreter.installSegment5 (63, new OpFetchMemberLong (false)); - interpreter.installSegment5 (64, new OpFetchMemberFloat (false)); - interpreter.installSegment5 (65, new OpStoreMemberShort (true)); - interpreter.installSegment5 (66, new OpStoreMemberLong (true)); - interpreter.installSegment5 (67, new OpStoreMemberFloat (true)); - interpreter.installSegment5 (68, new OpFetchMemberShort (true)); - interpreter.installSegment5 (69, new OpFetchMemberLong (true)); - interpreter.installSegment5 (70, new OpFetchMemberFloat (true)); + interpreter.installSegment5(0); + interpreter.installSegment5(1); + interpreter.installSegment5(2); + interpreter.installSegment5(4); + interpreter.installSegment5(5); + interpreter.installSegment5(21); + interpreter.installSegment5(22); + interpreter.installSegment5(23); + interpreter.installSegment5(39); + interpreter.installSegment5(40); + interpreter.installSegment5(41); + interpreter.installSegment5(42); + interpreter.installSegment5(43); + interpreter.installSegment5(44); + interpreter.installSegment5>(59); + interpreter.installSegment5>(60); + interpreter.installSegment5>(61); + interpreter.installSegment5>(62); + interpreter.installSegment5>(63); + interpreter.installSegment5>(64); + interpreter.installSegment5>(65); + interpreter.installSegment5>(66); + interpreter.installSegment5>(67); + interpreter.installSegment5>(68); + interpreter.installSegment5>(69); + interpreter.installSegment5>(70); // math - interpreter.installSegment5 (9, new OpAddInt); - interpreter.installSegment5 (10, new OpAddInt); - interpreter.installSegment5 (11, new OpSubInt); - interpreter.installSegment5 (12, new OpSubInt); - interpreter.installSegment5 (13, new OpMulInt); - interpreter.installSegment5 (14, new OpMulInt); - interpreter.installSegment5 (15, new OpDivInt); - interpreter.installSegment5 (16, new OpDivInt); - interpreter.installSegment5 (19, new OpSquareRoot); - interpreter.installSegment5 (26, - new OpCompare >); - interpreter.installSegment5 (27, - new OpCompare >); - interpreter.installSegment5 (28, - new OpCompare >); - interpreter.installSegment5 (29, - new OpCompare >); - interpreter.installSegment5 (30, - new OpCompare >); - interpreter.installSegment5 (31, - new OpCompare >); + interpreter.installSegment5>(9); + interpreter.installSegment5>(10); + interpreter.installSegment5>(11); + interpreter.installSegment5>(12); + interpreter.installSegment5>(13); + interpreter.installSegment5>(14); + interpreter.installSegment5>(15); + interpreter.installSegment5>(16); + interpreter.installSegment5 >>(26); + interpreter.installSegment5 >>(27); + interpreter.installSegment5 >>(28); + interpreter.installSegment5 >>(29); + interpreter.installSegment5 >>(30); + interpreter.installSegment5 >>(31); - interpreter.installSegment5 (32, - new OpCompare >); - interpreter.installSegment5 (33, - new OpCompare >); - interpreter.installSegment5 (34, - new OpCompare >); - interpreter.installSegment5 (35, - new OpCompare >); - interpreter.installSegment5 (36, - new OpCompare >); - interpreter.installSegment5 (37, - new OpCompare >); + interpreter.installSegment5 >>(32); + interpreter.installSegment5 >>(33); + interpreter.installSegment5 >>(34); + interpreter.installSegment5 >>(35); + interpreter.installSegment5 >>(36); + interpreter.installSegment5 >>(37); // control structures - interpreter.installSegment5 (20, new OpReturn); - interpreter.installSegment5 (24, new OpSkipZero); - interpreter.installSegment5 (25, new OpSkipNonZero); - interpreter.installSegment0 (1, new OpJumpForward); - interpreter.installSegment0 (2, new OpJumpBackward); + interpreter.installSegment5(20); + interpreter.installSegment5(24); + interpreter.installSegment5(25); + interpreter.installSegment0(1); + interpreter.installSegment0(2); // misc - interpreter.installSegment3 (0, new OpMessageBox); - interpreter.installSegment5 (58, new OpReport); + interpreter.installSegment3(0); + interpreter.installSegment5(58); } } diff --git a/components/interpreter/interpreter.cpp b/components/interpreter/interpreter.cpp index 0b636092c3..7047dec05f 100644 --- a/components/interpreter/interpreter.cpp +++ b/components/interpreter/interpreter.cpp @@ -2,97 +2,81 @@ #include #include +#include #include "opcodes.hpp" namespace Interpreter { + [[noreturn]] static void abortUnknownCode(int segment, int opcode) + { + const std::string error = "unknown opcode " + std::to_string(opcode) + " in segment " + std::to_string(segment); + throw std::runtime_error(error); + } + + [[noreturn]] static void abortUnknownSegment(Type_Code code) + { + const std::string error = "opcode outside of the allocated segment range: " + std::to_string(code); + throw std::runtime_error(error); + } + + template + auto& getDispatcher(const T& segment, unsigned int seg, int opcode) + { + auto it = segment.find(opcode); + if (it == segment.end()) + { + abortUnknownCode(seg, opcode); + } + return it->second; + } + void Interpreter::execute (Type_Code code) { - unsigned int segSpec = code>>30; + unsigned int segSpec = code >> 30; switch (segSpec) { case 0: { - int opcode = code>>24; - unsigned int arg0 = code & 0xffffff; + const int opcode = code >> 24; + const unsigned int arg0 = code & 0xffffff; - std::map::iterator iter = mSegment0.find (opcode); - - if (iter==mSegment0.end()) - abortUnknownCode (0, opcode); - - iter->second->execute (mRuntime, arg0); - - return; + return getDispatcher(mSegment0, 0, opcode)->execute(mRuntime, arg0); } case 2: { - int opcode = (code>>20) & 0x3ff; - unsigned int arg0 = code & 0xfffff; - - std::map::iterator iter = mSegment2.find (opcode); + const int opcode = (code >> 20) & 0x3ff; + const unsigned int arg0 = code & 0xfffff; - if (iter==mSegment2.end()) - abortUnknownCode (2, opcode); - - iter->second->execute (mRuntime, arg0); - - return; + return getDispatcher(mSegment2, 2, opcode)->execute(mRuntime, arg0); } } - segSpec = code>>26; + segSpec = code >> 26; switch (segSpec) { case 0x30: { - int opcode = (code>>8) & 0x3ffff; - unsigned int arg0 = code & 0xff; - - std::map::iterator iter = mSegment3.find (opcode); + const int opcode = (code >> 8) & 0x3ffff; + const unsigned int arg0 = code & 0xff; - if (iter==mSegment3.end()) - abortUnknownCode (3, opcode); - - iter->second->execute (mRuntime, arg0); - - return; + return getDispatcher(mSegment3, 3, opcode)->execute(mRuntime, arg0); } case 0x32: { - int opcode = code & 0x3ffffff; - - std::map::iterator iter = mSegment5.find (opcode); + const int opcode = code & 0x3ffffff; - if (iter==mSegment5.end()) - abortUnknownCode (5, opcode); - - iter->second->execute (mRuntime); - - return; + return getDispatcher(mSegment5, 5, opcode)->execute(mRuntime); } } abortUnknownSegment (code); } - void Interpreter::abortUnknownCode (int segment, int opcode) - { - const std::string error = "unknown opcode " + std::to_string(opcode) + " in segment " + std::to_string(segment); - throw std::runtime_error (error); - } - - void Interpreter::abortUnknownSegment (Type_Code code) - { - const std::string error = "opcode outside of the allocated segment range: " + std::to_string(code); - throw std::runtime_error (error); - } - void Interpreter::begin() { if (mRunning) @@ -123,49 +107,6 @@ namespace Interpreter Interpreter::Interpreter() : mRunning (false) {} - Interpreter::~Interpreter() - { - for (std::map::iterator iter (mSegment0.begin()); - iter!=mSegment0.end(); ++iter) - delete iter->second; - - for (std::map::iterator iter (mSegment2.begin()); - iter!=mSegment2.end(); ++iter) - delete iter->second; - - for (std::map::iterator iter (mSegment3.begin()); - iter!=mSegment3.end(); ++iter) - delete iter->second; - - for (std::map::iterator iter (mSegment5.begin()); - iter!=mSegment5.end(); ++iter) - delete iter->second; - } - - void Interpreter::installSegment0 (int code, Opcode1 *opcode) - { - assert(mSegment0.find(code) == mSegment0.end()); - mSegment0.insert (std::make_pair (code, opcode)); - } - - void Interpreter::installSegment2 (int code, Opcode1 *opcode) - { - assert(mSegment2.find(code) == mSegment2.end()); - mSegment2.insert (std::make_pair (code, opcode)); - } - - void Interpreter::installSegment3 (int code, Opcode1 *opcode) - { - assert(mSegment3.find(code) == mSegment3.end()); - mSegment3.insert (std::make_pair (code, opcode)); - } - - void Interpreter::installSegment5 (int code, Opcode0 *opcode) - { - assert(mSegment5.find(code) == mSegment5.end()); - mSegment5.insert (std::make_pair (code, opcode)); - } - void Interpreter::run (const Type_Code *code, int codeSize, Context& context) { assert (codeSize>=4); diff --git a/components/interpreter/interpreter.hpp b/components/interpreter/interpreter.hpp index ff3bcf7b7c..2e08e6614b 100644 --- a/components/interpreter/interpreter.hpp +++ b/components/interpreter/interpreter.hpp @@ -3,24 +3,25 @@ #include #include +#include +#include +#include #include "runtime.hpp" #include "types.hpp" +#include "opcodes.hpp" namespace Interpreter { - class Opcode0; - class Opcode1; - class Interpreter { std::stack mCallstack; bool mRunning; Runtime mRuntime; - std::map mSegment0; - std::map mSegment2; - std::map mSegment3; - std::map mSegment5; + std::map> mSegment0; + std::map> mSegment2; + std::map> mSegment3; + std::map> mSegment5; // not implemented Interpreter (const Interpreter&); @@ -28,31 +29,44 @@ namespace Interpreter void execute (Type_Code code); - void abortUnknownCode (int segment, int opcode); - - void abortUnknownSegment (Type_Code code); - void begin(); void end(); + template + void installSegment(TSeg& seg, int code, TOp&& op) + { + assert(seg.find(code) == seg.end()); + seg.emplace(code, std::move(op)); + } + public: Interpreter(); - ~Interpreter(); - - void installSegment0 (int code, Opcode1 *opcode); - ///< ownership of \a opcode is transferred to *this. - - void installSegment2 (int code, Opcode1 *opcode); - ///< ownership of \a opcode is transferred to *this. - - void installSegment3 (int code, Opcode1 *opcode); - ///< ownership of \a opcode is transferred to *this. - - void installSegment5 (int code, Opcode0 *opcode); - ///< ownership of \a opcode is transferred to *this. + template + void installSegment0(int code, TArgs&& ...args) + { + installSegment(mSegment0, code, std::make_unique(std::forward(args)...)); + } + + template + void installSegment2(int code, TArgs&& ...args) + { + installSegment(mSegment2, code, std::make_unique(std::forward(args)...)); + } + + template + void installSegment3(int code, TArgs&& ...args) + { + installSegment(mSegment3, code, std::make_unique(std::forward(args)...)); + } + + template + void installSegment5(int code, TArgs&& ...args) + { + installSegment(mSegment5, code, std::make_unique(std::forward(args)...)); + } void run (const Type_Code *code, int codeSize, Context& context); }; diff --git a/components/interpreter/localopcodes.hpp b/components/interpreter/localopcodes.hpp index 0227327b3a..6fe779493f 100644 --- a/components/interpreter/localopcodes.hpp +++ b/components/interpreter/localopcodes.hpp @@ -122,7 +122,7 @@ namespace Interpreter Type_Integer data = runtime[0].mInteger; int index = runtime[1].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); runtime.getContext().setGlobalShort (name, data); @@ -140,7 +140,7 @@ namespace Interpreter Type_Integer data = runtime[0].mInteger; int index = runtime[1].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); runtime.getContext().setGlobalLong (name, data); @@ -158,7 +158,7 @@ namespace Interpreter Type_Float data = runtime[0].mFloat; int index = runtime[1].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); runtime.getContext().setGlobalFloat (name, data); @@ -174,7 +174,7 @@ namespace Interpreter void execute (Runtime& runtime) override { int index = runtime[0].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); Type_Integer value = runtime.getContext().getGlobalShort (name); runtime[0].mInteger = value; } @@ -187,7 +187,7 @@ namespace Interpreter void execute (Runtime& runtime) override { int index = runtime[0].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); Type_Integer value = runtime.getContext().getGlobalLong (name); runtime[0].mInteger = value; } @@ -200,29 +200,26 @@ namespace Interpreter void execute (Runtime& runtime) override { int index = runtime[0].mInteger; - std::string name = runtime.getStringLiteral (index); + std::string_view name = runtime.getStringLiteral (index); Type_Float value = runtime.getContext().getGlobalFloat (name); runtime[0].mFloat = value; } }; + template class OpStoreMemberShort : public Opcode0 { - bool mGlobal; - public: - OpStoreMemberShort (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Integer data = runtime[0].mInteger; Type_Integer index = runtime[1].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[2].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); - runtime.getContext().setMemberShort (id, variable, data, mGlobal); + runtime.getContext().setMemberShort (id, variable, data, TGlobal); runtime.pop(); runtime.pop(); @@ -230,23 +227,20 @@ namespace Interpreter } }; + template class OpStoreMemberLong : public Opcode0 { - bool mGlobal; - public: - OpStoreMemberLong (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Integer data = runtime[0].mInteger; Type_Integer index = runtime[1].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[2].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); - runtime.getContext().setMemberLong (id, variable, data, mGlobal); + runtime.getContext().setMemberLong (id, variable, data, TGlobal); runtime.pop(); runtime.pop(); @@ -254,23 +248,20 @@ namespace Interpreter } }; + template class OpStoreMemberFloat : public Opcode0 { - bool mGlobal; - public: - OpStoreMemberFloat (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Float data = runtime[0].mFloat; Type_Integer index = runtime[1].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[2].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); - runtime.getContext().setMemberFloat (id, variable, data, mGlobal); + runtime.getContext().setMemberFloat (id, variable, data, TGlobal); runtime.pop(); runtime.pop(); @@ -278,65 +269,56 @@ namespace Interpreter } }; + template class OpFetchMemberShort : public Opcode0 { - bool mGlobal; - public: - OpFetchMemberShort (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Integer index = runtime[0].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[1].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); runtime.pop(); - int value = runtime.getContext().getMemberShort (id, variable, mGlobal); + int value = runtime.getContext().getMemberShort (id, variable, TGlobal); runtime[0].mInteger = value; } }; + template class OpFetchMemberLong : public Opcode0 { - bool mGlobal; - public: - OpFetchMemberLong (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Integer index = runtime[0].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[1].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); runtime.pop(); - int value = runtime.getContext().getMemberLong (id, variable, mGlobal); + int value = runtime.getContext().getMemberLong (id, variable, TGlobal); runtime[0].mInteger = value; } }; + template class OpFetchMemberFloat : public Opcode0 { - bool mGlobal; - public: - OpFetchMemberFloat (bool global) : mGlobal (global) {} - void execute (Runtime& runtime) override { Type_Integer index = runtime[0].mInteger; - std::string id = runtime.getStringLiteral (index); + std::string_view id = runtime.getStringLiteral (index); index = runtime[1].mInteger; - std::string variable = runtime.getStringLiteral (index); + std::string_view variable = runtime.getStringLiteral (index); runtime.pop(); - float value = runtime.getContext().getMemberFloat (id, variable, mGlobal); + float value = runtime.getContext().getMemberFloat (id, variable, TGlobal); runtime[0].mFloat = value; } }; diff --git a/components/interpreter/mathopcodes.hpp b/components/interpreter/mathopcodes.hpp index 42cb486b9c..bf580c6c2e 100644 --- a/components/interpreter/mathopcodes.hpp +++ b/components/interpreter/mathopcodes.hpp @@ -74,24 +74,6 @@ namespace Interpreter } }; - class OpSquareRoot : public Opcode0 - { - public: - - void execute (Runtime& runtime) override - { - Type_Float value = runtime[0].mFloat; - - if (value<0) - throw std::runtime_error ( - "square root of negative number (we aren't that imaginary)"); - - value = std::sqrt (value); - - runtime[0].mFloat = value; - } - }; - template class OpCompare : public Opcode0 { @@ -105,7 +87,7 @@ namespace Interpreter runtime[0].mInteger = result; } - }; + }; } #endif diff --git a/components/interpreter/miscopcodes.hpp b/components/interpreter/miscopcodes.hpp index 5a9311346a..32b4df2e47 100644 --- a/components/interpreter/miscopcodes.hpp +++ b/components/interpreter/miscopcodes.hpp @@ -97,7 +97,7 @@ namespace Interpreter { } - void process(const std::string& message) override + void process(std::string_view message) override { mFormattedMessage.clear(); MessageFormatParser::process(message); @@ -109,7 +109,7 @@ namespace Interpreter } }; - inline std::string formatMessage (const std::string& message, Runtime& runtime) + inline std::string formatMessage (std::string_view message, Runtime& runtime) { RuntimeMessageFormatter formatter(runtime); formatter.process(message); @@ -128,7 +128,7 @@ namespace Interpreter // message int index = runtime[0].mInteger; runtime.pop(); - std::string message = runtime.getStringLiteral (index); + std::string_view message = runtime.getStringLiteral (index); // buttons std::vector buttons; @@ -137,7 +137,7 @@ namespace Interpreter { index = runtime[0].mInteger; runtime.pop(); - buttons.push_back (runtime.getStringLiteral (index)); + buttons.emplace_back(runtime.getStringLiteral(index)); } std::reverse (buttons.begin(), buttons.end()); @@ -158,7 +158,7 @@ namespace Interpreter // message int index = runtime[0].mInteger; runtime.pop(); - std::string message = runtime.getStringLiteral (index); + std::string_view message = runtime.getStringLiteral (index); // handle additional parameters std::string formattedMessage = formatMessage (message, runtime); diff --git a/components/interpreter/runtime.cpp b/components/interpreter/runtime.cpp index a90bda94bc..d5bedfba57 100644 --- a/components/interpreter/runtime.cpp +++ b/components/interpreter/runtime.cpp @@ -6,7 +6,7 @@ namespace Interpreter { - Runtime::Runtime() : mContext (0), mCode (0), mCodeSize(0), mPC (0) {} + Runtime::Runtime() : mContext (nullptr), mCode (nullptr), mCodeSize(0), mPC (0) {} int Runtime::getPC() const { @@ -33,7 +33,7 @@ namespace Interpreter return *reinterpret_cast (&literalBlock[index]); } - std::string Runtime::getStringLiteral (int index) const + std::string_view Runtime::getStringLiteral(int index) const { if (index < 0 || static_cast (mCode[3]) <= 0) throw std::out_of_range("out of range"); @@ -41,12 +41,12 @@ namespace Interpreter const char *literalBlock = reinterpret_cast (mCode + 4 + mCode[0] + mCode[1] + mCode[2]); - int offset = 0; + size_t offset = 0; for (; index; --index) { - offset += std::strlen (literalBlock+offset) + 1; - if (offset / 4 >= static_cast (mCode[3])) + offset += std::strlen(literalBlock + offset) + 1; + if (offset / 4 >= mCode[3]) throw std::out_of_range("out of range"); } @@ -65,8 +65,8 @@ namespace Interpreter void Runtime::clear() { - mContext = 0; - mCode = 0; + mContext = nullptr; + mCode = nullptr; mCodeSize = 0; mStack.clear(); } @@ -100,7 +100,7 @@ namespace Interpreter if (mStack.empty()) throw std::runtime_error ("stack underflow"); - mStack.resize (mStack.size()-1); + mStack.pop_back(); } Data& Runtime::operator[] (int Index) diff --git a/components/interpreter/runtime.hpp b/components/interpreter/runtime.hpp index 2811ab0f0c..7f48ca8d3e 100644 --- a/components/interpreter/runtime.hpp +++ b/components/interpreter/runtime.hpp @@ -2,7 +2,7 @@ #define INTERPRETER_RUNTIME_H_INCLUDED #include -#include +#include #include "types.hpp" @@ -31,7 +31,7 @@ namespace Interpreter float getFloatLiteral (int index) const; - std::string getStringLiteral (int index) const; + std::string_view getStringLiteral(int index) const; void configure (const Type_Code *code, int codeSize, Context& context); ///< \a context and \a code must exist as least until either configure, clear or diff --git a/components/l10n/messagebundles.cpp b/components/l10n/messagebundles.cpp new file mode 100644 index 0000000000..6934dcaad8 --- /dev/null +++ b/components/l10n/messagebundles.cpp @@ -0,0 +1,168 @@ +#include "messagebundles.hpp" + +#include +#include +#include +#include + +#include + +namespace l10n +{ + MessageBundles::MessageBundles(const std::vector &preferredLocales, icu::Locale &fallbackLocale) : + mFallbackLocale(fallbackLocale) + { + setPreferredLocales(preferredLocales); + } + + void MessageBundles::setPreferredLocales(const std::vector &preferredLocales) + { + mPreferredLocales.clear(); + mPreferredLocaleStrings.clear(); + for (const icu::Locale &loc: preferredLocales) + { + mPreferredLocales.push_back(loc); + mPreferredLocaleStrings.emplace_back(loc.getName()); + // Try without variant or country if they are specified, starting with the most specific + if (strcmp(loc.getVariant(), "") != 0) + { + icu::Locale withoutVariant(loc.getLanguage(), loc.getCountry()); + mPreferredLocales.push_back(withoutVariant); + mPreferredLocaleStrings.emplace_back(withoutVariant.getName()); + } + if (strcmp(loc.getCountry(), "") != 0) + { + icu::Locale withoutCountry(loc.getLanguage()); + mPreferredLocales.push_back(withoutCountry); + mPreferredLocaleStrings.emplace_back(withoutCountry.getName()); + } + } + } + + std::string getErrorText(const UParseError &parseError) + { + icu::UnicodeString preContext(parseError.preContext), postContext(parseError.postContext); + std::string parseErrorString; + preContext.toUTF8String(parseErrorString); + postContext.toUTF8String(parseErrorString); + return parseErrorString; + } + + static bool checkSuccess(const icu::ErrorCode &status, const std::string &message, const UParseError parseError = UParseError()) + { + if (status.isFailure()) + { + std::string errorText = getErrorText(parseError); + if (!errorText.empty()) + { + Log(Debug::Error) << message << ": " << status.errorName() << " in \"" << errorText << "\""; + } + else + { + Log(Debug::Error) << message << ": " << status.errorName(); + } + } + return status.isSuccess(); + } + + void MessageBundles::load(std::istream &input, const icu::Locale& lang, const std::string &path) + { + try + { + YAML::Node data = YAML::Load(input); + std::string localeName = lang.getName(); + for (const auto& it: data) + { + const auto key = it.first.as(); + const auto value = it.second.as(); + icu::UnicodeString pattern = icu::UnicodeString::fromUTF8(icu::StringPiece(value.data(), value.size())); + icu::ErrorCode status; + UParseError parseError; + icu::MessageFormat message(pattern, lang, parseError, status); + if (checkSuccess(status, std::string("Failed to create message ") + + key + " for locale " + lang.getName(), parseError)) + { + mBundles[localeName].insert(std::make_pair(key, message)); + } + } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Can not load " << path << ": " << e.what(); + } + } + + const icu::MessageFormat * MessageBundles::findMessage(std::string_view key, const std::string &localeName) const + { + auto iter = mBundles.find(localeName); + if (iter != mBundles.end()) + { + auto message = iter->second.find(key.data()); + if (message != iter->second.end()) + { + return &(message->second); + } + } + return nullptr; + } + + std::string MessageBundles::formatMessage(std::string_view key, const std::map &args) const + { + std::vector argNames; + std::vector argValues; + for (auto& [k, v] : args) + { + argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(k.data(), k.size()))); + argValues.push_back(v); + } + return formatMessage(key, argNames, argValues); + } + + std::string MessageBundles::formatMessage(std::string_view key, const std::vector &argNames, const std::vector &args) const + { + icu::UnicodeString result; + std::string resultString; + icu::ErrorCode success; + + const icu::MessageFormat *message = nullptr; + for (auto &loc: mPreferredLocaleStrings) + { + message = findMessage(key, loc); + if (message) + break; + } + // If no requested locales included the message, try the fallback locale + if (!message) + message = findMessage(key, mFallbackLocale.getName()); + + if (message) + { + if (!args.empty() && !argNames.empty()) + message->format(&argNames[0], &args[0], args.size(), result, success); + else + message->format(nullptr, nullptr, args.size(), result, success); + checkSuccess(success, std::string("Failed to format message ") + key.data()); + result.toUTF8String(resultString); + return resultString; + } + icu::Locale defaultLocale(nullptr); + if (!mPreferredLocales.empty()) + { + defaultLocale = mPreferredLocales[0]; + } + UParseError parseError; + icu::MessageFormat defaultMessage(icu::UnicodeString::fromUTF8(icu::StringPiece(key.data(), key.size())), + defaultLocale, parseError, success); + if (!checkSuccess(success, std::string("Failed to create message ") + key.data(), parseError)) + // If we can't parse the key as a pattern, just return the key + return std::string(key); + + if (!args.empty() && !argNames.empty()) + defaultMessage.format(&argNames[0], &args[0], args.size(), result, success); + else + defaultMessage.format(nullptr, nullptr, args.size(), result, success); + checkSuccess(success, std::string("Failed to format message ") + key.data()); + result.toUTF8String(resultString); + return resultString; + } +} diff --git a/components/l10n/messagebundles.hpp b/components/l10n/messagebundles.hpp new file mode 100644 index 0000000000..1a4636c95a --- /dev/null +++ b/components/l10n/messagebundles.hpp @@ -0,0 +1,55 @@ +#ifndef COMPONENTS_L10N_MESSAGEBUNDLES_H +#define COMPONENTS_L10N_MESSAGEBUNDLES_H + +#include +#include +#include +#include + +#include +#include + +namespace l10n +{ + /** + * @brief A collection of Message Bundles + * + * Class handling localised message storage and lookup, including fallback locales when messages are missing. + * + * If no fallback locale is provided (or a message fails to be found), the key will be formatted instead, + * or returned verbatim if formatting fails. + * + */ + class MessageBundles + { + public: + /* @brief Constructs an empty MessageBundles + * + * @param preferredLocales user-requested locales, in order of priority + * Each locale will be checked when looking up messages, in case some resource files are incomplete. + * For each locale which contains a country code or a variant, the locales obtained by removing first + * the variant, then the country code, will also be checked before moving on to the next locale in the list. + * @param fallbackLocale the fallback locale which should be used if messages cannot be found for the user + * preferred locales + */ + MessageBundles(const std::vector &preferredLocales, icu::Locale &fallbackLocale); + std::string formatMessage(std::string_view key, const std::map &args) const; + std::string formatMessage(std::string_view key, const std::vector &argNames, const std::vector &args) const; + void setPreferredLocales(const std::vector &preferredLocales); + const std::vector & getPreferredLocales() const { return mPreferredLocales; } + void load(std::istream &input, const icu::Locale &lang, const std::string &path); + bool isLoaded(const icu::Locale& loc) const { return mBundles.find(loc.getName()) != mBundles.end(); } + const icu::Locale & getFallbackLocale() const { return mFallbackLocale; } + + private: + // icu::Locale isn't hashable (or comparable), so we use the string form instead, which is canonicalized + std::unordered_map> mBundles; + const icu::Locale mFallbackLocale; + std::vector mPreferredLocaleStrings; + std::vector mPreferredLocales; + const icu::MessageFormat * findMessage(std::string_view key, const std::string &localeName) const; + }; + +} + +#endif // COMPONENTS_L10N_MESSAGEBUNDLES_H diff --git a/components/loadinglistener/loadinglistener.hpp b/components/loadinglistener/loadinglistener.hpp index 93467c1414..14a1b96f9a 100644 --- a/components/loadinglistener/loadinglistener.hpp +++ b/components/loadinglistener/loadinglistener.hpp @@ -14,7 +14,7 @@ namespace Loading /// @note "non-important" labels may not show on screen if the loading process went so fast /// that the implementation decided not to show a loading screen at all. "important" labels /// will show in a separate message-box if the loading screen was not shown. - virtual void setLabel (const std::string& label, bool important=false, bool center=false) {} + virtual void setLabel (const std::string& label, bool important=false) {} /// Start a loading sequence. Must call loadingOff() when done. /// @note To get the loading screen to actually update, you must call setProgress / increaseProgress periodically. diff --git a/components/loadinglistener/reporter.cpp b/components/loadinglistener/reporter.cpp new file mode 100644 index 0000000000..0ad04fded1 --- /dev/null +++ b/components/loadinglistener/reporter.cpp @@ -0,0 +1,41 @@ +#include "reporter.hpp" +#include "loadinglistener.hpp" + +#include +#include +#include + +namespace Loading +{ + void Reporter::addTotal(std::size_t value) + { + const std::lock_guard lock(mMutex); + mTotal += value; + mUpdated.notify_all(); + } + + void Reporter::addProgress(std::size_t value) + { + const std::lock_guard lock(mMutex); + mProgress += value; + mUpdated.notify_all(); + } + + void Reporter::complete() + { + const std::lock_guard lock(mMutex); + mDone = true; + mUpdated.notify_all(); + } + + void Reporter::wait(Listener& listener) const + { + std::unique_lock lock(mMutex); + while (!mDone) + { + listener.setProgressRange(mTotal); + listener.setProgress(mProgress); + mUpdated.wait(lock); + } + } +} diff --git a/components/loadinglistener/reporter.hpp b/components/loadinglistener/reporter.hpp new file mode 100644 index 0000000000..b59c519082 --- /dev/null +++ b/components/loadinglistener/reporter.hpp @@ -0,0 +1,32 @@ +#ifndef COMPONENTS_LOADINGLISTENER_REPORTER_H +#define COMPONENTS_LOADINGLISTENER_REPORTER_H + +#include +#include +#include + +namespace Loading +{ + class Listener; + + class Reporter + { + public: + void addTotal(std::size_t value); + + void addProgress(std::size_t value); + + void complete(); + + void wait(Listener& listener) const; + + private: + std::size_t mProgress = 0; + std::size_t mTotal = 0; + bool mDone = false; + mutable std::mutex mMutex; + mutable std::condition_variable mUpdated; + }; +} + +#endif diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp new file mode 100644 index 0000000000..b4889c44d1 --- /dev/null +++ b/components/lua/configuration.cpp @@ -0,0 +1,255 @@ +#include "configuration.hpp" + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + + namespace + { + const std::map> flagsByName{ + {"GLOBAL", ESM::LuaScriptCfg::sGlobal}, + {"CUSTOM", ESM::LuaScriptCfg::sCustom}, + {"PLAYER", ESM::LuaScriptCfg::sPlayer}, + }; + + const std::map> typeTagsByName{ + {"ACTIVATOR", ESM::REC_ACTI}, + {"ARMOR", ESM::REC_ARMO}, + {"BOOK", ESM::REC_BOOK}, + {"CLOTHING", ESM::REC_CLOT}, + {"CONTAINER", ESM::REC_CONT}, + {"CREATURE", ESM::REC_CREA}, + {"DOOR", ESM::REC_DOOR}, + {"INGREDIENT", ESM::REC_INGR}, + {"LIGHT", ESM::REC_LIGH}, + {"MISC_ITEM", ESM::REC_MISC}, + {"NPC", ESM::REC_NPC_}, + {"POTION", ESM::REC_ALCH}, + {"WEAPON", ESM::REC_WEAP}, + {"APPARATUS", ESM::REC_APPA}, + {"LOCKPICK", ESM::REC_LOCK}, + {"PROBE", ESM::REC_PROB}, + {"REPAIR", ESM::REC_REPA}, + }; + + bool isSpace(char c) + { + return std::isspace(static_cast(c)); + } + } + + void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg) + { + mScripts.clear(); + mPathToIndex.clear(); + + // Find duplicates; only the last occurrence will be used (unless `sMerge` flag is used). + // Search for duplicates is case insensitive. + std::vector skip(cfg.mScripts.size(), false); + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + const ESM::LuaScriptCfg& script = cfg.mScripts[i]; + bool global = script.mFlags & ESM::LuaScriptCfg::sGlobal; + if (global && (script.mFlags & ~ESM::LuaScriptCfg::sMerge) != ESM::LuaScriptCfg::sGlobal) + throw std::runtime_error(std::string("Global script can not have local flags: ") + script.mScriptPath); + if (global && (!script.mTypes.empty() || !script.mRecords.empty() || !script.mRefs.empty())) + throw std::runtime_error(std::string( + "Global script can not have per-type and per-object configuration") + script.mScriptPath); + auto [it, inserted] = mPathToIndex.emplace( + Misc::StringUtils::lowerCase(script.mScriptPath), i); + if (inserted) + continue; + ESM::LuaScriptCfg& oldScript = cfg.mScripts[it->second]; + if (global != bool(oldScript.mFlags & ESM::LuaScriptCfg::sGlobal)) + throw std::runtime_error(std::string("Flags mismatch for ") + script.mScriptPath); + if (script.mFlags & ESM::LuaScriptCfg::sMerge) + { + oldScript.mFlags |= (script.mFlags & ~ESM::LuaScriptCfg::sMerge); + if (!script.mInitializationData.empty()) + oldScript.mInitializationData = script.mInitializationData; + oldScript.mTypes.insert(oldScript.mTypes.end(), script.mTypes.begin(), script.mTypes.end()); + oldScript.mRecords.insert(oldScript.mRecords.end(), script.mRecords.begin(), script.mRecords.end()); + oldScript.mRefs.insert(oldScript.mRefs.end(), script.mRefs.begin(), script.mRefs.end()); + skip[i] = true; + } + else + skip[it->second] = true; + } + + // Filter duplicates + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + if (!skip[i]) + mScripts.push_back(std::move(cfg.mScripts[i])); + } + + // Initialize mappings + mPathToIndex.clear(); + for (int i = 0; i < static_cast(mScripts.size()); ++i) + { + const ESM::LuaScriptCfg& s = mScripts[i]; + mPathToIndex[s.mScriptPath] = i; // Stored paths are case sensitive. + for (uint32_t t : s.mTypes) + mScriptsPerType[t].push_back(i); + for (const ESM::LuaScriptCfg::PerRecordCfg& r : s.mRecords) + { + std::string_view data = r.mInitializationData.empty() ? s.mInitializationData : r.mInitializationData; + mScriptsPerRecordId[r.mRecordId].push_back(DetailedConf{i, r.mAttach, data}); + } + for (const ESM::LuaScriptCfg::PerRefCfg& r : s.mRefs) + { + std::string_view data = r.mInitializationData.empty() ? s.mInitializationData : r.mInitializationData; + mScriptsPerRefNum[ESM::RefNum{r.mRefnumIndex, r.mRefnumContentFile}].push_back(DetailedConf{i, r.mAttach, data}); + } + } + } + + std::optional ScriptsConfiguration::findId(std::string_view path) const + { + auto it = mPathToIndex.find(path); + if (it != mPathToIndex.end()) + return it->second; + else + return std::nullopt; + } + + ScriptIdsWithInitializationData ScriptsConfiguration::getConfByFlag(ESM::LuaScriptCfg::Flags flag) const + { + ScriptIdsWithInitializationData res; + for (size_t id = 0; id < mScripts.size(); ++id) + { + const ESM::LuaScriptCfg& script = mScripts[id]; + if (script.mFlags & flag) + res[id] = script.mInitializationData; + } + return res; + } + + ScriptIdsWithInitializationData ScriptsConfiguration::getLocalConf( + uint32_t type, std::string_view recordId, ESM::RefNum refnum) const + { + ScriptIdsWithInitializationData res; + auto typeIt = mScriptsPerType.find(type); + if (typeIt != mScriptsPerType.end()) + for (int scriptId : typeIt->second) + res[scriptId] = mScripts[scriptId].mInitializationData; + auto recordIt = mScriptsPerRecordId.find(recordId); + if (recordIt != mScriptsPerRecordId.end()) + { + for (const DetailedConf& d : recordIt->second) + { + if (d.mAttach) + res[d.mScriptId] = d.mInitializationData; + else + res.erase(d.mScriptId); + } + } + if (!refnum.hasContentFile()) + return res; + auto refIt = mScriptsPerRefNum.find(refnum); + if (refIt == mScriptsPerRefNum.end()) + return res; + for (const DetailedConf& d : refIt->second) + { + if (d.mAttach) + res[d.mScriptId] = d.mInitializationData; + else + res.erase(d.mScriptId); + } + return res; + } + + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data) + { + while (!data.empty()) + { + // Get next line + std::string_view line = data.substr(0, data.find('\n')); + data = data.substr(std::min(line.size() + 1, data.size())); + if (!line.empty() && line.back() == '\r') + line = line.substr(0, line.size() - 1); + + while (!line.empty() && isSpace(line[0])) + line = line.substr(1); + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + while (!line.empty() && isSpace(line.back())) + line = line.substr(0, line.size() - 1); + + if (!Misc::StringUtils::ciEndsWith(line, ".lua")) + throw std::runtime_error(Misc::StringUtils::format( + "Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300)))); + + // Split tags and script path + size_t semicolonPos = line.find(':'); + if (semicolonPos == std::string::npos) + throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line))); + std::string_view tagsStr = line.substr(0, semicolonPos); + std::string_view scriptPath = line.substr(semicolonPos + 1); + while (isSpace(scriptPath[0])) + scriptPath = scriptPath.substr(1); + + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = std::string(scriptPath); + script.mFlags = 0; + + // Parse tags + size_t tagsPos = 0; + while (true) + { + while (tagsPos < tagsStr.size() && (isSpace(tagsStr[tagsPos]) || tagsStr[tagsPos] == ',')) + tagsPos++; + size_t startPos = tagsPos; + while (tagsPos < tagsStr.size() && !isSpace(tagsStr[tagsPos]) && tagsStr[tagsPos] != ',') + tagsPos++; + if (startPos == tagsPos) + break; + std::string_view tagName = tagsStr.substr(startPos, tagsPos - startPos); + auto it = flagsByName.find(tagName); + auto typesIt = typeTagsByName.find(tagName); + if (it != flagsByName.end()) + script.mFlags |= it->second; + else if (typesIt != typeTagsByName.end()) + script.mTypes.push_back(typesIt->second); + else + throw std::runtime_error(Misc::StringUtils::format("Unknown tag '%s' in: %s", + std::string(tagName), std::string(line))); + } + } + } + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script) + { + std::stringstream ss; + if (script.mFlags & ESM::LuaScriptCfg::sMerge) + ss << "+ "; + for (const auto& [flagName, flag] : flagsByName) + { + if (script.mFlags & flag) + ss << flagName << " "; + } + for (uint32_t type : script.mTypes) + { + for (const auto& [tagName, t] : typeTagsByName) + { + if (type == t) + ss << tagName << " "; + } + } + ss << ": " << script.mScriptPath; + if (!script.mInitializationData.empty()) + ss << " ; data " << script.mInitializationData.size() << " bytes"; + if (!script.mRecords.empty()) + ss << " ; " << script.mRecords.size() << " records"; + if (!script.mRefs.empty()) + ss << " ; " << script.mRefs.size() << " objects"; + return ss.str(); + } + +} diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp new file mode 100644 index 0000000000..87159068be --- /dev/null +++ b/components/lua/configuration.hpp @@ -0,0 +1,53 @@ +#ifndef COMPONENTS_LUA_CONFIGURATION_H +#define COMPONENTS_LUA_CONFIGURATION_H + +#include +#include + +#include "components/esm/luascripts.hpp" +#include "components/esm3/cellref.hpp" + +namespace LuaUtil +{ + using ScriptIdsWithInitializationData = std::map; + + class ScriptsConfiguration + { + public: + void init(ESM::LuaScriptsCfg); + + size_t size() const { return mScripts.size(); } + const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; } + + std::optional findId(std::string_view path) const; + bool isCustomScript(int id) const { return mScripts[id].mFlags & ESM::LuaScriptCfg::sCustom; } + + ScriptIdsWithInitializationData getGlobalConf() const { return getConfByFlag(ESM::LuaScriptCfg::sGlobal); } + ScriptIdsWithInitializationData getPlayerConf() const { return getConfByFlag(ESM::LuaScriptCfg::sPlayer); } + ScriptIdsWithInitializationData getLocalConf(uint32_t type, std::string_view recordId, ESM::RefNum refnum) const; + + private: + ScriptIdsWithInitializationData getConfByFlag(ESM::LuaScriptCfg::Flags flag) const; + + std::vector mScripts; + std::map> mPathToIndex; + + struct DetailedConf + { + int mScriptId; + bool mAttach; + std::string_view mInitializationData; + }; + std::map> mScriptsPerType; + std::map, std::less<>> mScriptsPerRecordId; + std::map> mScriptsPerRefNum; + }; + + // Parse ESM::LuaScriptsCfg from text and add to `cfg`. + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data); + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script); + +} + +#endif // COMPONENTS_LUA_CONFIGURATION_H diff --git a/components/lua/l10n.cpp b/components/lua/l10n.cpp new file mode 100644 index 0000000000..996b859157 --- /dev/null +++ b/components/lua/l10n.cpp @@ -0,0 +1,154 @@ +#include "l10n.hpp" + +#include + +#include +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace LuaUtil +{ + void L10nManager::init() + { + sol::usertype ctx = mLua->sol().new_usertype("L10nContext"); + ctx[sol::meta_function::call] = &Context::translate; + } + + std::string L10nManager::translate(const std::string& contextName, const std::string& key) + { + Context& ctx = getContext(contextName).as(); + return ctx.translate(key, sol::nil); + } + + void L10nManager::setPreferredLocales(const std::vector& langs) + { + mPreferredLocales.clear(); + for (const auto &lang : langs) + mPreferredLocales.push_back(icu::Locale(lang.c_str())); + { + Log msg(Debug::Info); + msg << "Preferred locales:"; + for (const icu::Locale& l : mPreferredLocales) + msg << " " << l.getName(); + } + for (auto& [_, context] : mContexts) + context.updateLang(this); + } + + void L10nManager::Context::readLangData(L10nManager* manager, const icu::Locale& lang) + { + std::string path = "l10n/"; + path.append(mName); + path.append("/"); + path.append(lang.getName()); + path.append(".yaml"); + if (!manager->mVFS->exists(path)) + return; + + mMessageBundles->load(*manager->mVFS->get(path), lang, path); + } + + std::pair, std::vector> getICUArgs(std::string_view messageId, const sol::table &table) + { + std::vector args; + std::vector argNames; + for (auto elem : table) + for (auto& [key, value] : table) + { + // Argument values + if (value.is()) + args.push_back(icu::Formattable(value.as().c_str())); + // Note: While we pass all numbers as doubles, they still seem to be handled appropriately. + // Numbers can be forced to be integers using the argType number and argStyle integer + // E.g. {var, number, integer} + else if (value.is()) + args.push_back(icu::Formattable(value.as())); + else + { + Log(Debug::Error) << "Unrecognized argument type for key \"" << key.as() + << "\" when formatting message \"" << messageId << "\""; + } + + // Argument names + const auto str = key.as(); + argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))); + } + return std::make_pair(args, argNames); + } + + std::string L10nManager::Context::translate(std::string_view key, const sol::object& data) + { + std::vector args; + std::vector argNames; + + if (data.is()) { + sol::table dataTable = data.as(); + auto argData = getICUArgs(key, dataTable); + args = argData.first; + argNames = argData.second; + } + + return mMessageBundles->formatMessage(key, argNames, args); + } + + void L10nManager::Context::updateLang(L10nManager* manager) + { + icu::Locale fallbackLocale = mMessageBundles->getFallbackLocale(); + mMessageBundles->setPreferredLocales(manager->mPreferredLocales); + int localeCount = 0; + bool fallbackLocaleInPreferred = false; + for (const icu::Locale& loc: mMessageBundles->getPreferredLocales()) + { + if (!mMessageBundles->isLoaded(loc)) + readLangData(manager, loc); + if (mMessageBundles->isLoaded(loc)) + { + localeCount++; + Log(Debug::Verbose) << "Language file \"l10n/" << mName << "/" << loc.getName() << ".yaml\" is enabled"; + if (loc == fallbackLocale) + fallbackLocaleInPreferred = true; + } + } + if (!mMessageBundles->isLoaded(fallbackLocale)) + readLangData(manager, fallbackLocale); + if (mMessageBundles->isLoaded(fallbackLocale) && !fallbackLocaleInPreferred) + Log(Debug::Verbose) << "Fallback language file \"l10n/" << mName << "/" << fallbackLocale.getName() << ".yaml\" is enabled"; + + if (localeCount == 0) + { + Log(Debug::Warning) << "No language files for the preferred languages found in \"l10n/" << mName << "\""; + } + } + + sol::object L10nManager::getContext(const std::string& contextName, const std::string& fallbackLocaleName) + { + auto it = mContexts.find(contextName); + if (it != mContexts.end()) + return sol::make_object(mLua->sol(), it->second); + auto allowedChar = [](char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_'; + }; + bool valid = !contextName.empty(); + for (char c : contextName) + valid = valid && allowedChar(c); + if (!valid) + throw std::runtime_error(std::string("Invalid l10n context name: ") + contextName); + icu::Locale fallbackLocale(fallbackLocaleName.c_str()); + Context ctx{contextName, std::make_shared(mPreferredLocales, fallbackLocale)}; + { + Log msg(Debug::Verbose); + msg << "Fallback locale: " << fallbackLocale.getName(); + } + ctx.updateLang(this); + mContexts.emplace(contextName, ctx); + return sol::make_object(mLua->sol(), ctx); + } + +} diff --git a/components/lua/l10n.hpp b/components/lua/l10n.hpp new file mode 100644 index 0000000000..51c8ecad9c --- /dev/null +++ b/components/lua/l10n.hpp @@ -0,0 +1,49 @@ +#ifndef COMPONENTS_LUA_I18N_H +#define COMPONENTS_LUA_I18N_H + +#include "luastate.hpp" + +#include + +namespace VFS +{ + class Manager; +} + +namespace LuaUtil +{ + + class L10nManager + { + public: + L10nManager(const VFS::Manager* vfs, LuaState* lua) : mVFS(vfs), mLua(lua) {} + void init(); + void clear() { mContexts.clear(); } + + void setPreferredLocales(const std::vector& locales); + const std::vector& getPreferredLocales() const { return mPreferredLocales; } + + sol::object getContext(const std::string& contextName, const std::string& fallbackLocale = "en"); + std::string translate(const std::string& contextName, const std::string& key); + + private: + struct Context + { + const std::string mName; + // Must be a shared pointer so that sol::make_object copies the pointer, not the data structure. + std::shared_ptr mMessageBundles; + + void updateLang(L10nManager* manager); + void readLangData(L10nManager* manager, const icu::Locale& lang); + std::string translate(std::string_view key, const sol::object& data); + }; + + const VFS::Manager* mVFS; + LuaState* mLua; + std::vector mPreferredLocales; + std::map mContexts; + }; + +} + +#endif // COMPONENTS_LUA_I18N_H diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp new file mode 100644 index 0000000000..dbdfde5100 --- /dev/null +++ b/components/lua/luastate.cpp @@ -0,0 +1,299 @@ +#include "luastate.hpp" + +#ifndef NO_LUAJIT +#include +#endif // NO_LUAJIT + +#include + +#include +#include + +namespace LuaUtil +{ + + static std::string packageNameToVfsPath(std::string_view packageName, const VFS::Manager* vfs) + { + std::string path(packageName); + std::replace(path.begin(), path.end(), '.', '/'); + std::string pathWithInit = path + "/init.lua"; + path.append(".lua"); + if (vfs->exists(path)) + return path; + else if (vfs->exists(pathWithInit)) + return pathWithInit; + else + throw std::runtime_error("module not found: " + std::string(packageName)); + } + + static std::string packageNameToPath(std::string_view packageName, const std::vector& searchDirs) + { + std::string path(packageName); + std::replace(path.begin(), path.end(), '.', '/'); + std::string pathWithInit = path + "/init.lua"; + path.append(".lua"); + for (const std::string& dir : searchDirs) + { + std::filesystem::path base(dir); + std::filesystem::path p1 = base / path; + if (std::filesystem::exists(p1)) + return p1.string(); + std::filesystem::path p2 = base / pathWithInit; + if (std::filesystem::exists(p2)) + return p2.string(); + } + throw std::runtime_error("module not found: " + std::string(packageName)); + } + + static const std::string safeFunctions[] = { + "assert", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber", "tostring", + "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable"}; + static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; + + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) + { + mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, + sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug); + + mLua["math"]["randomseed"](static_cast(std::time(nullptr))); + mLua["math"]["randomseed"] = []{}; + + mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; }; + + // Some fixes for compatibility between different Lua versions + if (mLua["unpack"] == sol::nil) + mLua["unpack"] = mLua["table"]["unpack"]; + else if (mLua["table"]["unpack"] == sol::nil) + mLua["table"]["unpack"] = mLua["unpack"]; + if (LUA_VERSION_NUM <= 501) + { + mLua.script(R"( + local _pairs = pairs + local _ipairs = ipairs + pairs = function(v) return (rawget(getmetatable(v) or {}, '__pairs') or _pairs)(v) end + ipairs = function(v) return (rawget(getmetatable(v) or {}, '__ipairs') or _ipairs)(v) end + )"); + } + + mLua.script(R"( + local printToLog = function(...) + local strs = {} + for i = 1, select('#', ...) do + strs[i] = tostring(select(i, ...)) + end + return writeToLog(table.concat(strs, '\t')) + end + printGen = function(name) return function(...) return printToLog(name, ...) end end + + function createStrictIndexFn(tbl) + return function(_, key) + local res = tbl[key] + if res ~= nil then + return res + else + error('Key not found: '..tostring(key), 2) + end + end + end + function pairsForReadOnly(v) + local nextFn, t, firstKey = pairs(getmetatable(v).t) + return function(_, k) return nextFn(t, k) end, v, firstKey + end + function ipairsForReadOnly(v) + local nextFn, t, firstKey = ipairs(getmetatable(v).t) + return function(_, k) return nextFn(t, k) end, v, firstKey + end + local function nextForArray(array, index) + index = (index or 0) + 1 + if index <= #array then + return index, array[index] + end + end + function ipairsForArray(array) + return nextForArray, array, 0 + end + + getmetatable('').__metatable = false + getSafeMetatable = function(v) + if type(v) ~= 'table' then error('getmetatable is allowed only for tables', 2) end + return getmetatable(v) + end + )"); + + mSandboxEnv = sol::table(mLua, sol::create); + mSandboxEnv["_VERSION"] = mLua["_VERSION"]; + for (const std::string& s : safeFunctions) + { + if (mLua[s] == sol::nil) throw std::logic_error("Lua function not found: " + s); + mSandboxEnv[s] = mLua[s]; + } + for (const std::string& s : safePackages) + { + if (mLua[s] == sol::nil) throw std::logic_error("Lua package not found: " + s); + mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]); + } + mSandboxEnv["getmetatable"] = mLua["getSafeMetatable"]; + mCommonPackages["os"] = mSandboxEnv["os"] = makeReadOnly(tableFromPairs({ + {"date", mLua["os"]["date"]}, + {"difftime", mLua["os"]["difftime"]}, + {"time", mLua["os"]["time"]} + })); + } + + LuaState::~LuaState() + { + // Should be cleaned before destructing mLua. + mCommonPackages.clear(); + mSandboxEnv = sol::nil; + } + + sol::table makeReadOnly(const sol::table& table, bool strictIndex) + { + if (table == sol::nil) + return table; + if (table.is()) + return table; // it is already userdata, no sense to wrap it again + + lua_State* luaState = table.lua_state(); + sol::state_view lua(luaState); + sol::table meta(lua, sol::create); + meta["t"] = table; + if (strictIndex) + meta["__index"] = lua["createStrictIndexFn"](table); + else + meta["__index"] = table; + meta["__pairs"] = lua["pairsForReadOnly"]; + meta["__ipairs"] = lua["ipairsForReadOnly"]; + + lua_newuserdata(luaState, 0); + sol::stack::push(luaState, meta); + lua_setmetatable(luaState, -2); + return sol::stack::pop(luaState); + } + + sol::table getMutableFromReadOnly(const sol::userdata& ro) + { + return ro[sol::metatable_key].get()["t"]; + } + + void LuaState::addCommonPackage(std::string packageName, sol::object package) + { + if (!package.is()) + package = makeReadOnly(std::move(package)); + mCommonPackages.emplace(std::move(packageName), std::move(package)); + } + + sol::protected_function_result LuaState::runInNewSandbox( + const std::string& path, const std::string& namePrefix, + const std::map& packages, const sol::object& hiddenData) + { + sol::protected_function script = loadScriptAndCache(path); + + sol::environment env(mLua, sol::create, mSandboxEnv); + std::string envName = namePrefix + "[" + path + "]:"; + env["print"] = mLua["printGen"](envName); + env["_G"] = env; + env[sol::metatable_key]["__metatable"] = false; + + auto maybeRunLoader = [&hiddenData](const sol::object& package) -> sol::object + { + if (package.is()) + return call(package.as(), hiddenData); + else + return package; + }; + sol::table loaded(mLua, sol::create); + for (const auto& [key, value] : mCommonPackages) + loaded[key] = maybeRunLoader(value); + for (const auto& [key, value] : packages) + loaded[key] = maybeRunLoader(value); + env["require"] = [this, env, loaded, hiddenData](std::string_view packageName) mutable + { + sol::object package = loaded[packageName]; + if (package != sol::nil) + return package; + sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS)); + sol::set_environment(env, packageLoader); + package = call(packageLoader, packageName); + loaded[packageName] = package; + return package; + }; + + sol::set_environment(env, script); + return call(script); + } + + sol::environment LuaState::newInternalLibEnvironment() + { + sol::environment env(mLua, sol::create, mSandboxEnv); + sol::table loaded(mLua, sol::create); + for (const std::string& s : safePackages) + loaded[s] = static_cast(mSandboxEnv[s]); + env["require"] = [this, loaded, env](const std::string& module) mutable + { + if (loaded[module] != sol::nil) + return loaded[module]; + sol::protected_function initializer = loadInternalLib(module); + sol::set_environment(env, initializer); + loaded[module] = call(initializer, module); + return loaded[module]; + }; + return env; + } + + sol::protected_function_result LuaState::throwIfError(sol::protected_function_result&& res) + { + if (!res.valid() && static_cast(res.get_type()) == LUA_TSTRING) + throw std::runtime_error("Lua error: " + res.get()); + else + return std::move(res); + } + + sol::function LuaState::loadScriptAndCache(const std::string& path) + { + auto iter = mCompiledScripts.find(path); + if (iter != mCompiledScripts.end()) + return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary); + sol::function res = loadFromVFS(path); + mCompiledScripts[path] = res.dump(); + return res; + } + + sol::function LuaState::loadFromVFS(const std::string& path) + { + std::string fileContent(std::istreambuf_iterator(*mVFS->get(path)), {}); + sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text); + if (!res.valid()) + throw std::runtime_error("Lua error: " + res.get()); + return res; + } + + sol::function LuaState::loadInternalLib(std::string_view libName) + { + std::string path = packageNameToPath(libName, mLibSearchPaths); + sol::load_result res = mLua.load_file(path, sol::load_mode::text); + if (!res.valid()) + throw std::runtime_error("Lua error: " + res.get()); + return res; + } + + std::string getLuaVersion() + { + #ifdef NO_LUAJIT + return LUA_RELEASE; + #else + return LUA_RELEASE " (" LUAJIT_VERSION ")"; + #endif + } + + std::string toString(const sol::object& obj) + { + if (obj == sol::nil) + return "nil"; + else if (obj.get_type() == sol::type::string) + return "\"" + obj.as() + "\""; + else + return call(sol::state_view(obj.lua_state())["tostring"], obj); + } + +} diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp new file mode 100644 index 0000000000..3bd27f2250 --- /dev/null +++ b/components/lua/luastate.hpp @@ -0,0 +1,152 @@ +#ifndef COMPONENTS_LUA_LUASTATE_H +#define COMPONENTS_LUA_LUASTATE_H + +#include + +#include + +#include "configuration.hpp" + +namespace VFS +{ + class Manager; +} + +namespace LuaUtil +{ + + std::string getLuaVersion(); + + // Holds Lua state. + // Provides additional features: + // - Load scripts from the virtual filesystem; + // - Caching of loaded scripts; + // - Disable unsafe Lua functions; + // - Run every instance of every script in a separate sandbox; + // - Forbid any interactions between sandboxes except than via provided API; + // - Access to common read-only resources from different sandboxes; + // - Replace standard `require` with a safe version that allows to search + // Lua libraries (only source, no dll's) in the virtual filesystem; + // - Make `print` to add the script name to every message and + // write to the Log rather than directly to stdout; + class LuaState + { + public: + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); + ~LuaState(); + + // Returns underlying sol::state. + sol::state& sol() { return mLua; } + + // Can be used by a C++ function that is called from Lua to get the Lua traceback. + // Makes no sense if called not from Lua code. + // Note: It is a slow function, should be used for debug purposes only. + std::string debugTraceback() { return mLua["debug"]["traceback"]().get(); } + + // A shortcut to create a new Lua table. + sol::table newTable() { return sol::table(mLua, sol::create); } + + template + sol::table tableFromPairs(std::initializer_list> list) + { + sol::table res(mLua, sol::create); + for (const auto& [k, v] : list) + res[k] = v; + return res; + } + + // Registers a package that will be available from every sandbox via `require(name)`. + // The package can be either a sol::table with an API or a sol::function. If it is a function, + // it will be evaluated (once per sandbox) the first time when requested. If the package + // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). + void addCommonPackage(std::string packageName, sol::object package); + + // Creates a new sandbox, runs a script, and returns the result + // (the result is expected to be an interface of the script). + // Args: + // path: path to the script in the virtual filesystem; + // namePrefix: sandbox name will be "[]". Sandbox name + // will be added to every `print` output. + // packages: additional packages that should be available from the sandbox via `require`. Each package + // should be either a sol::table or a sol::function. If it is a function, it will be evaluated + // (once per sandbox) with the argument 'hiddenData' the first time when requested. + sol::protected_function_result runInNewSandbox(const std::string& path, + const std::string& namePrefix = "", + const std::map& packages = {}, + const sol::object& hiddenData = sol::nil); + + void dropScriptCache() { mCompiledScripts.clear(); } + + const ScriptsConfiguration& getConfiguration() const { return *mConf; } + + // Load internal Lua library. All libraries are loaded in one sandbox and shouldn't be exposed to scripts directly. + void addInternalLibSearchPath(const std::string& path) { mLibSearchPaths.push_back(path); } + sol::function loadInternalLib(std::string_view libName); + sol::function loadFromVFS(const std::string& path); + sol::environment newInternalLibEnvironment(); + + private: + static sol::protected_function_result throwIfError(sol::protected_function_result&&); + template + friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); + + sol::function loadScriptAndCache(const std::string& path); + + sol::state mLua; + const ScriptsConfiguration* mConf; + sol::table mSandboxEnv; + std::map mCompiledScripts; + std::map mCommonPackages; + const VFS::Manager* mVFS; + std::vector mLibSearchPaths; + }; + + // Should be used for every call of every Lua function. + // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 + template + sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) + { + try + { + return LuaState::throwIfError(fn(std::forward(args)...)); + } + catch (std::exception&) { throw; } + catch (...) { throw std::runtime_error("Unknown error"); } + } + + // getFieldOrNil(table, "a", "b", "c") returns table["a"]["b"]["c"] or nil if some of the fields doesn't exist. + template + sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... str) + { + if (!table.is()) + return sol::nil; + if constexpr (sizeof...(str) == 0) + return table.as()[first]; + else + return getFieldOrNil(table.as()[first], str...); + } + + // String representation of a Lua object. Should be used for debugging/logging purposes only. + std::string toString(const sol::object&); + + template + T getValueOrDefault(const sol::object& obj, const T& defaultValue) + { + if (obj == sol::nil) + return defaultValue; + if (obj.is()) + return obj.as(); + else + throw std::logic_error(std::string("Value \"") + toString(obj) + std::string("\" has unexpected type")); + } + + // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. + // Needed to forbid any changes in common resources that can be accessed from different sandboxes. + // `strictIndex = true` replaces default `__index` with a strict version that throws an error if key is not found. + sol::table makeReadOnly(const sol::table&, bool strictIndex = false); + inline sol::table makeStrictReadOnly(const sol::table& tbl) { return makeReadOnly(tbl, true); } + sol::table getMutableFromReadOnly(const sol::userdata&); + +} + +#endif // COMPONENTS_LUA_LUASTATE_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp new file mode 100644 index 0000000000..9e20a215fa --- /dev/null +++ b/components/lua/scriptscontainer.cpp @@ -0,0 +1,536 @@ +#include "scriptscontainer.hpp" + +#include + +namespace LuaUtil +{ + static constexpr std::string_view ENGINE_HANDLERS = "engineHandlers"; + static constexpr std::string_view EVENT_HANDLERS = "eventHandlers"; + + static constexpr std::string_view INTERFACE_NAME = "interfaceName"; + static constexpr std::string_view INTERFACE = "interface"; + + static constexpr std::string_view HANDLER_INIT = "onInit"; + static constexpr std::string_view HANDLER_SAVE = "onSave"; + static constexpr std::string_view HANDLER_LOAD = "onLoad"; + static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; + + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) + : mNamePrefix(namePrefix), mLua(*lua) + { + registerEngineHandlers({&mUpdateHandlers}); + mPublicInterfaces = sol::table(lua->sol(), sol::create); + addPackage("openmw.interfaces", mPublicInterfaces); + } + + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); + } + + void ScriptsContainer::addPackage(std::string packageName, sol::object package) + { + mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package))); + } + + bool ScriptsContainer::addCustomScript(int scriptId) + { + const ScriptsConfiguration& conf = mLua.getConfiguration(); + assert(conf.isCustomScript(scriptId)); + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit, conf[scriptId].mInitializationData); + return ok; + } + + void ScriptsContainer::addAutoStartedScripts() + { + for (const auto& [scriptId, data] : mAutoStartScripts) + { + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit, data); + } + } + + bool ScriptsContainer::addScript(int scriptId, std::optional& onInit, std::optional& onLoad) + { + assert(scriptId >= 0 && scriptId < static_cast(mLua.getConfiguration().size())); + if (mScripts.count(scriptId) != 0) + return false; // already present + + const std::string& path = scriptPath(scriptId); + std::string debugName = mNamePrefix; + debugName.push_back('['); + debugName.append(path); + debugName.push_back(']'); + + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[sScriptIdKey] = ScriptId{this, scriptId}; + script.mHiddenData[sScriptDebugNameKey] = debugName; + script.mPath = path; + + try + { + sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); + if (scriptOutput == sol::nil) + return true; + sol::object engineHandlers = sol::nil, eventHandlers = sol::nil; + for (const auto& [key, value] : sol::table(scriptOutput)) + { + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + engineHandlers = value; + else if (sectionName == EVENT_HANDLERS) + eventHandlers = value; + else if (sectionName == INTERFACE_NAME) + script.mInterfaceName = value.as(); + else if (sectionName == INTERFACE) + script.mInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << debugName; + } + if (engineHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(engineHandlers)) + { + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_INIT) + onInit = sol::function(fn); + else if (handlerName == HANDLER_LOAD) + onLoad = sol::function(fn); + else if (handlerName == HANDLER_SAVE) + script.mOnSave = sol::function(fn); + else if (handlerName == HANDLER_INTERFACE_OVERRIDE) + script.mOnOverride = sol::function(fn); + else + { + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << debugName; + else + insertHandler(it->second->mList, scriptId, fn); + } + } + } + if (eventHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(eventHandlers)) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first; + insertHandler(it->second, scriptId, fn); + } + } + + if (script.mInterfaceName.empty() == script.mInterface.has_value()) + { + Log(Debug::Error) << debugName << ": 'interfaceName' should always be used together with 'interface'"; + script.mInterfaceName.clear(); + script.mInterface = sol::nil; + } + else if (script.mInterface) + { + script.mInterface = makeReadOnly(*script.mInterface); + insertInterface(scriptId, script); + } + + return true; + } + catch (std::exception& e) + { + mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil; + mScripts.erase(scriptId); + Log(Debug::Error) << "Can't start " << debugName << "; " << e.what(); + return false; + } + } + + void ScriptsContainer::removeScript(int scriptId) + { + auto scriptIter = mScripts.find(scriptId); + if (scriptIter == mScripts.end()) + return; // no such script + Script& script = scriptIter->second; + if (script.mInterface) + removeInterface(scriptId, script); + script.mHiddenData[sScriptIdKey] = sol::nil; + mScripts.erase(scriptIter); + for (auto& [_, handlers] : mEngineHandlers) + removeHandler(handlers->mList, scriptId); + for (auto& [_, handlers] : mEventHandlers) + removeHandler(handlers, scriptId); + } + + void ScriptsContainer::insertInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) + { + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else + { + next = &otherScript; + nextId = otherId; + break; + } + } + if (prev && script.mOnOverride) + { + try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); } + catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); } + } + if (next && next->mOnOverride) + { + try { LuaUtil::call(*next->mOnOverride, *script.mInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + if (next == nullptr) + mPublicInterfaces[script.mInterfaceName] = *script.mInterface; + } + + void ScriptsContainer::removeInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) + { + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else + { + next = &otherScript; + nextId = otherId; + break; + } + } + if (next) + { + if (next->mOnOverride) + { + sol::object prevInterface = sol::nil; + if (prev) + prevInterface = *prev->mInterface; + try { LuaUtil::call(*next->mOnOverride, prevInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + } + else if (prev) + mPublicInterfaces[script.mInterfaceName] = *prev->mInterface; + else + mPublicInterfaces[script.mInterfaceName] = sol::nil; + } + + void ScriptsContainer::insertHandler(std::vector& list, int scriptId, sol::function fn) + { + list.emplace_back(); + int pos = list.size() - 1; + while (pos > 0 && list[pos - 1].mScriptId > scriptId) + { + list[pos] = std::move(list[pos - 1]); + pos--; + } + list[pos].mScriptId = scriptId; + list[pos].mFn = std::move(fn); + } + + void ScriptsContainer::removeHandler(std::vector& list, int scriptId) + { + list.erase(std::remove_if(list.begin(), list.end(), + [scriptId](const Handler& h){ return h.mScriptId == scriptId; }), + list.end()); + } + + void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) + { + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + { + Log(Debug::Warning) << mNamePrefix << " has received event '" << eventName << "', but there are no handlers for this event"; + return; + } + sol::object data; + try + { + data = LuaUtil::deserialize(mLua.sol(), eventData, mSerializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " can not parse eventData for '" << eventName << "': " << e.what(); + return; + } + EventHandlerList& list = it->second; + for (int i = list.size() - 1; i >= 0; --i) + { + try + { + sol::object res = LuaUtil::call(list[i].mFn, data); + if (res != sol::nil && !res.as()) + break; // Skip other handlers if 'false' was returned. + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) + << "] eventHandler[" << eventName << "] failed. " << e.what(); + } + } + } + + void ScriptsContainer::registerEngineHandlers(std::initializer_list handlers) + { + for (EngineHandlerList* h : handlers) + mEngineHandlers[h->mName] = h; + } + + void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit, std::string_view data) + { + try + { + LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + } + catch (std::exception& e) { printError(scriptId, "onInit failed", e); } + } + + void ScriptsContainer::save(ESM::LuaScripts& data) + { + std::map> timers; + auto saveTimerFn = [&](const Timer& timer, TimerType timerType) + { + if (!timer.mSerializable) + return; + ESM::LuaTimer savedTimer; + savedTimer.mTime = timer.mTime; + savedTimer.mType = timerType; + savedTimer.mCallbackName = std::get(timer.mCallback); + savedTimer.mCallbackArgument = timer.mSerializedArg; + timers[timer.mScriptId].push_back(std::move(savedTimer)); + }; + for (const Timer& timer : mSimulationTimersQueue) + saveTimerFn(timer, TimerType::SIMULATION_TIME); + for (const Timer& timer : mGameTimersQueue) + saveTimerFn(timer, TimerType::GAME_TIME); + data.mScripts.clear(); + for (auto& [scriptId, script] : mScripts) + { + ESM::LuaScript savedScript; + // Note: We can not use `scriptPath(scriptId)` here because `save` can be called during + // evaluating "reloadlua" command when ScriptsConfiguration is already changed. + savedScript.mScriptPath = script.mPath; + if (script.mOnSave) + { + try + { + sol::object state = LuaUtil::call(*script.mOnSave); + savedScript.mData = serialize(state, mSerializer); + } + catch (std::exception& e) { printError(scriptId, "onSave failed", e); } + } + auto timersIt = timers.find(scriptId); + if (timersIt != timers.end()) + savedScript.mTimers = std::move(timersIt->second); + data.mScripts.push_back(std::move(savedScript)); + } + } + + void ScriptsContainer::load(const ESM::LuaScripts& data) + { + removeAllScripts(); + const ScriptsConfiguration& cfg = mLua.getConfiguration(); + + struct ScriptInfo + { + std::string_view mInitData; + const ESM::LuaScript* mSavedData; + }; + std::map scripts; + for (const auto& [scriptId, initData] : mAutoStartScripts) + scripts[scriptId] = {initData, nullptr}; + for (const ESM::LuaScript& s : data.mScripts) + { + std::optional scriptId = cfg.findId(s.mScriptPath); + if (!scriptId) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered"; + continue; + } + auto it = scripts.find(*scriptId); + if (it != scripts.end()) + it->second.mSavedData = &s; + else if (cfg.isCustomScript(*scriptId)) + scripts[*scriptId] = {cfg[*scriptId].mInitializationData, &s}; + else + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here"; + } + + for (const auto& [scriptId, scriptInfo] : scripts) + { + std::optional onInit, onLoad; + if (!addScript(scriptId, onInit, onLoad)) + continue; + if (scriptInfo.mSavedData == nullptr) + { + if (onInit) + callOnInit(scriptId, *onInit, scriptInfo.mInitData); + continue; + } + if (onLoad) + { + try + { + sol::object state = deserialize(mLua.sol(), scriptInfo.mSavedData->mData, mSavedDataDeserializer); + sol::object initializationData = + deserialize(mLua.sol(), scriptInfo.mInitData, mSerializer); + LuaUtil::call(*onLoad, state, initializationData); + } + catch (std::exception& e) { printError(scriptId, "onLoad failed", e); } + } + for (const ESM::LuaTimer& savedTimer : scriptInfo.mSavedData->mTimers) + { + Timer timer; + timer.mCallback = savedTimer.mCallbackName; + timer.mSerializable = true; + timer.mScriptId = scriptId; + timer.mTime = savedTimer.mTime; + + try + { + timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSavedDataDeserializer); + // It is important if the order of content files was changed. The deserialize-serialize procedure + // updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument. + timer.mSerializedArg = serialize(timer.mArg, mSerializer); + + if (savedTimer.mType == TimerType::GAME_TIME) + mGameTimersQueue.push_back(std::move(timer)); + else + mSimulationTimersQueue.push_back(std::move(timer)); + } + catch (std::exception& e) { printError(scriptId, "can not load timer", e); } + } + } + + std::make_heap(mSimulationTimersQueue.begin(), mSimulationTimersQueue.end()); + std::make_heap(mGameTimersQueue.begin(), mGameTimersQueue.end()); + } + + ScriptsContainer::~ScriptsContainer() + { + for (auto& [_, script] : mScripts) + script.mHiddenData[sScriptIdKey] = sol::nil; + } + + // Note: shouldn't be called from destructor because mEngineHandlers has pointers on + // external objects that are already removed during child class destruction. + void ScriptsContainer::removeAllScripts() + { + for (auto& [_, script] : mScripts) + script.mHiddenData[sScriptIdKey] = sol::nil; + mScripts.clear(); + for (auto& [_, handlers] : mEngineHandlers) + handlers->mList.clear(); + mEventHandlers.clear(); + mSimulationTimersQueue.clear(); + mGameTimersQueue.clear(); + mPublicInterfaces.clear(); + } + + ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId) + { + auto it = mScripts.find(scriptId); + if (it == mScripts.end()) + throw std::logic_error("Script doesn't exist"); + return it->second; + } + + void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) + { + getScript(scriptId).mRegisteredCallbacks.emplace(std::string(callbackName), std::move(callback)); + } + + void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) + { + timerQueue.push_back(std::move(t)); + std::push_heap(timerQueue.begin(), timerQueue.end()); + } + + void ScriptsContainer::setupSerializableTimer(TimerType type, double time, int scriptId, + std::string_view callbackName, sol::object callbackArg) + { + Timer t; + t.mCallback = std::string(callbackName); + t.mScriptId = scriptId; + t.mSerializable = true; + t.mTime = time; + t.mArg = callbackArg; + t.mSerializedArg = serialize(t.mArg, mSerializer); + insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t)); + } + + void ScriptsContainer::setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback) + { + Timer t; + t.mScriptId = scriptId; + t.mSerializable = false; + t.mTime = time; + + t.mCallback = mTemporaryCallbackCounter; + getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback)); + mTemporaryCallbackCounter++; + + insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t)); + } + + void ScriptsContainer::callTimer(const Timer& t) + { + try + { + Script& script = getScript(t.mScriptId); + if (t.mSerializable) + { + const std::string& callbackName = std::get(t.mCallback); + auto it = script.mRegisteredCallbacks.find(callbackName); + if (it == script.mRegisteredCallbacks.end()) + throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); + LuaUtil::call(it->second, t.mArg); + } + else + { + int64_t id = std::get(t.mCallback); + LuaUtil::call(script.mTemporaryCallbacks.at(id)); + script.mTemporaryCallbacks.erase(id); + } + } + catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } + } + + void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) + { + while (!timerQueue.empty() && timerQueue.front().mTime <= time) + { + callTimer(timerQueue.front()); + std::pop_heap(timerQueue.begin(), timerQueue.end()); + timerQueue.pop_back(); + } + } + + void ScriptsContainer::processTimers(double simulationTime, double gameTime) + { + updateTimerQueue(mSimulationTimersQueue, simulationTime); + updateTimerQueue(mGameTimersQueue, gameTime); + } + +} diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp new file mode 100644 index 0000000000..1599966376 --- /dev/null +++ b/components/lua/scriptscontainer.hpp @@ -0,0 +1,283 @@ +#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H +#define COMPONENTS_LUA_SCRIPTSCONTAINER_H + +#include +#include +#include + +#include +#include + +#include "luastate.hpp" +#include "serialization.hpp" + +namespace LuaUtil +{ + +// ScriptsContainer is a base class for all scripts containers (LocalScripts, +// GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. +// Scripts from different containers can interact to each other only via events. +// Scripts within one container can interact via interfaces. +// All scripts from one container have the same set of API packages available. +// +// Each script should return a table in a specific format that describes its +// handlers and interfaces. Every section of the table is optional. Basic structure: +// +// local function update(dt) +// print("Update") +// end +// +// local function someEventHandler(eventData) +// print("'SomeEvent' received") +// end +// +// return { +// -- Provides interface for other scripts in the same container +// interfaceName = "InterfaceName", +// interface = { +// someFunction = function() print("someFunction was called from another script") end, +// }, +// +// -- Script interface for the engine. Not available for other script. +// -- An error is printed if unknown handler is specified. +// engineHandlers = { +// onUpdate = update, +// onInit = function(initData) ... end, -- used when the script is just created (not loaded) +// onSave = function() return ... end, +// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave +// +// -- Works only if a child class has passed a EngineHandlerList +// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers. +// onSomethingElse = function() print("something else") end +// }, +// +// -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used for an event. +// eventHandlers = { +// SomeEvent = someEventHandler +// } +// } + + class ScriptsContainer + { + public: + // ScriptId of each script is stored with this key in Script::mHiddenData. + // Removed from mHiddenData when the script if removed. + constexpr static std::string_view sScriptIdKey = "_id"; + + // Debug identifier of each script is stored with this key in Script::mHiddenData. + // Present in mHiddenData even after removal of the script from ScriptsContainer. + constexpr static std::string_view sScriptDebugNameKey = "_name"; + + struct ScriptId + { + ScriptsContainer* mContainer; + int mIndex; // index in LuaUtil::ScriptsConfiguration + }; + using TimerType = ESM::LuaTimer::Type; + + // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. + // `autoStartScripts` specifies the list of scripts that should be autostarted in this container; + // the script names themselves are stored in ScriptsConfiguration. + ScriptsContainer(LuaState* lua, std::string_view namePrefix); + + ScriptsContainer(const ScriptsContainer&) = delete; + ScriptsContainer(ScriptsContainer&&) = delete; + virtual ~ScriptsContainer(); + + void setAutoStartConf(ScriptIdsWithInitializationData conf) { mAutoStartScripts = std::move(conf); } + const ScriptIdsWithInitializationData& getAutoStartConf() const { return mAutoStartScripts; } + + // Adds package that will be available (via `require`) for all scripts in the container. + // Automatically applies LuaUtil::makeReadOnly to the package. + void addPackage(std::string packageName, sol::object package); + + // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a new script, + // adds it to the container, and calls onInit for this script. Returns `true` if the script was successfully added. + // The script should have CUSTOM flag. If the flag is not set, or file not found, or has syntax errors, returns false. + // If such script already exists in the container, then also returns false. + bool addCustomScript(int scriptId); + + bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } + void removeScript(int scriptId); + + void processTimers(double simulationTime, double gameTime); + + // Calls `onUpdate` (if present) for every script in the container. + // Handlers are called in the same order as scripts were added. + void update(float dt) { callEngineHandlers(mUpdateHandlers, dt); } + + // Calls event handlers `eventName` (if present) for every script. + // If several scripts register handlers for `eventName`, they are called in reverse order. + // If some handler returns `false`, all remaining handlers are ignored. Any other return value + // (including `nil`) has no effect. + void receiveEvent(std::string_view eventName, std::string_view eventData); + + // Serializer defines how to serialize/deserialize userdata. If serializer is not provided, + // only built-in types and types from util package can be serialized. + void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + + // Special deserializer to use when load data from saves. Can be used to remap content files in Refnums. + void setSavedDataDeserializer(const UserdataSerializer* serializer) { mSavedDataDeserializer = serializer; } + + // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used. + void addAutoStartedScripts(); + + // Removes all scripts including the auto started. + void removeAllScripts(); + + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. + void save(ESM::LuaScripts&); + + // Removes all scripts; starts scripts according to `autoStartMode` and + // loads the savedScripts. Runs "onLoad" for each script. + void load(const ESM::LuaScripts& savedScripts); + + // Callbacks for serializable timers should be registered in advance. + // The script with the given path should already present in the container. + void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); + + // Sets up a timer, that can be automatically saved and loaded. + // type - the type of timer, either SIMULATION_TIME or GAME_TIME. + // time - the absolute game time (in seconds or in hours) when the timer should be executed. + // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. + // callbackName - callback (should be registered in advance) for this timer. + // callbackArg - parameter for the callback (should be serializable). + void setupSerializableTimer(TimerType type, double time, int scriptId, + std::string_view callbackName, sol::object callbackArg); + + // Creates a timer. `callback` is an arbitrary Lua function. These timers are called "unsavable" + // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state. + void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback); + + protected: + struct Handler + { + int mScriptId; + sol::function mFn; + }; + + struct EngineHandlerList + { + std::string_view mName; + std::vector mList; + + // "name" must be string literal + explicit EngineHandlerList(std::string_view name) : mName(name) {} + }; + + // Calls given handlers in direct order. + template + void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) + { + for (Handler& handler : handlers.mList) + { + try { LuaUtil::call(handler.mFn, args...); } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " + << handlers.mName << " failed. " << e.what(); + } + } + } + + // To add a new engine handler a derived class should register the corresponding EngineHandlerList and define + // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`. + void registerEngineHandlers(std::initializer_list handlers); + + const std::string mNamePrefix; + LuaUtil::LuaState& mLua; + + private: + struct Script + { + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; + std::string mInterfaceName; + sol::table mHiddenData; + std::map mRegisteredCallbacks; + std::map mTemporaryCallbacks; + std::string mPath; + }; + struct Timer + { + double mTime; + bool mSerializable; + int mScriptId; + std::variant mCallback; // string if serializable, integer otherwise + sol::object mArg; + std::string mSerializedArg; + + bool operator<(const Timer& t) const { return mTime > t.mTime; } + }; + using EventHandlerList = std::vector; + + // Add to container without calling onInit/onLoad. + bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); + + // Returns script by id (throws an exception if doesn't exist) + Script& getScript(int scriptId); + + void printError(int scriptId, std::string_view msg, const std::exception& e); + const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } + void callOnInit(int scriptId, const sol::function& onInit, std::string_view data); + void callTimer(const Timer& t); + void updateTimerQueue(std::vector& timerQueue, double time); + static void insertTimer(std::vector& timerQueue, Timer&& t); + static void insertHandler(std::vector& list, int scriptId, sol::function fn); + static void removeHandler(std::vector& list, int scriptId); + void insertInterface(int scriptId, const Script& script); + void removeInterface(int scriptId, const Script& script); + + ScriptIdsWithInitializationData mAutoStartScripts; + const UserdataSerializer* mSerializer = nullptr; + const UserdataSerializer* mSavedDataDeserializer = nullptr; + std::map mAPI; + + std::map mScripts; + sol::table mPublicInterfaces; + + EngineHandlerList mUpdateHandlers{"onUpdate"}; + std::map mEngineHandlers; + std::map> mEventHandlers; + + std::vector mSimulationTimersQueue; + std::vector mGameTimersQueue; + int64_t mTemporaryCallbackCounter = 0; + }; + + // Wrapper for a Lua function. + // Holds information about the script the function belongs to. + // Needed to prevent callback calls if the script was removed. + struct Callback + { + sol::function mFunc; + sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer + + bool isValid() const { return mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil; } + + template + sol::object call(Args&&... args) const + { + if (isValid()) + return LuaUtil::call(mFunc, std::forward(args)...); + else + Log(Debug::Debug) << "Ignored callback to the removed script " + << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); + return sol::nil; + } + + template + void tryCall(Args&&... args) const + { + try { this->call(std::forward(args)...); } + catch (std::exception& e) + { + Log(Debug::Error) << "Error in callback: " << e.what(); + } + } + }; + +} + +#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp new file mode 100644 index 0000000000..f1ee7c1aae --- /dev/null +++ b/components/lua/serialization.cpp @@ -0,0 +1,380 @@ +#include "serialization.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "luastate.hpp" +#include "utilpackage.hpp" + +namespace LuaUtil +{ + + constexpr unsigned char FORMAT_VERSION = 0; + + enum class SerializedType : char + { + NUMBER = 0x0, + LONG_STRING = 0x1, + BOOLEAN = 0x2, + TABLE_START = 0x3, + TABLE_END = 0x4, + + VEC2 = 0x10, + VEC3 = 0x11, + TRANSFORM_M = 0x12, + TRANSFORM_Q = 0x13, + VEC4 = 0x14, + COLOR = 0x15, + + // All values should be lesser than 0x20 (SHORT_STRING_FLAG). + }; + constexpr unsigned char SHORT_STRING_FLAG = 0x20; // 0b001SSSSS. SSSSS = string length + constexpr unsigned char CUSTOM_FULL_FLAG = 0x40; // 0b01TTTTTT + 32bit dataSize + constexpr unsigned char CUSTOM_COMPACT_FLAG = 0x80; // 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1) + + static void appendType(BinaryData& out, SerializedType type) + { + out.push_back(static_cast(type)); + } + + template + static void appendValue(BinaryData& out, T v) + { + v = Misc::toLittleEndian(v); + out.append(reinterpret_cast(&v), sizeof(v)); + } + + template + static T getValue(std::string_view& binaryData) + { + if (binaryData.size() < sizeof(T)) + throw std::runtime_error("Unexpected end of serialized data."); + T v; + std::memcpy(&v, binaryData.data(), sizeof(T)); + binaryData = binaryData.substr(sizeof(T)); + return Misc::fromLittleEndian(v); + } + + static void appendString(BinaryData& out, std::string_view str) + { + if (str.size() < 32) + out.push_back(SHORT_STRING_FLAG | char(str.size())); + else + { + appendType(out, SerializedType::LONG_STRING); + appendValue(out, str.size()); + } + out.append(str.data(), str.size()); + } + + static void appendData(BinaryData& out, const void* data, size_t dataSize) + { + out.append(reinterpret_cast(data), dataSize); + } + + void UserdataSerializer::append(BinaryData& out, std::string_view typeName, const void* data, size_t dataSize) + { + assert(!typeName.empty() && typeName.size() <= 64); + if (typeName.size() <= 8 && dataSize < 16) + { // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1). + unsigned char t = CUSTOM_COMPACT_FLAG | (dataSize << 3) | (typeName.size() - 1); + out.push_back(t); + } + else + { // Full form: 0b01TTTTTT + 32bit dataSize. + unsigned char t = CUSTOM_FULL_FLAG | (typeName.size() - 1); + out.push_back(t); + appendValue(out, dataSize); + } + out.append(typeName.data(), typeName.size()); + appendData(out, data, dataSize); + } + + void UserdataSerializer::appendRefNum(BinaryData& out, ESM::RefNum refnum) + { + static_assert(sizeof(ESM::RefNum) == 8); + refnum.mIndex = Misc::toLittleEndian(refnum.mIndex); + refnum.mContentFile = Misc::toLittleEndian(refnum.mContentFile); + append(out, sRefNumTypeName, &refnum, sizeof(ESM::RefNum)); + } + + bool BasicSerializer::serialize(BinaryData& out, const sol::userdata& data) const + { + appendRefNum(out, data.as()); + return true; + } + + bool BasicSerializer::deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const + { + if (typeName != sRefNumTypeName) + return false; + ESM::RefNum refnum = loadRefNum(binaryData); + if (mAdjustContentFilesIndexFn) + refnum.mContentFile = mAdjustContentFilesIndexFn(refnum.mContentFile); + sol::stack::push(lua, refnum); + return true; + } + + ESM::RefNum UserdataSerializer::loadRefNum(std::string_view data) + { + if (data.size() != sizeof(ESM::RefNum)) + throw std::runtime_error("Incorrect serialization format. Size of RefNum doesn't match."); + ESM::RefNum refnum; + std::memcpy(&refnum, data.data(), sizeof(ESM::RefNum)); + refnum.mIndex = Misc::fromLittleEndian(refnum.mIndex); + refnum.mContentFile = Misc::fromLittleEndian(refnum.mContentFile); + return refnum; + } + + static void serializeUserdata(BinaryData& out, const sol::userdata& data, const UserdataSerializer* customSerializer) + { + if (data.is()) + { + appendType(out, SerializedType::VEC2); + osg::Vec2f v = data.as(); + appendValue(out, v.x()); + appendValue(out, v.y()); + return; + } + if (data.is()) + { + appendType(out, SerializedType::VEC3); + osg::Vec3f v = data.as(); + appendValue(out, v.x()); + appendValue(out, v.y()); + appendValue(out, v.z()); + return; + } + if (data.is()) + { + appendType(out, SerializedType::TRANSFORM_M); + osg::Matrixf matrix = data.as().mM; + for (size_t i = 0; i < 4; i++) + for (size_t j = 0; j < 4; j++) + appendValue(out, matrix(i,j)); + return; + } + if (data.is()) + { + appendType(out, SerializedType::TRANSFORM_Q); + osg::Quat quat = data.as().mQ; + for(size_t i = 0; i < 4; i++) + appendValue(out, quat[i]); + return; + } + if (data.is()) + { + appendType(out, SerializedType::VEC4); + osg::Vec4f v = data.as(); + appendValue(out, v.x()); + appendValue(out, v.y()); + appendValue(out, v.z()); + appendValue(out, v.w()); + return; + } + if (data.is()) + { + appendType(out, SerializedType::COLOR); + Misc::Color v = data.as (); + appendValue(out, v.r()); + appendValue(out, v.g()); + appendValue(out, v.b()); + appendValue(out, v.a()); + return; + } + if (customSerializer && customSerializer->serialize(out, data)) + return; + else + throw std::runtime_error("Value is not serializable."); + } + + static void serialize(BinaryData& out, const sol::object& obj, const UserdataSerializer* customSerializer, int recursionCounter) + { + if (obj.get_type() == sol::type::lightuserdata) + throw std::runtime_error("Light userdata is not allowed to be serialized."); + if (obj.is()) + throw std::runtime_error("Functions are not allowed to be serialized."); + else if (obj.is()) + serializeUserdata(out, obj, customSerializer); + else if (obj.is()) + { + if (recursionCounter >= 32) + throw std::runtime_error("Can not serialize more than 32 nested tables. Likely the table contains itself."); + sol::table table = obj; + appendType(out, SerializedType::TABLE_START); + for (auto& [key, value] : table) + { + serialize(out, key, customSerializer, recursionCounter + 1); + serialize(out, value, customSerializer, recursionCounter + 1); + } + appendType(out, SerializedType::TABLE_END); + } + else if (obj.is()) + { + appendType(out, SerializedType::NUMBER); + appendValue(out, obj.as()); + } + else if (obj.is()) + appendString(out, obj.as()); + else if (obj.is()) + { + char v = obj.as() ? 1 : 0; + appendType(out, SerializedType::BOOLEAN); + out.push_back(v); + } else + throw std::runtime_error("Unknown Lua type."); + } + + static void deserializeImpl(lua_State* lua, std::string_view& binaryData, + const UserdataSerializer* customSerializer, bool readOnly) + { + if (binaryData.empty()) + throw std::runtime_error("Unexpected end of serialized data."); + unsigned char type = binaryData[0]; + binaryData = binaryData.substr(1); + if (type & (CUSTOM_COMPACT_FLAG | CUSTOM_FULL_FLAG)) + { + size_t typeNameSize, dataSize; + if (type & CUSTOM_COMPACT_FLAG) + { // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1). + typeNameSize = (type & 7) + 1; + dataSize = (type >> 3) & 15; + } + else + { // Full form: 0b01TTTTTT + 32bit dataSize. + typeNameSize = (type & 63) + 1; + dataSize = getValue(binaryData); + } + std::string_view typeName = binaryData.substr(0, typeNameSize); + std::string_view data = binaryData.substr(typeNameSize, dataSize); + binaryData = binaryData.substr(typeNameSize + dataSize); + if (!customSerializer || !customSerializer->deserialize(typeName, data, lua)) + throw std::runtime_error("Unknown type in serialized data: " + std::string(typeName)); + return; + } + if (type & SHORT_STRING_FLAG) + { + size_t size = type & 0x1f; + sol::stack::push(lua, binaryData.substr(0, size)); + binaryData = binaryData.substr(size); + return; + } + switch (static_cast(type)) + { + case SerializedType::NUMBER: + sol::stack::push(lua, getValue(binaryData)); + return; + case SerializedType::BOOLEAN: + sol::stack::push(lua, getValue(binaryData) != 0); + return; + case SerializedType::LONG_STRING: + { + uint32_t size = getValue(binaryData); + sol::stack::push(lua, binaryData.substr(0, size)); + binaryData = binaryData.substr(size); + return; + } + case SerializedType::TABLE_START: + { + lua_createtable(lua, 0, 0); + while (!binaryData.empty() && binaryData[0] != char(SerializedType::TABLE_END)) + { + deserializeImpl(lua, binaryData, customSerializer, readOnly); + deserializeImpl(lua, binaryData, customSerializer, readOnly); + lua_settable(lua, -3); + } + if (binaryData.empty()) + throw std::runtime_error("Unexpected end of serialized data."); + binaryData = binaryData.substr(1); + if (readOnly) + sol::stack::push(lua, makeReadOnly(sol::stack::pop(lua))); + return; + } + case SerializedType::TABLE_END: + throw std::runtime_error("Unexpected end of table during deserialization."); + case SerializedType::VEC2: + { + float x = getValue(binaryData); + float y = getValue(binaryData); + sol::stack::push(lua, osg::Vec2f(x, y)); + return; + } + case SerializedType::VEC3: + { + float x = getValue(binaryData); + float y = getValue(binaryData); + float z = getValue(binaryData); + sol::stack::push(lua, osg::Vec3f(x, y, z)); + return; + } + case SerializedType::TRANSFORM_M: + { + osg::Matrixf mat; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + mat(i, j) = getValue(binaryData); + sol::stack::push(lua, asTransform(mat)); + return; + } + case SerializedType::TRANSFORM_Q: + { + osg::Quat q; + for (int i = 0; i < 4; i++) + q[i] = getValue(binaryData); + sol::stack::push(lua, asTransform(q)); + return; + } + case SerializedType::VEC4: + { + float x = getValue(binaryData); + float y = getValue(binaryData); + float z = getValue(binaryData); + float w = getValue(binaryData); + sol::stack::push(lua, osg::Vec4f(x, y, z, w)); + return; + } + case SerializedType::COLOR: + { + float r = getValue(binaryData); + float g = getValue(binaryData); + float b = getValue(binaryData); + float a = getValue(binaryData); + sol::stack::push(lua, Misc::Color(r, g, b, a)); + return; + } + } + throw std::runtime_error("Unknown type in serialized data: " + std::to_string(type)); + } + + BinaryData serialize(const sol::object& obj, const UserdataSerializer* customSerializer) + { + if (obj == sol::nil) + return ""; + BinaryData res; + res.push_back(FORMAT_VERSION); + serialize(res, obj, customSerializer, 0); + return res; + } + + sol::object deserialize(lua_State* lua, std::string_view binaryData, + const UserdataSerializer* customSerializer, bool readOnly) + { + if (binaryData.empty()) + return sol::nil; + if (binaryData[0] != FORMAT_VERSION) + throw std::runtime_error("Incorrect version of Lua serialization format: " + + std::to_string(static_cast(binaryData[0]))); + binaryData = binaryData.substr(1); + deserializeImpl(lua, binaryData, customSerializer, readOnly); + if (!binaryData.empty()) + throw std::runtime_error("Unexpected data after serialized object"); + return sol::stack::pop(lua); + } + +} diff --git a/components/lua/serialization.hpp b/components/lua/serialization.hpp new file mode 100644 index 0000000000..03de88cf8e --- /dev/null +++ b/components/lua/serialization.hpp @@ -0,0 +1,57 @@ +#ifndef COMPONENTS_LUA_SERIALIZATION_H +#define COMPONENTS_LUA_SERIALIZATION_H + +#include + +#include + +namespace LuaUtil +{ + + // Note: it can contain \0 + using BinaryData = std::string; + + class UserdataSerializer + { + public: + virtual ~UserdataSerializer() {} + + // Appends serialized sol::userdata to the end of BinaryData. + // Returns false if this type of userdata is not supported by this serializer. + virtual bool serialize(BinaryData&, const sol::userdata&) const = 0; + + // Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push. + // Returns false if this type is not supported by this serializer. + virtual bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State*) const = 0; + + protected: + static void append(BinaryData&, std::string_view typeName, const void* data, size_t dataSize); + + static constexpr std::string_view sRefNumTypeName = "o"; + static void appendRefNum(BinaryData&, ESM::RefNum); + static ESM::RefNum loadRefNum(std::string_view data); + }; + + // Serializer that can load Lua data from content files and saved games, but doesn't depend on apps/openmw. + // Instead of LObject/GObject (that are defined in apps/openmw) it loads refnums directly as ESM::RefNum. + class BasicSerializer final : public UserdataSerializer + { + public: + BasicSerializer() = default; + explicit BasicSerializer(std::function adjustContentFileIndexFn) : + mAdjustContentFilesIndexFn(std::move(adjustContentFileIndexFn)) {} + + private: + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override; + bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override; + + std::function mAdjustContentFilesIndexFn; + }; + + BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr); + sol::object deserialize(lua_State* lua, std::string_view binaryData, + const UserdataSerializer* customSerializer = nullptr, bool readOnly = false); + +} + +#endif // COMPONENTS_LUA_SERIALIZATION_H diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp new file mode 100644 index 0000000000..bff978b633 --- /dev/null +++ b/components/lua/storage.cpp @@ -0,0 +1,220 @@ +#include "storage.hpp" + +#include +#include + +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace LuaUtil +{ + LuaStorage::Value LuaStorage::Section::sEmpty; + + sol::object LuaStorage::Value::getCopy(lua_State* L) const + { + return deserialize(L, mSerializedValue); + } + + sol::object LuaStorage::Value::getReadOnly(lua_State* L) const + { + if (mReadOnlyValue == sol::nil && !mSerializedValue.empty()) + mReadOnlyValue = deserialize(L, mSerializedValue, nullptr, true); + return mReadOnlyValue; + } + + const LuaStorage::Value& LuaStorage::Section::get(std::string_view key) const + { + auto it = mValues.find(key); + if (it != mValues.end()) + return it->second; + else + return sEmpty; + } + + void LuaStorage::Section::runCallbacks(sol::optional changedKey) + { + mStorage->mRunningCallbacks.insert(this); + mCallbacks.erase(std::remove_if(mCallbacks.begin(), mCallbacks.end(), [&](const Callback& callback) + { + bool valid = callback.isValid(); + if (valid) + callback.tryCall(mSectionName, changedKey); + return !valid; + }), mCallbacks.end()); + mStorage->mRunningCallbacks.erase(this); + } + + void LuaStorage::Section::throwIfCallbackRecursionIsTooDeep() + { + if (mStorage->mRunningCallbacks.count(this) > 0) + throw std::runtime_error("Storage handler shouldn't change the storage section it handles (leads to an infinite recursion)"); + if (mStorage->mRunningCallbacks.size() > 10) + throw std::runtime_error("Too many subscribe callbacks triggering in a chain, likely an infinite recursion"); + } + + void LuaStorage::Section::set(std::string_view key, const sol::object& value) + { + throwIfCallbackRecursionIsTooDeep(); + if (value != sol::nil) + mValues[std::string(key)] = Value(value); + else + { + auto it = mValues.find(key); + if (it != mValues.end()) + mValues.erase(it); + } + if (mStorage->mListener) + mStorage->mListener->valueChanged(mSectionName, key, value); + runCallbacks(key); + } + + void LuaStorage::Section::setAll(const sol::optional& values) + { + throwIfCallbackRecursionIsTooDeep(); + mValues.clear(); + if (values) + { + for (const auto& [k, v] : *values) + mValues[k.as()] = Value(v); + } + if (mStorage->mListener) + mStorage->mListener->sectionReplaced(mSectionName, values); + runCallbacks(sol::nullopt); + } + + sol::table LuaStorage::Section::asTable() + { + sol::table res(mStorage->mLua, sol::create); + for (const auto& [k, v] : mValues) + res[k] = v.getCopy(mStorage->mLua); + return res; + } + + void LuaStorage::initLuaBindings(lua_State* L) + { + sol::state_view lua(L); + sol::usertype sview = lua.new_usertype("Section"); + sview["get"] = [](sol::this_state s, const SectionView& section, std::string_view key) + { + return section.mSection->get(key).getReadOnly(s); + }; + sview["getCopy"] = [](sol::this_state s, const SectionView& section, std::string_view key) + { + return section.mSection->get(key).getCopy(s); + }; + sview["asTable"] = [](const SectionView& section) { return section.mSection->asTable(); }; + sview["subscribe"] = [](const SectionView& section, const Callback& callback) + { + std::vector& callbacks = section.mSection->mCallbacks; + if (!callbacks.empty() && callbacks.size() == callbacks.capacity()) + { + callbacks.erase(std::remove_if(callbacks.begin(), callbacks.end(), + [&](const Callback& c) { return !c.isValid(); }), + callbacks.end()); + } + callbacks.push_back(callback); + }; + sview["reset"] = [](const SectionView& section, const sol::optional& newValues) + { + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); + section.mSection->setAll(newValues); + }; + sview["removeOnExit"] = [](const SectionView& section) + { + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); + section.mSection->mPermanent = false; + }; + sview["set"] = [](const SectionView& section, std::string_view key, const sol::object& value) + { + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); + section.mSection->set(key, value); + }; + } + + void LuaStorage::clearTemporaryAndRemoveCallbacks() + { + auto it = mData.begin(); + while (it != mData.end()) + { + it->second->mCallbacks.clear(); + if (!it->second->mPermanent) + { + it->second->mValues.clear(); + it = mData.erase(it); + } + else + ++it; + } + } + + void LuaStorage::load(const std::string& path) + { + assert(mData.empty()); // Shouldn't be used before loading + try + { + Log(Debug::Info) << "Loading Lua storage \"" << path << "\" (" << std::filesystem::file_size(path) << " bytes)"; + std::ifstream fin(path, std::fstream::binary); + std::string serializedData((std::istreambuf_iterator(fin)), std::istreambuf_iterator()); + sol::table data = deserialize(mLua, serializedData); + for (const auto& [sectionName, sectionTable] : data) + { + const std::shared_ptr
& section = getSection(sectionName.as()); + for (const auto& [key, value] : sol::table(sectionTable)) + section->set(key.as(), value); + } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Can not read \"" << path << "\": " << e.what(); + } + } + + void LuaStorage::save(const std::string& path) const + { + sol::table data(mLua, sol::create); + for (const auto& [sectionName, section] : mData) + { + if (section->mPermanent && !section->mValues.empty()) + data[sectionName] = section->asTable(); + } + std::string serializedData = serialize(data); + Log(Debug::Info) << "Saving Lua storage \"" << path << "\" (" << serializedData.size() << " bytes)"; + std::ofstream fout(path, std::fstream::binary); + fout.write(serializedData.data(), serializedData.size()); + fout.close(); + } + + const std::shared_ptr& LuaStorage::getSection(std::string_view sectionName) + { + auto it = mData.find(sectionName); + if (it != mData.end()) + return it->second; + auto section = std::make_shared
(this, std::string(sectionName)); + sectionName = section->mSectionName; + auto [newIt, _] = mData.emplace(sectionName, std::move(section)); + return newIt->second; + } + + sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly) + { + const std::shared_ptr
& section = getSection(sectionName); + return sol::make_object(mLua, SectionView{section, readOnly}); + } + + sol::table LuaStorage::getAllSections(bool readOnly) + { + sol::table res(mLua, sol::create); + for (const auto& [sectionName, _] : mData) + res[sectionName] = getSection(sectionName, readOnly); + return res; + } + +} diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp new file mode 100644 index 0000000000..8ae944c5ab --- /dev/null +++ b/components/lua/storage.hpp @@ -0,0 +1,90 @@ +#ifndef COMPONENTS_LUA_STORAGE_H +#define COMPONENTS_LUA_STORAGE_H + +#include +#include + +#include "scriptscontainer.hpp" +#include "serialization.hpp" + +namespace LuaUtil +{ + + class LuaStorage + { + public: + static void initLuaBindings(lua_State*); + + explicit LuaStorage(lua_State* lua) : mLua(lua) {} + + void clearTemporaryAndRemoveCallbacks(); + void load(const std::string& path); + void save(const std::string& path) const; + + sol::object getSection(std::string_view sectionName, bool readOnly); + sol::object getMutableSection(std::string_view sectionName) { return getSection(sectionName, false); } + sol::object getReadOnlySection(std::string_view sectionName) { return getSection(sectionName, true); } + sol::table getAllSections(bool readOnly = false); + + void setSingleValue(std::string_view section, std::string_view key, const sol::object& value) + { getSection(section)->set(key, value); } + + void setSectionValues(std::string_view section, const sol::optional& values) + { getSection(section)->setAll(values); } + + class Listener + { + public: + virtual void valueChanged(std::string_view section, std::string_view key, const sol::object& value) const = 0; + virtual void sectionReplaced(std::string_view section, const sol::optional& values) const = 0; + }; + void setListener(const Listener* listener) { mListener = listener; } + + private: + class Value + { + public: + Value() {} + Value(const sol::object& value) : mSerializedValue(serialize(value)) {} + sol::object getCopy(lua_State* L) const; + sol::object getReadOnly(lua_State* L) const; + + private: + std::string mSerializedValue; + mutable sol::object mReadOnlyValue = sol::nil; + }; + + struct Section + { + explicit Section(LuaStorage* storage, std::string name) : mStorage(storage), mSectionName(std::move(name)) {} + const Value& get(std::string_view key) const; + void set(std::string_view key, const sol::object& value); + void setAll(const sol::optional& values); + sol::table asTable(); + void runCallbacks(sol::optional changedKey); + void throwIfCallbackRecursionIsTooDeep(); + + LuaStorage* mStorage; + std::string mSectionName; + std::map> mValues; + std::vector mCallbacks; + bool mPermanent = true; + static Value sEmpty; + }; + struct SectionView + { + std::shared_ptr
mSection; + bool mReadOnly; + }; + + const std::shared_ptr
& getSection(std::string_view sectionName); + + lua_State* mLua; + std::map> mData; + const Listener* mListener = nullptr; + std::set mRunningCallbacks; + }; + +} + +#endif // COMPONENTS_LUA_STORAGE_H diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp new file mode 100644 index 0000000000..a9c5c40a7c --- /dev/null +++ b/components/lua/utilpackage.cpp @@ -0,0 +1,267 @@ +#include "utilpackage.hpp" + +#include +#include +#include + +#include +#include + +#include "luastate.hpp" + +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 {}; + + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; +} + +namespace LuaUtil +{ + namespace { + template + void addVectorMethods(sol::usertype& vectorType) + { + vectorType[sol::meta_function::unary_minus] = [](const T& a) { return -a; }; + vectorType[sol::meta_function::addition] = [](const T& a, const T& b) { return a + b; }; + vectorType[sol::meta_function::subtraction] = [](const T& a, const T& b) { return a - b; }; + vectorType[sol::meta_function::equal_to] = [](const T& a, const T& b) { return a == b; }; + vectorType[sol::meta_function::multiplication] = sol::overload( + [](const T& a, float c) { return a * c; }, + [](const T& a, const T& b) { return a * b; }); + vectorType[sol::meta_function::division] = [](const T& a, float c) { return a / c; }; + vectorType["dot"] = [](const T& a, const T b) { return a * b; }; + vectorType["length"] = &T::length; + vectorType["length2"] = &T::length2; + vectorType["normalize"] = [](const T& v) + { + float len = v.length(); + if (len == 0) + return std::make_tuple(T(), 0.f); + else + return std::make_tuple(v * (1.f / len), len); + }; + vectorType["emul"] = [](const T& a, const T& b) + { + T result; + for (int i = 0; i < T::num_components; ++i) + result[i] = a[i] * b[i]; + return result; + }; + vectorType["ediv"] = [](const T& a, const T& b) + { + T result; + for (int i = 0; i < T::num_components; ++i) + result[i] = a[i] / b[i]; + return result; + }; + vectorType[sol::meta_function::to_string] = [](const T& v) + { + std::stringstream ss; + ss << "(" << v[0]; + for (int i = 1; i < T::num_components; ++i) + ss << ", " << v[i]; + ss << ")"; + return ss.str(); + }; + } + } + + sol::table initUtilPackage(sol::state& lua) + { + sol::table util(lua, sol::create); + + // Lua bindings for Vec2 + util["vector2"] = [](float x, float y) { return Vec2(x, y); }; + sol::usertype vec2Type = lua.new_usertype("Vec2"); + vec2Type["x"] = sol::readonly_property([](const Vec2& v) -> float { return v.x(); } ); + vec2Type["y"] = sol::readonly_property([](const Vec2& v) -> float { return v.y(); } ); + addVectorMethods(vec2Type); + vec2Type["rotate"] = &Misc::rotateVec2f; + + // Lua bindings for Vec3 + util["vector3"] = [](float x, float y, float z) { return Vec3(x, y, z); }; + sol::usertype vec3Type = lua.new_usertype("Vec3"); + vec3Type["x"] = sol::readonly_property([](const Vec3& v) -> float { return v.x(); } ); + vec3Type["y"] = sol::readonly_property([](const Vec3& v) -> float { return v.y(); } ); + vec3Type["z"] = sol::readonly_property([](const Vec3& v) -> float { return v.z(); } ); + addVectorMethods(vec3Type); + vec3Type[sol::meta_function::involution] = [](const Vec3& a, const Vec3& b) { return a ^ b; }; + vec3Type["cross"] = [](const Vec3& a, const Vec3& b) { return a ^ b; }; + + // Lua bindings for Vec4 + util["vector4"] = [](float x, float y, float z, float w) + { return Vec4(x, y, z, w); }; + sol::usertype vec4Type = lua.new_usertype("Vec4"); + vec4Type["x"] = sol::readonly_property([](const Vec4& v) -> float { return v.x(); }); + vec4Type["y"] = sol::readonly_property([](const Vec4& v) -> float { return v.y(); }); + vec4Type["z"] = sol::readonly_property([](const Vec4& v) -> float { return v.z(); }); + vec4Type["w"] = sol::readonly_property([](const Vec4& v) -> float { return v.w(); }); + addVectorMethods(vec4Type); + + // Lua bindings for Color + sol::usertype colorType = lua.new_usertype("Color"); + colorType["r"] = [](const Misc::Color& c) { return c.r(); }; + colorType["g"] = [](const Misc::Color& c) { return c.g(); }; + colorType["b"] = [](const Misc::Color& c) { return c.b(); }; + colorType["a"] = [](const Misc::Color& c) { return c.a(); }; + colorType[sol::meta_function::to_string] = [](const Misc::Color& c) { return c.toString(); }; + colorType["asRgba"] = [](const Misc::Color& c) { return Vec4(c.r(), c.g(), c.b(), c.a()); }; + colorType["asRgb"] = [](const Misc::Color& c) { return Vec3(c.r(), c.g(), c.b()); }; + colorType["asHex"] = [](const Misc::Color& c) { return c.toHex(); }; + + sol::table color(lua, sol::create); + color["rgba"] = [](float r, float g, float b, float a) { return Misc::Color(r, g, b, a); }; + color["rgb"] = [](float r, float g, float b) { return Misc::Color(r, g, b, 1); }; + color["hex"] = [](std::string_view hex) { return Misc::Color::fromHex(hex); }; + util["color"] = LuaUtil::makeReadOnly(color); + + // Lua bindings for Transform + sol::usertype transMType = lua.new_usertype("TransformM"); + sol::usertype transQType = lua.new_usertype("TransformQ"); + sol::table transforms(lua, sol::create); + util["transform"] = LuaUtil::makeReadOnly(transforms); + + transforms["identity"] = sol::make_object(lua, TransformM{osg::Matrixf::identity()}); + transforms["move"] = sol::overload( + [](const Vec3& v) { return TransformM{osg::Matrixf::translate(v)}; }, + [](float x, float y, float z) { return TransformM{osg::Matrixf::translate(x, y, z)}; }); + transforms["scale"] = sol::overload( + [](const Vec3& v) { return TransformM{osg::Matrixf::scale(v)}; }, + [](float x, float y, float z) { return TransformM{osg::Matrixf::scale(x, y, z)}; }); + transforms["rotate"] = [](float angle, const Vec3& axis) { return TransformQ{osg::Quat(angle, axis)}; }; + transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(-1, 0, 0))}; }; + transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, -1, 0))}; }; + transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, -1))}; }; + + transMType[sol::meta_function::multiplication] = sol::overload( + [](const TransformM& a, const Vec3& b) { return a.mM.preMult(b); }, + [](const TransformM& a, const TransformM& b) { return TransformM{b.mM * a.mM}; }, + [](const TransformM& a, const TransformQ& b) + { + TransformM res{a.mM}; + res.mM.preMultRotate(b.mQ); + return res; + }); + transMType[sol::meta_function::to_string] = [](const TransformM& m) + { + osg::Vec3f trans, scale; + osg::Quat rotation, so; + m.mM.decompose(trans, rotation, scale, so); + osg::Quat::value_type rot_angle, so_angle; + osg::Vec3f rot_axis, so_axis; + rotation.getRotate(rot_angle, rot_axis); + so.getRotate(so_angle, so_axis); + std::stringstream ss; + ss << "TransformM{ "; + if (trans.length2() > 0) + ss << "move(" << trans.x() << ", " << trans.y() << ", " << trans.z() << ") "; + if (rot_angle != 0) + ss << "rotation(angle=" << rot_angle << ", axis=(" + << rot_axis.x() << ", " << rot_axis.y() << ", " << rot_axis.z() << ")) "; + if (scale.x() != 1 || scale.y() != 1 || scale.z() != 1) + ss << "scale(" << scale.x() << ", " << scale.y() << ", " << scale.z() << ") "; + if (so_angle != 0) + ss << "rotation(angle=" << so_angle << ", axis=(" + << so_axis.x() << ", " << so_axis.y() << ", " << so_axis.z() << ")) "; + ss << "}"; + return ss.str(); + }; + transMType["inverse"] = [](const TransformM& m) + { + TransformM res; + if (!res.mM.invert_4x3(m.mM)) + throw std::runtime_error("This Transform is not invertible"); + return res; + }; + + transQType[sol::meta_function::multiplication] = sol::overload( + [](const TransformQ& a, const Vec3& b) { return a.mQ * b; }, + [](const TransformQ& a, const TransformQ& b) { return TransformQ{b.mQ * a.mQ}; }, + [](const TransformQ& a, const TransformM& b) + { + TransformM res{b}; + res.mM.postMultRotate(a.mQ); + return res; + }); + transQType[sol::meta_function::to_string] = [](const TransformQ& q) + { + osg::Quat::value_type angle; + osg::Vec3f axis; + q.mQ.getRotate(angle, axis); + std::stringstream ss; + ss << "TransformQ{ rotation(angle=" << angle << ", axis=(" + << axis.x() << ", " << axis.y() << ", " << axis.z() << ")) }"; + return ss.str(); + }; + transQType["inverse"] = [](const TransformQ& q) { return TransformQ{q.mQ.inverse()}; }; + + // Utility functions + util["clamp"] = [](float value, float from, float to) { return std::clamp(value, from, to); }; + // NOTE: `util["clamp"] = std::clamp` causes error 'AddressSanitizer: stack-use-after-scope' + util["normalizeAngle"] = &Misc::normalizeAngle; + util["makeReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/false); }; + util["makeStrictReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/true); }; + + if (lua["bit32"] != sol::nil) + { + sol::table bit = lua["bit32"]; + util["bitOr"] = bit["bor"]; + util["bitAnd"] = bit["band"]; + util["bitXor"] = bit["bxor"]; + util["bitNot"] = bit["bnot"]; + } + else + { + util["bitOr"] = [](unsigned a, sol::variadic_args va) + { + for (auto v : va) + a |= v.as(); + return a; + }; + util["bitAnd"] = [](unsigned a, sol::variadic_args va) + { + for (auto v : va) + a &= v.as(); + return a; + }; + util["bitXor"] = [](unsigned a, sol::variadic_args va) + { + for (auto v : va) + a ^= v.as(); + return a; + }; + util["bitNot"] = [](unsigned a) { return ~a; }; + } + + util["loadCode"] = [](const std::string& code, const sol::table& env, sol::this_state s) + { + sol::state_view lua(s); + sol::load_result res = lua.load(code, "", sol::load_mode::text); + if (!res.valid()) + throw std::runtime_error("Lua error: " + res.get()); + sol::function fn = res; + sol::environment newEnv(lua, sol::create, env); + newEnv[sol::metatable_key][sol::meta_function::new_index] = env; + sol::set_environment(newEnv, fn); + return fn; + }; + + return util; + } + +} diff --git a/components/lua/utilpackage.hpp b/components/lua/utilpackage.hpp new file mode 100644 index 0000000000..a647b682af --- /dev/null +++ b/components/lua/utilpackage.hpp @@ -0,0 +1,29 @@ +#ifndef COMPONENTS_LUA_UTILPACKAGE_H +#define COMPONENTS_LUA_UTILPACKAGE_H + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + using Vec2 = osg::Vec2f; + using Vec3 = osg::Vec3f; + using Vec4 = osg::Vec4f; + + // For performance reasons "Transform" is implemented as 2 types with the same interface. + // Transform supports only composition, inversion, and applying to a 3d vector. + struct TransformM { osg::Matrixf mM; }; + struct TransformQ { osg::Quat mQ; }; + + inline TransformM asTransform(const osg::Matrixf& m) { return {m}; } + inline TransformQ asTransform(const osg::Quat& q) { return {q}; } + + sol::table initUtilPackage(sol::state&); + +} + +#endif // COMPONENTS_LUA_UTILPACKAGE_H diff --git a/components/lua_ui/adapter.cpp b/components/lua_ui/adapter.cpp new file mode 100644 index 0000000000..88f282c970 --- /dev/null +++ b/components/lua_ui/adapter.cpp @@ -0,0 +1,61 @@ +#include "adapter.hpp" + +#include + +#include "element.hpp" +#include "container.hpp" + +namespace LuaUi +{ + namespace + { + sol::state luaState; + } + + LuaAdapter::LuaAdapter() + : mElement(nullptr) + , mContainer(nullptr) + { + mContainer = MyGUI::Gui::getInstancePtr()->createWidget( + "", MyGUI::IntCoord(), MyGUI::Align::Default, "", ""); + mContainer->initialize(luaState, mContainer); + mContainer->onCoordChange([this](WidgetExtension* ext, MyGUI::IntCoord coord) + { + setSize(coord.size()); + }); + mContainer->widget()->attachToWidget(this); + } + + void LuaAdapter::attach(const std::shared_ptr& element) + { + detachElement(); + mElement = element; + attachElement(); + setSize(mContainer->widget()->getSize()); + + // workaround for MyGUI bug + // parent visibility doesn't affect added children + setVisible(!getVisible()); + setVisible(!getVisible()); + } + + void LuaAdapter::detach() + { + detachElement(); + setSize({ 0, 0 }); + } + + void LuaAdapter::attachElement() + { + if (mElement.get()) + mElement->attachToWidget(mContainer); + } + + void LuaAdapter::detachElement() + { + if (mElement.get()) + mElement->detachFromWidget(); + mElement = nullptr; + } +} + diff --git a/components/lua_ui/adapter.hpp b/components/lua_ui/adapter.hpp new file mode 100644 index 0000000000..2cb08b7367 --- /dev/null +++ b/components/lua_ui/adapter.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_LUAUI_ADAPTER +#define OPENMW_LUAUI_ADAPTER + +#include + +#include + +namespace LuaUi +{ + class LuaContainer; + struct Element; + class LuaAdapter : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(LuaAdapter) + + public: + LuaAdapter(); + + void attach(const std::shared_ptr& element); + void detach(); + + private: + std::shared_ptr mElement; + LuaContainer* mContainer; + + void attachElement(); + void detachElement(); + }; +} + +#endif // !OPENMW_LUAUI_ADAPTER diff --git a/components/lua_ui/alignment.cpp b/components/lua_ui/alignment.cpp new file mode 100644 index 0000000000..358c5ba14b --- /dev/null +++ b/components/lua_ui/alignment.cpp @@ -0,0 +1,18 @@ +#include "alignment.hpp" + +namespace LuaUi +{ + MyGUI::Align alignmentToMyGui(Alignment horizontal, Alignment vertical) + { + MyGUI::Align align(MyGUI::Align::Center); + if (horizontal == Alignment::Start) + align |= MyGUI::Align::Left; + if (horizontal == Alignment::End) + align |= MyGUI::Align::Right; + if (vertical == Alignment::Start) + align |= MyGUI::Align::Top; + if (vertical == Alignment::End) + align |= MyGUI::Align::Bottom; + return align; + } +} diff --git a/components/lua_ui/alignment.hpp b/components/lua_ui/alignment.hpp new file mode 100644 index 0000000000..8503ffba03 --- /dev/null +++ b/components/lua_ui/alignment.hpp @@ -0,0 +1,18 @@ +#ifndef OPENMW_LUAUI_ALIGNMENT +#define OPENMW_LUAUI_ALIGNMENT + +#include + +namespace LuaUi +{ + enum class Alignment + { + Start = 0, + Center = 1, + End = 2 + }; + + MyGUI::Align alignmentToMyGui(Alignment horizontal, Alignment vertical); +} + +#endif // !OPENMW_LUAUI_PROPERTIES diff --git a/components/lua_ui/container.cpp b/components/lua_ui/container.cpp new file mode 100644 index 0000000000..52fea684d7 --- /dev/null +++ b/components/lua_ui/container.cpp @@ -0,0 +1,53 @@ +#include "container.hpp" + +#include + +namespace LuaUi +{ + void LuaContainer::updateChildren() + { + WidgetExtension::updateChildren(); + updateSizeToFit(); + } + + MyGUI::IntSize LuaContainer::childScalingSize() + { + return MyGUI::IntSize(); + } + + MyGUI::IntSize LuaContainer::templateScalingSize() + { + return mInnerSize; + } + + void LuaContainer::updateSizeToFit() + { + MyGUI::IntSize innerSize = MyGUI::IntSize(); + for (auto w : children()) + { + MyGUI::IntCoord coord = w->calculateCoord(); + innerSize.width = std::max(innerSize.width, coord.left + coord.width); + innerSize.height = std::max(innerSize.height, coord.top + coord.height); + } + MyGUI::IntSize outerSize = innerSize; + for (auto w : templateChildren()) + { + MyGUI::IntCoord coord = w->calculateCoord(); + outerSize.width = std::max(outerSize.width, coord.left + coord.width); + outerSize.height = std::max(outerSize.height, coord.top + coord.height); + } + mInnerSize = innerSize; + mOuterSize = outerSize; + } + + MyGUI::IntSize LuaContainer::calculateSize() + { + return mOuterSize; + } + + void LuaContainer::updateCoord() + { + updateSizeToFit(); + WidgetExtension::updateCoord(); + } +} diff --git a/components/lua_ui/container.hpp b/components/lua_ui/container.hpp new file mode 100644 index 0000000000..1a8adee89f --- /dev/null +++ b/components/lua_ui/container.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_LUAUI_CONTAINER +#define OPENMW_LUAUI_CONTAINER + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaContainer : public WidgetExtension, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(LuaContainer) + + MyGUI::IntSize calculateSize() override; + void updateCoord() override; + + protected: + void updateChildren() override; + MyGUI::IntSize childScalingSize() override; + MyGUI::IntSize templateScalingSize() override; + + private: + void updateSizeToFit(); + MyGUI::IntSize mInnerSize; + MyGUI::IntSize mOuterSize; + }; +} + +#endif // !OPENMW_LUAUI_CONTAINER diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp new file mode 100644 index 0000000000..e7cf474bc9 --- /dev/null +++ b/components/lua_ui/content.cpp @@ -0,0 +1,105 @@ +#include "content.hpp" + +namespace LuaUi +{ + Content::Content(const sol::table& table) + { + size_t size = table.size(); + for (size_t index = 0; index < size; ++index) + { + sol::object value = table.get(index + 1); + if (value.is()) + assign(index, value.as()); + else + throw std::logic_error("UI Content children must all be tables."); + } + } + + void Content::assign(size_t index, const sol::table& table) + { + if (mOrdered.size() < index) + throw std::logic_error("Can't have gaps in UI Content."); + if (index == mOrdered.size()) + mOrdered.push_back(table); + else + { + sol::optional oldName = mOrdered[index]["name"]; + if (oldName.has_value()) + mNamed.erase(oldName.value()); + mOrdered[index] = table; + } + sol::optional name = table["name"]; + if (name.has_value()) + mNamed[name.value()] = index; + } + + void Content::assign(std::string_view name, const sol::table& table) + { + auto it = mNamed.find(name); + if (it != mNamed.end()) + assign(it->second, table); + else + throw std::logic_error(std::string("Can't find a UI Content child with name ") += name); + } + + void Content::insert(size_t index, const sol::table& table) + { + if (mOrdered.size() < index) + throw std::logic_error("Can't have gaps in UI Content."); + mOrdered.insert(mOrdered.begin() + index, table); + for (size_t i = index; i < mOrdered.size(); ++i) + { + sol::optional name = mOrdered[i]["name"]; + if (name.has_value()) + mNamed[name.value()] = index; + } + } + + sol::table Content::at(size_t index) const + { + if (index > size()) + throw std::logic_error("Invalid UI Content index."); + return mOrdered.at(index); + } + + sol::table Content::at(std::string_view name) const + { + auto it = mNamed.find(name); + if (it == mNamed.end()) + throw std::logic_error("Invalid UI Content name."); + return mOrdered.at(it->second); + } + + size_t Content::remove(size_t index) + { + sol::table table = at(index); + sol::optional name = table["name"]; + if (name.has_value()) + { + auto it = mNamed.find(name.value()); + if (it != mNamed.end()) + mNamed.erase(it); + } + mOrdered.erase(mOrdered.begin() + index); + return index; + } + + size_t Content::remove(std::string_view name) + { + auto it = mNamed.find(name); + if (it == mNamed.end()) + throw std::logic_error("Invalid UI Content name."); + size_t index = it->second; + remove(index); + return index; + } + + size_t Content::indexOf(const sol::table& table) + { + auto it = std::find(mOrdered.begin(), mOrdered.end(), table); + if (it == mOrdered.end()) + return size(); + else + return it - mOrdered.begin(); + } +} diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp new file mode 100644 index 0000000000..e970744b3d --- /dev/null +++ b/components/lua_ui/content.hpp @@ -0,0 +1,41 @@ +#ifndef COMPONENTS_LUAUI_CONTENT +#define COMPONENTS_LUAUI_CONTENT + +#include +#include + +#include + +namespace LuaUi +{ + class Content + { + public: + using iterator = std::vector::iterator; + + Content() {} + + // expects a Lua array - a table with keys from 1 to n without any nil values in between + // any other keys are ignored + explicit Content(const sol::table&); + + size_t size() const { return mOrdered.size(); } + + void assign(std::string_view name, const sol::table& table); + void assign(size_t index, const sol::table& table); + void insert(size_t index, const sol::table& table); + + sol::table at(size_t index) const; + sol::table at(std::string_view name) const; + size_t remove(size_t index); + size_t remove(std::string_view name); + size_t indexOf(const sol::table& table); + + private: + std::map> mNamed; + std::vector mOrdered; + }; + +} + +#endif // COMPONENTS_LUAUI_CONTENT diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp new file mode 100644 index 0000000000..bd9f84fec2 --- /dev/null +++ b/components/lua_ui/element.cpp @@ -0,0 +1,258 @@ +#include "element.hpp" + +#include + +#include "content.hpp" +#include "util.hpp" +#include "widget.hpp" + +namespace LuaUi +{ + namespace + { + namespace LayoutKeys + { + constexpr std::string_view type = "type"; + constexpr std::string_view name = "name"; + constexpr std::string_view layer = "layer"; + constexpr std::string_view templateLayout = "template"; + constexpr std::string_view props = "props"; + constexpr std::string_view events = "events"; + constexpr std::string_view content = "content"; + constexpr std::string_view external = "external"; + } + + const std::string defaultWidgetType = "LuaWidget"; + + constexpr uint64_t maxDepth = 250; + + std::string widgetType(const sol::table& layout) + { + sol::object typeField = LuaUtil::getFieldOrNil(layout, LayoutKeys::type); + std::string type = LuaUtil::getValueOrDefault(typeField, defaultWidgetType); + sol::object templateTypeField = LuaUtil::getFieldOrNil(layout, LayoutKeys::templateLayout, LayoutKeys::type); + if (templateTypeField != sol::nil) + { + std::string templateType = LuaUtil::getValueOrDefault(templateTypeField, defaultWidgetType); + if (typeField != sol::nil && templateType != type) + throw std::logic_error(std::string("Template layout type ") + type + + std::string(" doesn't match template type ") + templateType); + type = templateType; + } + return type; + } + + void destroyWidget(LuaUi::WidgetExtension* ext) + { + ext->deinitialize(); + MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget()); + } + + WidgetExtension* createWidget(const sol::table& layout, uint64_t depth); + void updateWidget(WidgetExtension* ext, const sol::table& layout, uint64_t depth); + + std::vector updateContent( + const std::vector& children, const sol::object& contentObj, uint64_t depth) + { + ++depth; + std::vector result; + if (contentObj == sol::nil) + { + for (WidgetExtension* w : children) + destroyWidget(w); + return result; + } + if (!contentObj.is()) + throw std::logic_error("Layout content field must be a openmw.ui.content"); + Content content = contentObj.as(); + result.resize(content.size()); + size_t minSize = std::min(children.size(), content.size()); + for (size_t i = 0; i < minSize; i++) + { + WidgetExtension* ext = children[i]; + sol::table newLayout = content.at(i); + if (ext->widget()->getTypeName() == widgetType(newLayout)) + { + updateWidget(ext, newLayout, depth); + } + else + { + destroyWidget(ext); + ext = createWidget(newLayout, depth); + } + result[i] = ext; + } + for (size_t i = minSize; i < children.size(); i++) + destroyWidget(children[i]); + for (size_t i = minSize; i < content.size(); i++) + result[i] = createWidget(content.at(i), depth); + return result; + } + + void setTemplate(WidgetExtension* ext, const sol::object& templateLayout, uint64_t depth) + { + ++depth; + sol::object props = LuaUtil::getFieldOrNil(templateLayout, LayoutKeys::props); + ext->setTemplateProperties(props); + sol::object content = LuaUtil::getFieldOrNil(templateLayout, LayoutKeys::content); + ext->setTemplateChildren(updateContent(ext->templateChildren(), content, depth)); + } + + void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::object& eventsObj) + { + ext->clearCallbacks(); + if (eventsObj == sol::nil) + return; + if (!eventsObj.is()) + throw std::logic_error("The \"events\" layout field must be a table of callbacks"); + auto events = eventsObj.as(); + events.for_each([ext](const sol::object& name, const sol::object& callback) + { + if (name.is() && callback.is()) + ext->setCallback(name.as(), callback.as()); + else if (!name.is()) + Log(Debug::Warning) << "UI event key must be a string"; + else if (!callback.is()) + Log(Debug::Warning) << "UI event handler for key \"" << name.as() + << "\" must be an openmw.async.callback"; + }); + } + + WidgetExtension* createWidget(const sol::table& layout, uint64_t depth) + { + static auto widgetTypeMap = widgetTypeToName(); + std::string type = widgetType(layout); + if (widgetTypeMap.find(type) == widgetTypeMap.end()) + throw std::logic_error(std::string("Invalid widget type ") += type); + + std::string name = layout.get_or(LayoutKeys::name, std::string()); + MyGUI::Widget* widget = MyGUI::Gui::getInstancePtr()->createWidgetT( + type, "", + MyGUI::IntCoord(), MyGUI::Align::Default, + std::string(), name); + + WidgetExtension* ext = dynamic_cast(widget); + if (!ext) + throw std::runtime_error("Invalid widget!"); + ext->initialize(layout.lua_state(), widget); + + updateWidget(ext, layout, depth); + return ext; + } + + void updateWidget(WidgetExtension* ext, const sol::table& layout, uint64_t depth) + { + if (depth >= maxDepth) + throw std::runtime_error("Maximum layout depth exceeded, probably caused by a circular reference"); + ext->reset(); + ext->setLayout(layout); + ext->setExternal(layout.get(LayoutKeys::external)); + setTemplate(ext, layout.get(LayoutKeys::templateLayout), depth); + ext->setProperties(layout.get(LayoutKeys::props)); + setEventCallbacks(ext, layout.get(LayoutKeys::events)); + ext->setChildren(updateContent(ext->children(), layout.get(LayoutKeys::content), depth)); + ext->updateCoord(); + } + + std::string setLayer(WidgetExtension* ext, const sol::table& layout) + { + MyGUI::ILayer* layerNode = ext->widget()->getLayer(); + std::string currentLayer = layerNode ? layerNode->getName() : std::string(); + std::string newLayer = layout.get_or(LayoutKeys::layer, std::string()); + if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer)) + throw std::logic_error(std::string("Layer ") + newLayer + " doesn't exist"); + else if (newLayer != currentLayer) + { + MyGUI::LayerManager::getInstance().attachToLayerNode(newLayer, ext->widget()); + } + return newLayer; + } + } + + std::map> Element::sAllElements; + + Element::Element(sol::table layout) + : mRoot(nullptr) + , mAttachedTo(nullptr) + , mLayout(std::move(layout)) + , mLayer() + , mUpdate(false) + , mDestroy(false) + {} + + + std::shared_ptr Element::make(sol::table layout) + { + std::shared_ptr ptr(new Element(std::move(layout))); + sAllElements[ptr.get()] = ptr; + return ptr; + } + + void Element::create() + { + assert(!mRoot); + if (!mRoot) + { + mRoot = createWidget(mLayout, 0); + mLayer = setLayer(mRoot, mLayout); + updateAttachment(); + } + } + + void Element::update() + { + if (mRoot && mUpdate) + { + if (mRoot->widget()->getTypeName() != widgetType(mLayout)) + { + destroyWidget(mRoot); + mRoot = createWidget(mLayout, 0); + } + else + { + updateWidget(mRoot, mLayout, 0); + } + mLayer = setLayer(mRoot, mLayout); + updateAttachment(); + } + mUpdate = false; + } + + void Element::destroy() + { + if (mRoot) + destroyWidget(mRoot); + mRoot = nullptr; + sAllElements.erase(this); + } + + void Element::attachToWidget(WidgetExtension* w) + { + if (mAttachedTo) + throw std::logic_error("A UI element can't be attached to two widgets at once"); + mAttachedTo = w; + updateAttachment(); + } + + void Element::detachFromWidget() + { + if (mRoot) + mRoot->widget()->detachFromWidget(); + if (mAttachedTo) + mAttachedTo->setChildren({}); + mAttachedTo = nullptr; + } + + void Element::updateAttachment() + { + if (!mRoot) + return; + if (mAttachedTo) + { + if (!mLayer.empty()) + Log(Debug::Warning) << "Ignoring element's layer " << mLayer << " because it's attached to a widget"; + mAttachedTo->setChildren({ mRoot }); + mAttachedTo->updateCoord(); + } + } +} diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp new file mode 100644 index 0000000000..1387a1b51e --- /dev/null +++ b/components/lua_ui/element.hpp @@ -0,0 +1,44 @@ +#ifndef OPENMW_LUAUI_ELEMENT +#define OPENMW_LUAUI_ELEMENT + +#include "widget.hpp" + +namespace LuaUi +{ + struct Element + { + static std::shared_ptr make(sol::table layout); + + template + static void forEach(Callback callback) + { + for(auto& [e, _] : sAllElements) + callback(e); + } + + WidgetExtension* mRoot; + WidgetExtension* mAttachedTo; + sol::table mLayout; + std::string mLayer; + bool mUpdate; + bool mDestroy; + + void create(); + + void update(); + + void destroy(); + + friend void clearUserInterface(); + + void attachToWidget(WidgetExtension* w); + void detachFromWidget(); + + private: + Element(sol::table layout); + static std::map> sAllElements; + void updateAttachment(); + }; +} + +#endif // !OPENMW_LUAUI_ELEMENT diff --git a/components/lua_ui/flex.cpp b/components/lua_ui/flex.cpp new file mode 100644 index 0000000000..b54120e78b --- /dev/null +++ b/components/lua_ui/flex.cpp @@ -0,0 +1,106 @@ +#include "flex.hpp" + +namespace LuaUi +{ + void LuaFlex::updateProperties() + { + mHorizontal = propertyValue("horizontal", false); + mAutoSized = propertyValue("autoSize", true); + mAlign = propertyValue("align", Alignment::Start); + mArrange = propertyValue("arrange", Alignment::Start); + WidgetExtension::updateProperties(); + } + + namespace + { + int alignSize(int container, int content, Alignment alignment) + { + int alignedPosition = 0; + { + switch (alignment) + { + case Alignment::Start: + alignedPosition = 0; + break; + case Alignment::Center: + alignedPosition = (container - content) / 2; + break; + case Alignment::End: + alignedPosition = container - content; + break; + } + } + return alignedPosition; + } + + float getGrow(WidgetExtension* w) + { + return std::max(0.0f, w->externalValue("grow", 0.0f)); + } + } + + void LuaFlex::updateChildren() + { + float totalGrow = 0; + MyGUI::IntSize childrenSize; + for (auto* w: children()) + { + w->clearForced(); + MyGUI::IntSize size = w->calculateSize(); + primary(childrenSize) += primary(size); + secondary(childrenSize) = std::max(secondary(childrenSize), secondary(size)); + totalGrow += getGrow(w); + } + mChildrenSize = childrenSize; + + MyGUI::IntSize flexSize = calculateSize(); + int growSize = 0; + float growFactor = 0; + if (totalGrow > 0) + { + growSize = primary(flexSize) - primary(childrenSize); + growFactor = growSize / totalGrow; + } + + MyGUI::IntPoint childPosition; + primary(childPosition) = alignSize(primary(flexSize) - growSize, primary(childrenSize), mAlign); + for (auto* w : children()) + { + MyGUI::IntSize size = w->calculateSize(); + primary(size) += static_cast(growFactor * getGrow(w)); + float stretch = std::clamp(w->externalValue("stretch", 0.0f), 0.0f, 1.0f); + secondary(size) = std::max(secondary(size), static_cast(stretch * secondary(flexSize))); + secondary(childPosition) = alignSize(secondary(flexSize), secondary(size), mArrange); + w->forcePosition(childPosition); + w->forceSize(size); + w->updateCoord(); + primary(childPosition) += primary(size); + } + WidgetExtension::updateChildren(); + } + + MyGUI::IntSize LuaFlex::childScalingSize() + { + // Call the base method to prevent relativeSize feedback loop + MyGUI::IntSize size = WidgetExtension::calculateSize(); + if (mAutoSized) + primary(size) = 0; + return size; + } + + MyGUI::IntSize LuaFlex::calculateSize() + { + MyGUI::IntSize size = WidgetExtension::calculateSize(); + if (mAutoSized) { + primary(size) = std::max(primary(size), primary(mChildrenSize)); + secondary(size) = std::max(secondary(size), secondary(mChildrenSize)); + } + return size; + } + + void LuaFlex::updateCoord() + { + updateChildren(); + WidgetExtension::updateCoord(); + } +} diff --git a/components/lua_ui/flex.hpp b/components/lua_ui/flex.hpp new file mode 100644 index 0000000000..50a3404425 --- /dev/null +++ b/components/lua_ui/flex.hpp @@ -0,0 +1,54 @@ +#ifndef OPENMW_LUAUI_FLEX +#define OPENMW_LUAUI_FLEX + +#include "widget.hpp" +#include "alignment.hpp" + +namespace LuaUi +{ + class LuaFlex : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaFlex) + + protected: + MyGUI::IntSize calculateSize() override; + void updateProperties() override; + void updateChildren() override; + MyGUI::IntSize childScalingSize() override; + + void updateCoord() override; + + private: + bool mHorizontal; + bool mAutoSized; + MyGUI::IntSize mChildrenSize; + Alignment mAlign; + Alignment mArrange; + + template + T& primary(MyGUI::types::TPoint& point) + { + return mHorizontal ? point.left : point.top; + } + + template + T& secondary(MyGUI::types::TPoint& point) + { + return mHorizontal ? point.top : point.left; + } + + template + T& primary(MyGUI::types::TSize& size) + { + return mHorizontal ? size.width : size.height; + } + + template + T& secondary(MyGUI::types::TSize& size) + { + return mHorizontal ? size.height : size.width; + } + }; +} + +#endif // OPENMW_LUAUI_FLEX diff --git a/components/lua_ui/image.cpp b/components/lua_ui/image.cpp new file mode 100644 index 0000000000..8319780b23 --- /dev/null +++ b/components/lua_ui/image.cpp @@ -0,0 +1,76 @@ +#include "image.hpp" + +#include + +#include "resources.hpp" + +namespace LuaUi +{ + void LuaTileRect::_setAlign(const MyGUI::IntSize& _oldsize) + { + mCurrentCoord.set(0, 0, mCroppedParent->getWidth(), mCroppedParent->getHeight()); + mAlign = MyGUI::Align::Stretch; + MyGUI::TileRect::_setAlign(_oldsize); + mTileSize = mSetTileSize; + + // zero tilesize stands for not tiling + if (mTileSize.width == 0) + mTileSize.width = mCoord.width; + if (mTileSize.height == 0) + mTileSize.height = mCoord.height; + + // mCoord could be zero, prevent division by 0 + // use arbitrary large numbers to prevent performance issues + if (mTileSize.width <= 0) + mTileSize.width = 1e7; + if (mTileSize.height <= 0) + mTileSize.height = 1e7; + } + + void LuaImage::initialize() + { + changeWidgetSkin("LuaImage"); + mTileRect = dynamic_cast(getSubWidgetMain()); + WidgetExtension::initialize(); + } + + void LuaImage::updateProperties() + { + deleteAllItems(); + TextureResource* resource = propertyValue("resource", nullptr); + MyGUI::IntCoord atlasCoord; + if (resource) + { + atlasCoord = MyGUI::IntCoord( + static_cast(resource->mOffset.x()), + static_cast(resource->mOffset.y()), + static_cast(resource->mSize.x()), + static_cast(resource->mSize.y())); + setImageTexture(resource->mPath); + } + + bool tileH = propertyValue("tileH", false); + bool tileV = propertyValue("tileV", false); + + MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(_getTextureName()); + MyGUI::IntSize textureSize; + if (texture != nullptr) + textureSize = MyGUI::IntSize(texture->getWidth(), texture->getHeight()); + + mTileRect->updateSize(MyGUI::IntSize( + tileH ? textureSize.width : 0, + tileV ? textureSize.height : 0 + )); + setImageTile(textureSize); + + if (atlasCoord.width == 0) + atlasCoord.width = textureSize.width; + if (atlasCoord.height == 0) + atlasCoord.height = textureSize.height; + setImageCoord(atlasCoord); + + setColour(propertyValue("color", MyGUI::Colour(1,1,1,1))); + + WidgetExtension::updateProperties(); + } +} diff --git a/components/lua_ui/image.hpp b/components/lua_ui/image.hpp new file mode 100644 index 0000000000..f7df102440 --- /dev/null +++ b/components/lua_ui/image.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_LUAUI_IMAGE +#define OPENMW_LUAUI_IMAGE + +#include +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaTileRect : public MyGUI::TileRect + { + MYGUI_RTTI_DERIVED(LuaTileRect) + + public: + void _setAlign(const MyGUI::IntSize& _oldsize) override; + + void updateSize(MyGUI::IntSize tileSize) { mSetTileSize = tileSize; } + + protected: + MyGUI::IntSize mSetTileSize; + }; + + class LuaImage : public MyGUI::ImageBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaImage) + + protected: + void initialize() override; + void updateProperties() override; + LuaTileRect* mTileRect; + }; +} + +#endif // OPENMW_LUAUI_IMAGE diff --git a/components/lua_ui/layers.cpp b/components/lua_ui/layers.cpp new file mode 100644 index 0000000000..645c44f69f --- /dev/null +++ b/components/lua_ui/layers.cpp @@ -0,0 +1,29 @@ +#include "layers.hpp" + +#include + +namespace LuaUi +{ + size_t Layer::indexOf(std::string_view name) + { + for (size_t i = 0; i < count(); i++) + if (at(i)->getName() == name) + return i; + return count(); + } + + void Layer::insert(size_t index, std::string_view name, Options options) + { + if (index > count()) + throw std::logic_error("Invalid layer index"); + if (indexOf(name) < count()) + Log(Debug::Error) << "Layer \"" << name << "\" already exists"; + else + { + auto layer = MyGUI::LayerManager::getInstance() + .createLayerAt(std::string(name), "OverlappedLayer", index); + auto overlappedLayer = dynamic_cast(layer); + overlappedLayer->setPick(options.mInteractive); + } + } +} diff --git a/components/lua_ui/layers.hpp b/components/lua_ui/layers.hpp new file mode 100644 index 0000000000..bb07e13c69 --- /dev/null +++ b/components/lua_ui/layers.hpp @@ -0,0 +1,68 @@ +#ifndef OPENMW_LUAUI_LAYERS +#define OPENMW_LUAUI_LAYERS + +#include +#include + +#include +#include +#include + +namespace LuaUi +{ + // this wrapper is necessary, because the MyGUI LayerManager + // stores layers in a vector and their indices could change + class Layer + { + public: + Layer(size_t index) + : mName(at(index)->getName()) + , mCachedIndex(index) + {} + + const std::string& name() const noexcept { return mName; }; + const osg::Vec2f size() + { + MyGUI::ILayer* p = refresh(); + MyGUI::IntSize size = p->getSize(); + return osg::Vec2f(size.width, size.height); + } + + struct Options + { + bool mInteractive; + }; + + static size_t count() + { + return MyGUI::LayerManager::getInstance().getLayerCount(); + } + + static size_t indexOf(std::string_view name); + + static void insert(size_t index, std::string_view name, Options options); + + private: + static MyGUI::ILayer* at(size_t index) + { + if (index >= count()) + throw std::logic_error("Invalid layer index"); + return MyGUI::LayerManager::getInstance().getLayer(index); + } + + MyGUI::ILayer* refresh() + { + MyGUI::ILayer* p = at(mCachedIndex); + if (p->getName() != mName) + { + mCachedIndex = indexOf(mName); + p = at(mCachedIndex); + } + return p; + } + std::string mName; + size_t mCachedIndex; + }; +} + +#endif // OPENMW_LUAUI_LAYERS diff --git a/components/lua_ui/properties.hpp b/components/lua_ui/properties.hpp new file mode 100644 index 0000000000..ade25156e3 --- /dev/null +++ b/components/lua_ui/properties.hpp @@ -0,0 +1,101 @@ +#ifndef OPENMW_LUAUI_PROPERTIES +#define OPENMW_LUAUI_PROPERTIES + +#include +#include +#include + +#include +#include + +namespace LuaUi +{ + template + constexpr bool isMyGuiVector() { + return + std::is_same() || + std::is_same() || + std::is_same() || + std::is_same(); + } + + template + constexpr bool isMyGuiColor() + { + return std::is_same(); + } + + template + sol::optional parseValue( + sol::object table, + std::string_view field, + std::string_view errorPrefix) + { + sol::object opt = LuaUtil::getFieldOrNil(table, field); + if (opt != sol::nil && !opt.is()) + { + std::string error(errorPrefix); + error += " \""; + error += field; + error += "\" has an invalid value \""; + error += LuaUtil::toString(opt); + error += "\""; + throw std::logic_error(error); + } + if (!opt.is()) + return sol::nullopt; + + LuaT luaT = opt.as(); + if constexpr (isMyGuiVector()) + return T(luaT.x(), luaT.y()); + else if constexpr (isMyGuiColor()) + return T(luaT.r(), luaT.g(), luaT.b(), luaT.a()); + else + return luaT; + } + + template + sol::optional parseValue( + sol::object table, + std::string_view field, + std::string_view errorPrefix) + { + if constexpr (isMyGuiVector()) + return parseValue(table, field, errorPrefix); + else if constexpr (isMyGuiColor()) + return parseValue(table, field, errorPrefix); + else + return parseValue(table, field, errorPrefix); + } + + template + T parseProperty( + sol::object props, + sol::object templateProps, + std::string_view field, + const T& defaultValue) + { + auto propOptional = parseValue(props, field, "Property"); + auto templateOptional = parseValue(templateProps, field, "Template property"); + + if (propOptional.has_value()) + return propOptional.value(); + else if (templateOptional.has_value()) + return templateOptional.value(); + else + return defaultValue; + } + + template + T parseExternal( + sol::object external, + std::string_view field, + const T& defaultValue) + { + auto optional = parseValue(external, field, "External value"); + + return optional.value_or(defaultValue); + } +} + +#endif // !OPENMW_LUAUI_PROPERTIES diff --git a/components/lua_ui/registerscriptsettings.hpp b/components/lua_ui/registerscriptsettings.hpp new file mode 100644 index 0000000000..fb794468da --- /dev/null +++ b/components/lua_ui/registerscriptsettings.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_LUAUI_REGISTERSCRIPTSETTINGS +#define OPENMW_LUAUI_REGISTERSCRIPTSETTINGS + +#include + +namespace LuaUi +{ + // implemented in scriptsettings.cpp + void registerSettingsPage(const sol::table& options); + void clearSettings(); +} + +#endif // !OPENMW_LUAUI_REGISTERSCRIPTSETTINGS diff --git a/components/lua_ui/resources.cpp b/components/lua_ui/resources.cpp new file mode 100644 index 0000000000..0f9890c523 --- /dev/null +++ b/components/lua_ui/resources.cpp @@ -0,0 +1,21 @@ +#include "resources.hpp" + +#include +#include + +namespace LuaUi +{ + std::shared_ptr ResourceManager::registerTexture(TextureData data) + { + data.mPath = mVfs->normalizeFilename(data.mPath); + + TextureResources& list = mTextures[data.mPath]; + list.push_back(std::make_shared(data)); + return list.back(); + } + + void ResourceManager::clear() + { + mTextures.clear(); + } +} diff --git a/components/lua_ui/resources.hpp b/components/lua_ui/resources.hpp new file mode 100644 index 0000000000..cabcd63bf4 --- /dev/null +++ b/components/lua_ui/resources.hpp @@ -0,0 +1,45 @@ +#ifndef OPENMW_LUAUI_RESOURCES +#define OPENMW_LUAUI_RESOURCES + +#include +#include +#include +#include + +#include + +namespace VFS +{ + class Manager; +} + +namespace LuaUi +{ + struct TextureData + { + std::string mPath; + osg::Vec2f mOffset; + osg::Vec2f mSize; + }; + + // will have more/different data when automated atlasing is supported + using TextureResource = TextureData; + + class ResourceManager + { + public: + ResourceManager(const VFS::Manager* vfs) + : mVfs(vfs) + {} + + std::shared_ptr registerTexture(TextureData data); + void clear(); + + private: + const VFS::Manager* mVfs; + using TextureResources = std::vector>; + std::unordered_map mTextures; + }; +} + +#endif // OPENMW_LUAUI_LAYERS diff --git a/components/lua_ui/scriptsettings.cpp b/components/lua_ui/scriptsettings.cpp new file mode 100644 index 0000000000..f2ab84bde0 --- /dev/null +++ b/components/lua_ui/scriptsettings.cpp @@ -0,0 +1,60 @@ +#include "scriptsettings.hpp" + +#include +#include + +#include "registerscriptsettings.hpp" +#include "element.hpp" +#include "adapter.hpp" + +namespace LuaUi +{ + namespace + { + std::vector allPages; + ScriptSettingsPage parse(const sol::table& options) + { + auto name = options.get_or("name", std::string()); + auto searchHints = options.get_or("searchHints", std::string()); + auto element = options.get_or>("element", nullptr); + if (name.empty()) + Log(Debug::Warning) << "A script settings page has an empty name"; + if (!element.get()) + Log(Debug::Warning) << "A script settings page has no UI element assigned"; + return { + name, searchHints, element + }; + } + } + + size_t scriptSettingsPageCount() + { + return allPages.size(); + } + + ScriptSettingsPage scriptSettingsPageAt(size_t index) + { + return parse(allPages[index]); + } + + void registerSettingsPage(const sol::table& options) + { + allPages.push_back(options); + } + + void clearSettings() + { + allPages.clear(); + } + + void attachPageAt(size_t index, LuaAdapter* adapter) + { + if (index < allPages.size()) + { + ScriptSettingsPage page = parse(allPages[index]); + adapter->detach(); + if (page.mElement.get()) + adapter->attach(page.mElement); + } + } +} diff --git a/components/lua_ui/scriptsettings.hpp b/components/lua_ui/scriptsettings.hpp new file mode 100644 index 0000000000..99d43bb518 --- /dev/null +++ b/components/lua_ui/scriptsettings.hpp @@ -0,0 +1,23 @@ +#ifndef OPENMW_LUAUI_SCRIPTSETTINGS +#define OPENMW_LUAUI_SCRIPTSETTINGS + +#include +#include +#include + +namespace LuaUi +{ + class LuaAdapter; + struct Element; + struct ScriptSettingsPage + { + std::string mName; + std::string mSearchHints; + std::shared_ptr mElement; + }; + size_t scriptSettingsPageCount(); + ScriptSettingsPage scriptSettingsPageAt(size_t index); + void attachPageAt(size_t index, LuaAdapter* adapter); +} + +#endif // !OPENMW_LUAUI_SCRIPTSETTINGS diff --git a/components/lua_ui/text.cpp b/components/lua_ui/text.cpp new file mode 100644 index 0000000000..9ec31834f4 --- /dev/null +++ b/components/lua_ui/text.cpp @@ -0,0 +1,55 @@ +#include "text.hpp" + +#include "alignment.hpp" + +namespace LuaUi +{ + LuaText::LuaText() + : mAutoSized(true) + {} + + void LuaText::initialize() + { + changeWidgetSkin("LuaText"); + setEditStatic(true); + setVisibleHScroll(false); + setVisibleVScroll(false); + + WidgetExtension::initialize(); + } + + void LuaText::updateProperties() + { + mAutoSized = propertyValue("autoSize", true); + + setCaption(propertyValue("text", std::string())); + setFontHeight(propertyValue("textSize", 10)); + setTextColour(propertyValue("textColor", MyGUI::Colour(0, 0, 0, 1))); + setEditMultiLine(propertyValue("multiline", false)); + setEditWordWrap(propertyValue("wordWrap", false)); + + Alignment horizontal(propertyValue("textAlignH", Alignment::Start)); + Alignment vertical(propertyValue("textAlignV", Alignment::Start)); + setTextAlign(alignmentToMyGui(horizontal, vertical)); + + setTextShadow(propertyValue("textShadow", false)); + setTextShadowColour(propertyValue("textShadowColor", MyGUI::Colour(0, 0, 0, 1))); + + WidgetExtension::updateProperties(); + } + + void LuaText::setCaption(const MyGUI::UString& caption) + { + MyGUI::TextBox::setCaption(caption); + if (mAutoSized) + updateCoord(); + } + + MyGUI::IntSize LuaText::calculateSize() + { + if (mAutoSized) + return getTextSize(); + else + return WidgetExtension::calculateSize(); + } +} diff --git a/components/lua_ui/text.hpp b/components/lua_ui/text.hpp new file mode 100644 index 0000000000..e42b6f9935 --- /dev/null +++ b/components/lua_ui/text.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_LUAUI_TEXT +#define OPENMW_LUAUI_TEXT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaText : public MyGUI::EditBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaText) + + public: + LuaText(); + void initialize() override; + void updateProperties() override; + void setCaption(const MyGUI::UString& caption) override; + + private: + bool mAutoSized; + + protected: + MyGUI::IntSize calculateSize() override; + }; +} + +#endif // OPENMW_LUAUI_TEXT diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp new file mode 100644 index 0000000000..c6fda38416 --- /dev/null +++ b/components/lua_ui/textedit.cpp @@ -0,0 +1,76 @@ +#include "textedit.hpp" + +#include "alignment.hpp" + +namespace LuaUi +{ + void LuaTextEdit::initialize() + { + mEditBox = createWidget("LuaTextEdit", MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); + mEditBox->eventEditTextChange += MyGUI::newDelegate(this, &LuaTextEdit::textChange); + registerEvents(mEditBox); + WidgetExtension::initialize(); + } + + void LuaTextEdit::deinitialize() + { + mEditBox->eventEditTextChange -= MyGUI::newDelegate(this, &LuaTextEdit::textChange); + clearEvents(mEditBox); + WidgetExtension::deinitialize(); + } + + void LuaTextEdit::updateProperties() + { + mEditBox->setFontHeight(propertyValue("textSize", 10)); + mEditBox->setTextColour(propertyValue("textColor", MyGUI::Colour(0, 0, 0, 1))); + mEditBox->setEditWordWrap(propertyValue("wordWrap", false)); + + Alignment horizontal(propertyValue("textAlignH", Alignment::Start)); + Alignment vertical(propertyValue("textAlignV", Alignment::Start)); + mEditBox->setTextAlign(alignmentToMyGui(horizontal, vertical)); + + mMultiline = propertyValue("multiline", false); + mEditBox->setEditMultiLine(mMultiline); + + bool readOnly = propertyValue("readOnly", false); + mEditBox->setEditStatic(readOnly); + + mAutoSize = (readOnly || !mMultiline) && propertyValue("autoSize", false); + + // change caption last, for multiline and wordwrap to apply + mEditBox->setCaption(propertyValue("text", std::string())); + + WidgetExtension::updateProperties(); + } + + void LuaTextEdit::textChange(MyGUI::EditBox*) + { + triggerEvent("textChanged", sol::make_object(lua(), mEditBox->getCaption().asUTF8())); + } + + void LuaTextEdit::updateCoord() + { + WidgetExtension::updateCoord(); + mEditBox->setSize(widget()->getSize()); + } + + void LuaTextEdit::updateChildren() + { + WidgetExtension::updateChildren(); + // otherwise it won't be focusable + mEditBox->detachFromWidget(); + mEditBox->attachToWidget(this); + } + + MyGUI::IntSize LuaTextEdit::calculateSize() + { + MyGUI::IntSize normalSize = WidgetExtension::calculateSize(); + if (mAutoSize) + { + mEditBox->setSize(normalSize); + int targetHeight = mMultiline ? mEditBox->getTextSize().height : mEditBox->getFontHeight(); + normalSize.height = std::max(normalSize.height, targetHeight); + } + return normalSize; + } +} diff --git a/components/lua_ui/textedit.hpp b/components/lua_ui/textedit.hpp new file mode 100644 index 0000000000..a9b8de1621 --- /dev/null +++ b/components/lua_ui/textedit.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_LUAUI_TEXTEDIT +#define OPENMW_LUAUI_TEXTEDIT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaTextEdit : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaTextEdit) + + protected: + void initialize() override; + void deinitialize() override; + void updateProperties() override; + void updateCoord() override; + void updateChildren() override; + MyGUI::IntSize calculateSize() override; + + private: + void textChange(MyGUI::EditBox*); + + MyGUI::EditBox* mEditBox = nullptr; + bool mMultiline{false}; + bool mAutoSize{false}; + }; +} + +#endif // OPENMW_LUAUI_TEXTEDIT diff --git a/components/lua_ui/util.cpp b/components/lua_ui/util.cpp new file mode 100644 index 0000000000..f3cb0d288c --- /dev/null +++ b/components/lua_ui/util.cpp @@ -0,0 +1,53 @@ +#include "util.hpp" + +#include + +#include "adapter.hpp" +#include "widget.hpp" +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" +#include "image.hpp" +#include "container.hpp" +#include "flex.hpp" + +#include "element.hpp" +#include "registerscriptsettings.hpp" + +namespace LuaUi +{ + + void registerAllWidgets() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("BasisSkin"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + + const std::unordered_map& widgetTypeToName() + { + static std::unordered_map types{ + { "LuaWidget", "Widget" }, + { "LuaText", "Text" }, + { "LuaTextEdit", "TextEdit" }, + { "LuaWindow", "Window" }, + { "LuaImage", "Image" }, + { "LuaFlex", "Flex" }, + { "LuaContainer", "Container" }, + }; + return types; + } + + void clearUserInterface() + { + clearSettings(); + while (!Element::sAllElements.empty()) + Element::sAllElements.begin()->second->destroy(); + } +} diff --git a/components/lua_ui/util.hpp b/components/lua_ui/util.hpp new file mode 100644 index 0000000000..3851e6c947 --- /dev/null +++ b/components/lua_ui/util.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_LUAUI_WIDGETLIST +#define OPENMW_LUAUI_WIDGETLIST + +#include +#include + +namespace LuaUi +{ + void registerAllWidgets(); + + const std::unordered_map& widgetTypeToName(); + + void clearUserInterface(); +} + +#endif // OPENMW_LUAUI_WIDGETLIST diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp new file mode 100644 index 0000000000..e3f45d90dd --- /dev/null +++ b/components/lua_ui/widget.cpp @@ -0,0 +1,412 @@ +#include "widget.hpp" + +#include +#include + +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" + +namespace LuaUi +{ + WidgetExtension::WidgetExtension() + : mForcePosition(false) + , mForceSize(false) + , mPropagateEvents(true) + , mLua(nullptr) + , mWidget(nullptr) + , mSlot(this) + , mLayout(sol::nil) + , mProperties(sol::nil) + , mTemplateProperties(sol::nil) + , mExternal(sol::nil) + , mParent(nullptr) + , mTemplateChild(false) + {} + + void WidgetExtension::initialize(lua_State* lua, MyGUI::Widget* self) + { + mLua = lua; + mWidget = self; + initialize(); + updateTemplate(); + } + + void WidgetExtension::initialize() + { + // \todo might be more efficient to only register these if there are Lua callbacks + registerEvents(mWidget); + } + + void WidgetExtension::deinitialize() + { + clearCallbacks(); + clearEvents(mWidget); + + mOnCoordChange.reset(); + + for (WidgetExtension* w : mChildren) + w->deinitialize(); + for (WidgetExtension* w : mTemplateChildren) + w->deinitialize(); + } + + void WidgetExtension::registerEvents(MyGUI::Widget* w) + { + w->eventKeyButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::keyPress); + w->eventKeyButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::keyRelease); + w->eventMouseButtonClick += MyGUI::newDelegate(this, &WidgetExtension::mouseClick); + w->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WidgetExtension::mouseDoubleClick); + w->eventMouseButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::mousePress); + w->eventMouseButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::mouseRelease); + w->eventMouseMove += MyGUI::newDelegate(this, &WidgetExtension::mouseMove); + w->eventMouseDrag += MyGUI::newDelegate(this, &WidgetExtension::mouseDrag); + + w->eventMouseSetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + w->eventMouseLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + w->eventKeySetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + w->eventKeyLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + } + void WidgetExtension::clearEvents(MyGUI::Widget* w) + { + w->eventKeyButtonPressed.clear(); + w->eventKeyButtonReleased.clear(); + w->eventMouseButtonClick.clear(); + w->eventMouseButtonDoubleClick.clear(); + w->eventMouseButtonPressed.clear(); + w->eventMouseButtonReleased.clear(); + w->eventMouseMove.clear(); + w->eventMouseDrag.m_event.clear(); + + w->eventMouseSetFocus.clear(); + w->eventMouseLostFocus.clear(); + w->eventKeySetFocus.clear(); + w->eventKeyLostFocus.clear(); + } + + void WidgetExtension::reset() + { + // detach all children from the slot widget, in case it gets destroyed + for (auto& w: mChildren) + w->widget()->detachFromWidget(); + } + + void WidgetExtension::attach(WidgetExtension* ext) + { + ext->mParent = this; + ext->mTemplateChild = false; + ext->widget()->attachToWidget(mSlot->widget()); + // workaround for MyGUI bug + // parent visibility doesn't affect added children + ext->widget()->setVisible(!ext->widget()->getVisible()); + ext->widget()->setVisible(!ext->widget()->getVisible()); + } + + void WidgetExtension::attachTemplate(WidgetExtension* ext) + { + ext->mParent = this; + ext->mTemplateChild = true; + ext->widget()->attachToWidget(widget()); + // workaround for MyGUI bug + // parent visibility doesn't affect added children + ext->widget()->setVisible(!ext->widget()->getVisible()); + ext->widget()->setVisible(!ext->widget()->getVisible()); + } + + WidgetExtension* WidgetExtension::findDeep(std::string_view flagName) + { + for (WidgetExtension* w : mChildren) + { + WidgetExtension* result = w->findDeep(flagName); + if (result != nullptr) + return result; + } + if (externalValue(flagName, false)) + return this; + return nullptr; + } + + void WidgetExtension::findAll(std::string_view flagName, std::vector& result) + { + if (externalValue(flagName, false)) + result.push_back(this); + for (WidgetExtension* w : mChildren) + w->findAll(flagName, result); + } + + WidgetExtension* WidgetExtension::findDeepInTemplates(std::string_view flagName) + { + for (WidgetExtension* w : mTemplateChildren) + { + WidgetExtension* result = w->findDeep(flagName); + if (result != nullptr) + return result; + } + return nullptr; + } + + std::vector WidgetExtension::findAllInTemplates(std::string_view flagName) + { + std::vector result; + for (WidgetExtension* w : mTemplateChildren) + w->findAll(flagName, result); + return result; + } + + sol::table WidgetExtension::makeTable() const + { + return sol::table(lua(), sol::create); + } + + sol::object WidgetExtension::keyEvent(MyGUI::KeyCode code) const + { + auto keySym = SDL_Keysym(); + keySym.sym = SDLUtil::myGuiKeyToSdl(code); + keySym.scancode = SDL_GetScancodeFromKey(keySym.sym); + keySym.mod = SDL_GetModState(); + return sol::make_object(lua(), keySym); + } + + sol::object WidgetExtension::mouseEvent(int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const + { + osg::Vec2f position(left, top); + MyGUI::IntPoint absolutePosition = mWidget->getAbsolutePosition(); + osg::Vec2f offset = position - osg::Vec2f(absolutePosition.left, absolutePosition.top); + sol::table table = makeTable(); + int sdlButton = SDLUtil::myGuiMouseButtonToSdl(button); + table["position"] = position; + table["offset"] = offset; + if (sdlButton != 0) // nil if no button was pressed + table["button"] = sdlButton; + return table; + } + + void WidgetExtension::setChildren(const std::vector& children) + { + mChildren.resize(children.size()); + for (size_t i = 0; i < children.size(); ++i) + { + mChildren[i] = children[i]; + attach(mChildren[i]); + } + updateChildren(); + } + + void WidgetExtension::setTemplateChildren(const std::vector& children) + { + mTemplateChildren.resize(children.size()); + for (size_t i = 0; i < children.size(); ++i) + { + mTemplateChildren[i] = children[i]; + attachTemplate(mTemplateChildren[i]); + } + updateTemplate(); + } + + void WidgetExtension::updateTemplate() + { + WidgetExtension* slot = findDeepInTemplates("slot"); + if (slot == nullptr) + mSlot = this; + else + mSlot = slot->mSlot; + } + + void WidgetExtension::setCallback(const std::string& name, const LuaUtil::Callback& callback) + { + mCallbacks[name] = callback; + } + + void WidgetExtension::clearCallbacks() + { + mCallbacks.clear(); + } + + MyGUI::IntCoord WidgetExtension::forcedCoord() + { + return mForcedCoord; + } + + void WidgetExtension::forceCoord(const MyGUI::IntCoord& offset) + { + mForcePosition = true; + mForceSize = true; + mForcedCoord = offset; + } + + void WidgetExtension::forcePosition(const MyGUI::IntPoint& pos) + { + mForcePosition = true; + mForcedCoord = pos; + } + + void WidgetExtension::forceSize(const MyGUI::IntSize& size) + { + mForceSize = true; + mForcedCoord = size; + } + + void WidgetExtension::clearForced() { + mForcePosition = false; + mForceSize = false; + } + + void WidgetExtension::updateCoord() + { + MyGUI::IntCoord oldCoord = mWidget->getCoord(); + MyGUI::IntCoord newCoord = calculateCoord(); + + if (oldCoord != newCoord) + mWidget->setCoord(newCoord); + updateChildrenCoord(); + if (oldCoord != newCoord && mOnCoordChange.has_value()) + mOnCoordChange.value()(this, newCoord); + } + + void WidgetExtension::setProperties(sol::object props) + { + mProperties = props; + updateProperties(); + } + + void WidgetExtension::updateProperties() + { + mPropagateEvents = propertyValue("propagateEvents", true); + mAbsoluteCoord = propertyValue("position", MyGUI::IntPoint()); + mAbsoluteCoord = propertyValue("size", MyGUI::IntSize()); + mRelativeCoord = propertyValue("relativePosition", MyGUI::FloatPoint()); + mRelativeCoord = propertyValue("relativeSize", MyGUI::FloatSize()); + mAnchor = propertyValue("anchor", MyGUI::FloatSize()); + mWidget->setVisible(propertyValue("visible", true)); + mWidget->setPointer(propertyValue("pointer", std::string("arrow"))); + mWidget->setAlpha(propertyValue("alpha", 1.f)); + mWidget->setInheritsAlpha(propertyValue("inheritAlpha", true)); + } + + void WidgetExtension::updateChildrenCoord() + { + for (WidgetExtension* w : mTemplateChildren) + w->updateCoord(); + for (WidgetExtension* w : mChildren) + w->updateCoord(); + } + + MyGUI::IntSize WidgetExtension::parentSize() + { + if (!mParent) + return widget()->getParentSize(); // size of the layer + if (mTemplateChild) + return mParent->templateScalingSize(); + else + return mParent->childScalingSize(); + } + + MyGUI::IntSize WidgetExtension::calculateSize() + { + if (mForceSize) + return mForcedCoord.size(); + + MyGUI::IntSize pSize = parentSize(); + MyGUI::IntSize newSize; + newSize = mAbsoluteCoord.size(); + newSize.width += mRelativeCoord.width * pSize.width; + newSize.height += mRelativeCoord.height * pSize.height; + return newSize; + } + + MyGUI::IntPoint WidgetExtension::calculatePosition(const MyGUI::IntSize& size) + { + if (mForcePosition) + return mForcedCoord.point(); + MyGUI::IntSize pSize = parentSize(); + MyGUI::IntPoint newPosition; + newPosition = mAbsoluteCoord.point(); + newPosition.left += mRelativeCoord.left * pSize.width - mAnchor.width * size.width; + newPosition.top += mRelativeCoord.top * pSize.height - mAnchor.height * size.height; + return newPosition; + } + + MyGUI::IntCoord WidgetExtension::calculateCoord() + { + MyGUI::IntCoord newCoord; + newCoord = calculateSize(); + newCoord = calculatePosition(newCoord.size()); + return newCoord; + } + + MyGUI::IntSize WidgetExtension::childScalingSize() + { + return mSlot->widget()->getSize(); + } + + MyGUI::IntSize WidgetExtension::templateScalingSize() + { + return widget()->getSize(); + } + + void WidgetExtension::triggerEvent(std::string_view name, sol::object argument) const + { + auto it = mCallbacks.find(name); + if (it != mCallbacks.end()) + it->second.call(argument, mLayout); + } + + void WidgetExtension::keyPress(MyGUI::Widget*, MyGUI::KeyCode code, MyGUI::Char ch) + { + if (code == MyGUI::KeyCode::None) + { + propagateEvent("textInput", [ch](auto w) { + MyGUI::UString uString; + uString.push_back(static_cast(ch)); + return sol::make_object(w->lua(), uString.asUTF8()); + }); + } + else + propagateEvent("keyPress", [code](auto w){ return w->keyEvent(code); }); + } + + void WidgetExtension::keyRelease(MyGUI::Widget*, MyGUI::KeyCode code) + { + propagateEvent("keyRelease", [code](auto w) { return w->keyEvent(code); }); + } + + void WidgetExtension::mouseMove(MyGUI::Widget*, int left, int top) + { + propagateEvent("mouseMove", [left, top](auto w) { return w->mouseEvent(left, top); }); + } + + void WidgetExtension::mouseDrag(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + propagateEvent("mouseMove", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + } + + void WidgetExtension::mouseClick(MyGUI::Widget* _widget) + { + propagateEvent("mouseClick", [](auto){ return sol::nil; }); + } + + void WidgetExtension::mouseDoubleClick(MyGUI::Widget* _widget) + { + propagateEvent("mouseDoubleClick", [](auto){ return sol::nil; }); + } + + void WidgetExtension::mousePress(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + propagateEvent("mousePress", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + } + + void WidgetExtension::mouseRelease(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + propagateEvent("mouseRelease", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + } + + void WidgetExtension::focusGain(MyGUI::Widget*, MyGUI::Widget*) + { + propagateEvent("focusGain", [](auto){ return sol::nil; }); + } + + void WidgetExtension::focusLoss(MyGUI::Widget*, MyGUI::Widget*) + { + propagateEvent("focusLoss", [](auto){ return sol::nil; }); + } +} diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp new file mode 100644 index 0000000000..3285f629ac --- /dev/null +++ b/components/lua_ui/widget.hpp @@ -0,0 +1,181 @@ +#ifndef OPENMW_LUAUI_WIDGET +#define OPENMW_LUAUI_WIDGET + +#include +#include + +#include +#include + +#include + +#include "properties.hpp" + +namespace LuaUi +{ + /* + * extends MyGUI::Widget and its child classes + * memory ownership is controlled by MyGUI + * it is important not to call any WidgetExtension methods after destroying the MyGUI::Widget + */ + class WidgetExtension + { + public: + WidgetExtension(); + // must be called after creating the underlying MyGUI::Widget + void initialize(lua_State* lua, MyGUI::Widget* self); + // must be called after before destroying the underlying MyGUI::Widget + virtual void deinitialize(); + + MyGUI::Widget* widget() const { return mWidget; } + WidgetExtension* slot() const { return mSlot; } + + void reset(); + + const std::vector& children() { return mChildren; } + void setChildren(const std::vector&); + + const std::vector& templateChildren() { return mTemplateChildren; } + void setTemplateChildren(const std::vector&); + + void setCallback(const std::string&, const LuaUtil::Callback&); + void clearCallbacks(); + + void setProperties(sol::object); + void setTemplateProperties(sol::object props) { mTemplateProperties = props; } + + void setExternal(sol::object external) { mExternal = external; } + + MyGUI::IntCoord forcedCoord(); + void forceCoord(const MyGUI::IntCoord& offset); + void forceSize(const MyGUI::IntSize& size); + void forcePosition(const MyGUI::IntPoint& pos); + void clearForced(); + + virtual void updateCoord(); + + const sol::table& getLayout() { return mLayout; } + void setLayout(const sol::table& layout) { mLayout = layout; } + + template + T externalValue(std::string_view name, const T& defaultValue) + { + return parseExternal(mExternal, name, defaultValue); + } + + void onCoordChange(const std::optional>& callback) + { + mOnCoordChange = callback; + } + + virtual MyGUI::IntSize calculateSize(); + virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); + MyGUI::IntCoord calculateCoord(); + + protected: + virtual void initialize(); + void registerEvents(MyGUI::Widget* w); + void clearEvents(MyGUI::Widget* w); + + sol::table makeTable() const; + sol::object keyEvent(MyGUI::KeyCode) const; + sol::object mouseEvent(int left, int top, MyGUI::MouseButton button) const; + + MyGUI::IntSize parentSize(); + virtual MyGUI::IntSize childScalingSize(); + virtual MyGUI::IntSize templateScalingSize(); + + template + T propertyValue(std::string_view name, const T& defaultValue) + { + return parseProperty(mProperties, mTemplateProperties, name, defaultValue); + } + + WidgetExtension* findDeepInTemplates(std::string_view flagName); + std::vector findAllInTemplates(std::string_view flagName); + + virtual void updateTemplate(); + virtual void updateProperties(); + virtual void updateChildren() {}; + + lua_State* lua() const { return mLua; } + + void triggerEvent(std::string_view name, sol::object argument) const; + template + void propagateEvent(std::string_view name, const ArgFactory& argumentFactory) const + { + const WidgetExtension* w = this; + while (w) + { + bool shouldPropagate = true; + auto it = w->mCallbacks.find(name); + if (it != w->mCallbacks.end()) + { + sol::object res = it->second.call(argumentFactory(w), w->mLayout); + shouldPropagate = res.is() && res.as(); + } + if (w->mParent && w->mPropagateEvents && shouldPropagate) + w = w->mParent; + else + w = nullptr; + } + } + + bool mForcePosition; + bool mForceSize; + // offsets the position and size, used only in C++ widget code + MyGUI::IntCoord mForcedCoord; + // position and size in pixels + MyGUI::IntCoord mAbsoluteCoord; + // position and size as a ratio of parent size + MyGUI::FloatCoord mRelativeCoord; + // negative position offset as a ratio of this widget's size + // used in combination with relative coord to align the widget, e. g. center it + MyGUI::FloatSize mAnchor; + + bool mPropagateEvents; + + private: + // use lua_State* instead of sol::state_view because MyGUI requires a default constructor + lua_State* mLua; + MyGUI::Widget* mWidget; + std::vector mChildren; + std::vector mTemplateChildren; + WidgetExtension* mSlot; + std::map> mCallbacks; + sol::table mLayout; + sol::object mProperties; + sol::object mTemplateProperties; + sol::object mExternal; + WidgetExtension* mParent; + bool mTemplateChild; + + void attach(WidgetExtension* ext); + void attachTemplate(WidgetExtension* ext); + + WidgetExtension* findDeep(std::string_view name); + void findAll(std::string_view flagName, std::vector& result); + + void updateChildrenCoord(); + + void keyPress(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char); + void keyRelease(MyGUI::Widget*, MyGUI::KeyCode); + void mouseMove(MyGUI::Widget*, int, int); + void mouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseClick(MyGUI::Widget*); + void mouseDoubleClick(MyGUI::Widget*); + void mousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseRelease(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void focusGain(MyGUI::Widget*, MyGUI::Widget*); + void focusLoss(MyGUI::Widget*, MyGUI::Widget*); + + std::optional> mOnCoordChange; + }; + + class LuaWidget : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWidget) + }; +} + +#endif // !OPENMW_LUAUI_WIDGET diff --git a/components/lua_ui/window.cpp b/components/lua_ui/window.cpp new file mode 100644 index 0000000000..5dbf3f5219 --- /dev/null +++ b/components/lua_ui/window.cpp @@ -0,0 +1,86 @@ +#include "window.hpp" + +#include +#include +#include + +namespace LuaUi +{ + LuaWindow::LuaWindow() + : mCaption(nullptr) + {} + + void LuaWindow::updateTemplate() + { + for (auto& [w, _] : mActionWidgets) + { + w->eventMouseButtonPressed.clear(); + w->eventMouseDrag.m_event.clear(); + } + mActionWidgets.clear(); + + WidgetExtension* captionWidget = findDeepInTemplates("caption"); + mCaption = dynamic_cast(captionWidget); + + if (mCaption) + mActionWidgets.emplace(mCaption->widget(), mCaption); + for (WidgetExtension* ext : findAllInTemplates("action")) + mActionWidgets.emplace(ext->widget(), ext); + + for (auto& [w, _] : mActionWidgets) + { + w->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress); + w->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag); + } + + WidgetExtension::updateTemplate(); + } + + void LuaWindow::updateProperties() + { + if (mCaption) + mCaption->setCaption(propertyValue("caption", std::string())); + mMoveResize = MyGUI::IntCoord(); + clearForced(); + WidgetExtension::updateProperties(); + } + + void LuaWindow::notifyMousePress(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + WidgetExtension* ext = mActionWidgets[sender]; + + mChangeScale = MyGUI::IntCoord( + ext->externalValue("move", MyGUI::IntPoint(1, 1)), + ext->externalValue("resize", MyGUI::IntSize(0, 0))); + } + + void LuaWindow::notifyMouseDrag(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + MyGUI::IntCoord change = mChangeScale; + change.left *= (left - mPreviousMouse.left); + change.top *= (top - mPreviousMouse.top); + change.width *= (left - mPreviousMouse.left); + change.height *= (top - mPreviousMouse.top); + + mMoveResize = mMoveResize + change; + forceCoord(mMoveResize); + updateCoord(); + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + sol::table table = makeTable(); + table["position"] = osg::Vec2f(mCoord.left, mCoord.top); + table["size"] = osg::Vec2f(mCoord.width, mCoord.height); + triggerEvent("windowDrag", table); + } +} diff --git a/components/lua_ui/window.hpp b/components/lua_ui/window.hpp new file mode 100644 index 0000000000..8c466e73d1 --- /dev/null +++ b/components/lua_ui/window.hpp @@ -0,0 +1,34 @@ +#ifndef OPENMW_LUAUI_WINDOW +#define OPENMW_LUAUI_WINDOW + +#include + +#include "widget.hpp" +#include "text.hpp" + +namespace LuaUi +{ + class LuaWindow : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWindow) + + public: + LuaWindow(); + void updateTemplate() override; + void updateProperties() override; + + private: + LuaText* mCaption; + std::map mActionWidgets; + MyGUI::IntPoint mPreviousMouse; + MyGUI::IntCoord mChangeScale; + + MyGUI::IntCoord mMoveResize; + + protected: + void notifyMousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void notifyMouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + }; +} + +#endif // OPENMW_LUAUI_WINDOW diff --git a/components/misc/algorithm.hpp b/components/misc/algorithm.hpp new file mode 100644 index 0000000000..54ac74e97e --- /dev/null +++ b/components/misc/algorithm.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_MISC_ALGORITHM_H +#define OPENMW_COMPONENTS_MISC_ALGORITHM_H + +#include +#include + +#include "stringops.hpp" + +namespace Misc +{ + template + inline Iterator forEachUnique(Iterator begin, Iterator end, BinaryPredicate predicate, Function function) + { + static_assert( + std::is_base_of_v< + std::forward_iterator_tag, + typename std::iterator_traits::iterator_category + > + ); + if (begin == end) + return begin; + function(*begin); + auto last = begin; + ++begin; + while (begin != end) + { + if (!predicate(*begin, *last)) + { + function(*begin); + last = begin; + } + ++begin; + } + return begin; + } + + /// Performs a binary search on a sorted container for a string that 'key' starts with + template + static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + { + const Iterator notFound = end; + + while(begin < end) + { + const Iterator middle = begin + (std::distance(begin, end) / 2); + + int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); + + if(comp == 0) + return middle; + else if(comp > 0) + end = middle; + else + begin = middle + 1; + } + + return notFound; + } + +} + +#endif diff --git a/components/misc/barrier.hpp b/components/misc/barrier.hpp index a5af9f5650..9738fbb7fa 100644 --- a/components/misc/barrier.hpp +++ b/components/misc/barrier.hpp @@ -2,7 +2,6 @@ #define OPENMW_BARRIER_H #include -#include #include namespace Misc @@ -11,25 +10,25 @@ namespace Misc class Barrier { public: - using BarrierCallback = std::function; /// @param count number of threads to wait on - /// @param func callable to be executed once after all threads have met - Barrier(int count, BarrierCallback&& func) : mThreadCount(count), mRendezvousCount(0), mGeneration(0) - , mFunc(std::forward(func)) - {} + explicit Barrier(unsigned count) : mThreadCount(count), mRendezvousCount(0), mGeneration(0) + { + } /// @brief stop execution of threads until count distinct threads reach this point - void wait() + /// @param func callable to be executed once after all threads have met + template + void wait(Callback&& func) { std::unique_lock lock(mMutex); ++mRendezvousCount; - const int currentGeneration = mGeneration; - if (mRendezvousCount == mThreadCount) + const unsigned int currentGeneration = mGeneration; + if (mRendezvousCount == mThreadCount || mThreadCount == 0) { ++mGeneration; mRendezvousCount = 0; - mFunc(); + func(); mRendezvous.notify_all(); } else @@ -39,12 +38,11 @@ namespace Misc } private: - int mThreadCount; - int mRendezvousCount; - int mGeneration; + unsigned int mThreadCount; + unsigned int mRendezvousCount; + unsigned int mGeneration; mutable std::mutex mMutex; std::condition_variable mRendezvous; - BarrierCallback mFunc; }; } diff --git a/components/misc/budgetmeasurement.hpp b/components/misc/budgetmeasurement.hpp new file mode 100644 index 0000000000..3d56477af1 --- /dev/null +++ b/components/misc/budgetmeasurement.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_COMPONENTS_MISC_BUDGETMEASUREMENT_H +#define OPENMW_COMPONENTS_MISC_BUDGETMEASUREMENT_H + + +namespace Misc +{ + +class BudgetMeasurement +{ + std::array mBudgetHistory; + std::array mBudgetStepCount; + +public: + BudgetMeasurement(const float default_expense) + { + mBudgetHistory = {default_expense, default_expense, default_expense, default_expense}; + mBudgetStepCount = {1, 1, 1, 1}; + } + + void reset(const float default_expense) + { + mBudgetHistory = {default_expense, default_expense, default_expense, default_expense}; + mBudgetStepCount = {1, 1, 1, 1}; + } + + void update(double delta, unsigned int stepCount, size_t cursor) + { + mBudgetHistory[cursor%4] = delta; + mBudgetStepCount[cursor%4] = stepCount; + } + + double get() const + { + float sum = (mBudgetHistory[0] + mBudgetHistory[1] + mBudgetHistory[2] + mBudgetHistory[3]); + unsigned int stepCountSum = (mBudgetStepCount[0] + mBudgetStepCount[1] + mBudgetStepCount[2] + mBudgetStepCount[3]); + return sum/float(stepCountSum); + } +}; + +} + +#endif diff --git a/components/misc/color.cpp b/components/misc/color.cpp new file mode 100644 index 0000000000..56520c5d74 --- /dev/null +++ b/components/misc/color.cpp @@ -0,0 +1,59 @@ +#include "color.hpp" + +#include +#include +#include +#include + +namespace Misc +{ + Color::Color(float r, float g, float b, float a) + : mR(std::clamp(r, 0.f, 1.f)) + , mG(std::clamp(g, 0.f, 1.f)) + , mB(std::clamp(b, 0.f, 1.f)) + , mA(std::clamp(a, 0.f, 1.f)) + {} + + std::string Color::toString() const + { + std::ostringstream ss; + ss << "(" << r() << ", " << g() << ", " << b() << ", " << a() << ')'; + return ss.str(); + } + + Color Color::fromHex(std::string_view hex) + { + if (hex.size() != 6) + throw std::logic_error(std::string("Invalid hex color: ") += hex); + std::array rgb; + for (size_t i = 0; i < rgb.size(); i++) + { + auto sub = hex.substr(i * 2, 2); + int v = 0; + auto [_, ec] = std::from_chars(sub.data(), sub.data() + sub.size(), v, 16); + if (ec != std::errc()) + throw std::logic_error(std::string("Invalid hex color: ") += hex); + rgb[i] = v / 255.0f; + } + return Color(rgb[0], rgb[1], rgb[2], 1); + } + + std::string Color::toHex() const + { + std::string result(6, '0'); + std::array rgb = { mR, mG, mB }; + for (size_t i = 0; i < rgb.size(); i++) + { + int b = static_cast(rgb[i] * 255.0f); + auto [_, ec] = std::to_chars(result.data() + i * 2, result.data() + (i + 1) * 2, b, 16); + if (ec != std::errc()) + throw std::logic_error("Error when converting number to base 16"); + } + return result; + } + + bool operator==(const Color& l, const Color& r) + { + return l.mR == r.mR && l.mG == r.mG && l.mB == r.mB && l.mA == r.mA; + } +} diff --git a/components/misc/color.hpp b/components/misc/color.hpp new file mode 100644 index 0000000000..932d261fad --- /dev/null +++ b/components/misc/color.hpp @@ -0,0 +1,34 @@ +#ifndef COMPONENTS_MISC_COLOR +#define COMPONENTS_MISC_COLOR + +#include + +namespace Misc +{ + class Color + { + public: + Color(float r, float g, float b, float a); + + float r() const { return mR; } + float g() const { return mG; } + float b() const { return mB; } + float a() const { return mA; } + + std::string toString() const; + + static Color fromHex(std::string_view hex); + + std::string toHex() const; + + friend bool operator==(const Color& l, const Color& r); + + private: + float mR; + float mG; + float mB; + float mA; + }; +} + +#endif // !COMPONENTS_MISC_COLOR diff --git a/components/misc/compression.cpp b/components/misc/compression.cpp new file mode 100644 index 0000000000..7f76d0900c --- /dev/null +++ b/components/misc/compression.cpp @@ -0,0 +1,48 @@ +#include "compression.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace Misc +{ + std::vector compress(const std::vector& data) + { + const std::size_t originalSize = data.size(); + std::vector result(static_cast(LZ4_compressBound(static_cast(originalSize)) + sizeof(originalSize))); + const int size = LZ4_compress_default( + reinterpret_cast(data.data()), + reinterpret_cast(result.data()) + sizeof(originalSize), + static_cast(data.size()), + static_cast(result.size() - sizeof(originalSize)) + ); + if (size == 0) + throw std::runtime_error("Failed to compress"); + std::memcpy(result.data(), &originalSize, sizeof(originalSize)); + result.resize(static_cast(size) + sizeof(originalSize)); + return result; + } + + std::vector decompress(const std::vector& data) + { + std::size_t originalSize; + std::memcpy(&originalSize, data.data(), sizeof(originalSize)); + std::vector result(originalSize); + const int size = LZ4_decompress_safe( + reinterpret_cast(data.data()) + sizeof(originalSize), + reinterpret_cast(result.data()), + static_cast(data.size() - sizeof(originalSize)), + static_cast(result.size()) + ); + if (size < 0) + throw std::runtime_error("Failed to decompress"); + if (originalSize != static_cast(size)) + throw std::runtime_error("Size of decompressed data (" + std::to_string(size) + + ") doesn't match stored (" + std::to_string(originalSize) + ")"); + return result; + } +} diff --git a/components/misc/compression.hpp b/components/misc/compression.hpp new file mode 100644 index 0000000000..2b951ebed6 --- /dev/null +++ b/components/misc/compression.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_MISC_COMPRESSION_H +#define OPENMW_COMPONENTS_MISC_COMPRESSION_H + +#include +#include + +namespace Misc +{ + std::vector compress(const std::vector& data); + + std::vector decompress(const std::vector& data); +} + +#endif diff --git a/components/misc/constants.hpp b/components/misc/constants.hpp index 1053b1c560..2f4d4f8a18 100644 --- a/components/misc/constants.hpp +++ b/components/misc/constants.hpp @@ -33,6 +33,15 @@ const std::string NightDayLabel = "NightDaySwitch"; // A label to mark visual switches for herbalism feature const std::string HerbalismLabel = "HerbalismSwitch"; +// Percentage height at which projectiles are spawned from an actor +const float TorsoHeight = 0.75f; + +static constexpr float sStepSizeUp = 34.0f; +static constexpr float sMaxSlope = 46.0f; + +// Identifier for main scene camera +const std::string SceneCamera = "SceneCam"; + } #endif diff --git a/components/misc/convert.hpp b/components/misc/convert.hpp index c5784d33ae..0b84c32059 100644 --- a/components/misc/convert.hpp +++ b/components/misc/convert.hpp @@ -1,7 +1,8 @@ #ifndef OPENMW_COMPONENTS_MISC_CONVERT_H #define OPENMW_COMPONENTS_MISC_CONVERT_H -#include +#include +#include #include #include @@ -9,20 +10,13 @@ #include #include -namespace Misc -{ -namespace Convert +namespace Misc::Convert { inline osg::Vec3f makeOsgVec3f(const float* values) { return osg::Vec3f(values[0], values[1], values[2]); } - inline osg::Vec3f makeOsgVec3f(const btVector3& value) - { - return osg::Vec3f(value.x(), value.y(), value.z()); - } - inline osg::Vec3f makeOsgVec3f(const ESM::Pathgrid::Point& value) { return osg::Vec3f(value.mX, value.mY, value.mZ); @@ -47,7 +41,40 @@ namespace Convert { return osg::Quat(quat.x(), quat.y(), quat.z(), quat.w()); } -} + + inline osg::Quat makeOsgQuat(const float (&rotation)[3]) + { + return osg::Quat(rotation[2], osg::Vec3f(0, 0, -1)) + * osg::Quat(rotation[1], osg::Vec3f(0, -1, 0)) + * osg::Quat(rotation[0], osg::Vec3f(-1, 0, 0)); + } + + inline osg::Quat makeOsgQuat(const ESM::Position& position) + { + return makeOsgQuat(position.rot); + } + + inline btQuaternion makeBulletQuaternion(const float (&rotation)[3]) + { + return btQuaternion(btVector3(0, 0, -1), rotation[2]) + * btQuaternion(btVector3(0, -1, 0), rotation[1]) + * btQuaternion(btVector3(-1, 0, 0), rotation[0]); + } + + inline btQuaternion makeBulletQuaternion(const ESM::Position& position) + { + return makeBulletQuaternion(position.rot); + } + + inline btTransform makeBulletTransform(const ESM::Position& position) + { + return btTransform(makeBulletQuaternion(position), toBullet(position.asVec3())); + } + + inline osg::Vec2f toOsgXY(const btVector3& value) + { + return osg::Vec2f(static_cast(value.x()), static_cast(value.y())); + } } -#endif \ No newline at end of file +#endif diff --git a/components/misc/coordinateconverter.hpp b/components/misc/coordinateconverter.hpp index 1906414150..42e772c16d 100644 --- a/components/misc/coordinateconverter.hpp +++ b/components/misc/coordinateconverter.hpp @@ -2,9 +2,9 @@ #define OPENMW_COMPONENTS_MISC_COORDINATECONVERTER_H #include -#include -#include -#include +#include +#include +#include namespace Misc { diff --git a/components/misc/endianness.hpp b/components/misc/endianness.hpp new file mode 100644 index 0000000000..ad8aba18c7 --- /dev/null +++ b/components/misc/endianness.hpp @@ -0,0 +1,90 @@ +#ifndef COMPONENTS_MISC_ENDIANNESS_H +#define COMPONENTS_MISC_ENDIANNESS_H + +#include +#include +#include + +namespace Misc +{ + + // Two-way conversion little-endian <-> big-endian + template + void swapEndiannessInplace(T& v) + { + static_assert(std::is_arithmetic_v); + static_assert(sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4 || sizeof(T) == 8); + + if constexpr (sizeof(T) == 2) + { + uint16_t v16; + std::memcpy(&v16, &v, sizeof(T)); + v16 = (v16 >> 8) | (v16 << 8); + std::memcpy(&v, &v16, sizeof(T)); + } + if constexpr (sizeof(T) == 4) + { + uint32_t v32; + std::memcpy(&v32, &v, sizeof(T)); + v32 = (v32 >> 24) | ((v32 >> 8) & 0xff00) | ((v32 & 0xff00) << 8) | (v32 << 24); + std::memcpy(&v, &v32, sizeof(T)); + } + if constexpr (sizeof(T) == 8) + { + uint64_t v64; + std::memcpy(&v64, &v, sizeof(T)); + v64 = (v64 >> 56) | ((v64 & 0x00ff'0000'0000'0000) >> 40) | ((v64 & 0x0000'ff00'0000'0000) >> 24) + | ((v64 & 0x0000'00ff'0000'0000) >> 8) | ((v64 & 0x0000'0000'ff00'0000) << 8) + | ((v64 & 0x0000'0000'00ff'0000) << 24) | ((v64 & 0x0000'0000'0000'ff00) << 40) | (v64 << 56); + std::memcpy(&v, &v64, sizeof(T)); + } + } + + #ifdef _WIN32 + constexpr bool IS_LITTLE_ENDIAN = true; + constexpr bool IS_BIG_ENDIAN = false; + #else + constexpr bool IS_LITTLE_ENDIAN = __BYTE_ORDER__ != __ORDER_BIG_ENDIAN__; + constexpr bool IS_BIG_ENDIAN = __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__; + #endif + + // Usage: swapEndiannessInplaceIf(v) - native to little-endian or back + // swapEndiannessInplaceIf(v) - native to big-endian or back + template + void swapEndiannessInplaceIf(T& v) + { + static_assert(std::is_arithmetic_v); + static_assert(sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4 || sizeof(T) == 8); + if constexpr (C) + swapEndiannessInplace(v); + } + + template + T toLittleEndian(T v) + { + swapEndiannessInplaceIf(v); + return v; + } + template + T fromLittleEndian(T v) + { + swapEndiannessInplaceIf(v); + return v; + } + + template + T toBigEndian(T v) + { + swapEndiannessInplaceIf(v); + return v; + } + template + T fromBigEndian(T v) + { + swapEndiannessInplaceIf(v); + return v; + } + +} + +#endif // COMPONENTS_MISC_ENDIANNESS_H diff --git a/components/misc/errorMarker.cpp b/components/misc/errorMarker.cpp new file mode 100644 index 0000000000..9fb2f178ee --- /dev/null +++ b/components/misc/errorMarker.cpp @@ -0,0 +1,1390 @@ +#include "errorMarker.hpp" + +namespace Misc +{ + const std::string errorMarker = "#Ascii Scene " +"#Version 162 " +"#Generator OpenSceneGraph 3.6.5 " +"" +"osg::Group {" +" UniqueID 1 " +" Children 5 {" +" osg::Group {" +" UniqueID 2 " +" Name \"Error\" " +" Children 1 {" +" osg::Geometry {" +" UniqueID 3 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 4 " +" DataVariance STATIC " +" ModeList 1 {" +" GL_BLEND ON " +" }" +" AttributeList 1 {" +" osg::Material {" +" UniqueID 5 " +" Name \"Error\" " +" Ambient TRUE Front 1 1 1 0.5 Back 1 1 1 0.5 " +" Diffuse TRUE Front 0.8 0.704 0.32 0.5 Back 0.8 0.704 0.32 0.5 " +" Specular TRUE Front 0.5 0.5 0.5 0.5 Back 0.5 0.5 0.5 0.5 " +" Emission TRUE Front 1 0.88 0.4 0.5 Back 1 0.88 0.4 0.5 " +" Shininess TRUE Front 28.8 Back 28.8 " +" }" +" Value OFF " +" }" +" RenderingHint 2 " +" RenderBinMode USE_RENDERBIN_DETAILS " +" BinNumber 10 " +" BinName \"DepthSortedBin\" " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 6 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 7 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 108 {" +" 0 1 2 3 " +" 0 2 2 4 " +" 3 5 3 4 " +" 4 6 5 6 " +" 7 8 8 9 " +" 10 6 8 10 " +" 5 6 11 11 " +" 6 10 12 5 " +" 11 13 12 11 " +" 11 14 13 10 " +" 15 11 11 15 " +" 16 15 17 16 " +" 18 16 17 17 " +" 19 18 20 21 " +" 22 23 20 22 " +" 22 24 23 25 " +" 23 24 24 26 " +" 25 26 27 28 " +" 28 29 30 26 " +" 28 30 25 26 " +" 31 31 26 30 " +" 32 25 31 33 " +" 32 31 31 34 " +" 33 30 35 31 " +" 31 35 36 35 " +" 37 36 38 36 " +" 37 37 39 38 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 8 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" -4.51996 -5.9634 -60.7026 " +" 2e-06 -5.96339 -61.6017 " +" 4.51996 -5.96339 -60.7026 " +" -8.3518 -5.9634 -58.1422 " +" 8.3518 -5.96339 -58.1422 " +" -58.1422 -5.96341 -8.3518 " +" 58.1422 -5.96339 -8.3518 " +" 60.7026 -5.96339 -4.51996 " +" 61.6017 -5.96339 0 " +" 60.7026 -5.96339 4.51996 " +" 58.1423 -5.96339 8.3518 " +" -58.1422 -5.96341 8.3518 " +" -60.7026 -5.96341 -4.51996 " +" -61.6017 -5.96341 0 " +" -60.7026 -5.96341 4.51996 " +" 8.3518 -5.9634 58.1422 " +" -8.3518 -5.96341 58.1422 " +" 4.51997 -5.96341 60.7026 " +" -4.51996 -5.96341 60.7026 " +" 2e-06 -5.96341 61.6017 " +" -60.7026 5.96339 -4.51996 " +" -61.6017 5.96339 0 " +" -60.7026 5.96339 4.51996 " +" -58.1423 5.96339 -8.3518 " +" -58.1422 5.96339 8.3518 " +" -8.3518 5.9634 -58.1422 " +" -8.3518 5.96339 58.1422 " +" -4.51996 5.96339 60.7026 " +" -2e-06 5.96339 61.6017 " +" 4.51996 5.9634 60.7026 " +" 8.3518 5.9634 58.1422 " +" 8.3518 5.96341 -58.1422 " +" -4.51997 5.96341 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" 4.51996 5.96341 -60.7026 " +" 58.1422 5.96341 8.3518 " +" 58.1422 5.96341 -8.3518 " +" 60.7026 5.96341 4.51996 " +" 60.7026 5.96341 -4.51996 " +" 61.6017 5.96341 0 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 10 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 11 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" 0.37739 0.519384 " +" 0.384197 0.509197 " +" 0.394384 0.50239 " +" 0.375 0.531401 " +" 0.406401 0.5 " +" 0.375 0.718599 " +" 0.593599 0.5 " +" 0.605616 0.50239 " +" 0.615803 0.509197 " +" 0.62261 0.519384 " +" 0.625 0.531401 " +" 0.406401 0.75 " +" 0.37739 0.730616 " +" 0.384197 0.740803 " +" 0.394384 0.74761 " +" 0.625 0.718599 " +" 0.593599 0.75 " +" 0.62261 0.730616 " +" 0.605616 0.74761 " +" 0.615803 0.740803 " +" 0.37739 0.019384 " +" 0.384197 0.009197 " +" 0.394384 0.00239 " +" 0.375 0.031401 " +" 0.406401 0 " +" 0.375 0.218599 " +" 0.593599 0 " +" 0.605616 0.00239 " +" 0.615803 0.009197 " +" 0.62261 0.019384 " +" 0.625 0.031401 " +" 0.406401 0.25 " +" 0.37739 0.230616 " +" 0.384197 0.240803 " +" 0.394384 0.24761 " +" 0.625 0.218599 " +" 0.593599 0.25 " +" 0.62261 0.230616 " +" 0.605616 0.24761 " +" 0.615803 0.240803 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Group {" +" UniqueID 12 " +" Name \"Error\" " +" Children 1 {" +" osg::Geometry {" +" UniqueID 13 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 4 " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 14 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 15 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 120 {" +" 0 1 2 0 " +" 3 1 3 4 " +" 1 3 5 4 " +" 4 5 6 4 " +" 6 7 8 7 " +" 6 8 6 9 " +" 10 8 9 10 " +" 9 11 12 13 " +" 11 12 11 14 " +" 15 12 14 15 " +" 14 16 16 17 " +" 15 16 18 17 " +" 18 19 17 18 " +" 20 19 20 21 " +" 19 20 22 21 " +" 22 23 24 22 " +" 25 23 25 26 " +" 23 25 27 26 " +" 28 26 27 28 " +" 29 26 30 29 " +" 28 30 28 31 " +" 32 30 31 32 " +" 31 33 34 35 " +" 33 34 33 36 " +" 37 34 36 37 " +" 36 38 39 37 " +" 38 39 38 40 " +" 40 41 39 40 " +" 42 41 42 43 " +" 41 42 0 43 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 16 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 61.6017 -5.96339 0 " +" 60.7026 5.96341 -4.51996 " +" 61.6017 5.96341 0 " +" 60.7026 -5.96339 -4.51996 " +" 58.1422 5.96341 -8.3518 " +" 58.1422 -5.96339 -8.3518 " +" 8.3518 -5.96339 -58.1422 " +" 8.3518 5.96341 -58.1422 " +" 4.51996 5.96341 -60.7026 " +" 4.51996 -5.96339 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" 2e-06 -5.96339 -61.6017 " +" -4.51997 5.96341 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" -4.51996 -5.9634 -60.7026 " +" -8.3518 5.9634 -58.1422 " +" -8.3518 -5.9634 -58.1422 " +" -58.1423 5.96339 -8.3518 " +" -58.1422 -5.96341 -8.3518 " +" -60.7026 5.96339 -4.51996 " +" -60.7026 -5.96341 -4.51996 " +" -61.6017 5.96339 0 " +" -61.6017 -5.96341 0 " +" -60.7026 5.96339 4.51996 " +" -61.6017 5.96339 0 " +" -60.7026 -5.96341 4.51996 " +" -58.1422 5.96339 8.3518 " +" -58.1422 -5.96341 8.3518 " +" -8.3518 -5.96341 58.1422 " +" -8.3518 5.96339 58.1422 " +" -4.51996 5.96339 60.7026 " +" -4.51996 -5.96341 60.7026 " +" -2e-06 5.96339 61.6017 " +" 2e-06 -5.96341 61.6017 " +" 4.51996 5.9634 60.7026 " +" -2e-06 5.96339 61.6017 " +" 4.51997 -5.96341 60.7026 " +" 8.3518 5.9634 58.1422 " +" 8.3518 -5.9634 58.1422 " +" 58.1422 5.96341 8.3518 " +" 58.1423 -5.96339 8.3518 " +" 60.7026 5.96341 4.51996 " +" 60.7026 -5.96339 4.51996 " +" 61.6017 5.96341 0 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 18 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 1 0 0 " +" 0.923877 0 -0.38269 " +" 0.980784 0 -0.195097 " +" 0.923877 0 -0.38269 " +" 0.773003 0 -0.634402 " +" 0.773003 0 -0.634402 " +" 0.634402 0 -0.773003 " +" 0.634402 0 -0.773003 " +" 0.38269 0 -0.923877 " +" 0.38269 0 -0.923877 " +" 0.195097 0 -0.980784 " +" 0 0 -1 " +" -0.38269 -0 -0.923877 " +" -0.195097 -0 -0.980784 " +" -0.38269 -0 -0.923877 " +" -0.634402 -0 -0.773003 " +" -0.634402 -0 -0.773003 " +" -0.773003 -0 -0.634402 " +" -0.773003 -0 -0.634402 " +" -0.923877 -0 -0.38269 " +" -0.923877 -0 -0.38269 " +" -0.980784 -0 -0.195097 " +" -1 0 0 " +" -0.923877 0 0.38269 " +" -0.980784 0 0.195097 " +" -0.923877 0 0.38269 " +" -0.773003 0 0.634402 " +" -0.773003 0 0.634402 " +" -0.634402 0 0.773003 " +" -0.634402 0 0.773003 " +" -0.38269 0 0.923877 " +" -0.38269 0 0.923877 " +" -0.195097 0 0.980784 " +" 0 0 1 " +" 0.38269 0 0.923877 " +" 0.195097 0 0.980784 " +" 0.38269 0 0.923877 " +" 0.634402 0 0.773003 " +" 0.634402 0 0.773003 " +" 0.773003 0 0.634402 " +" 0.773003 0 0.634402 " +" 0.923877 0 0.38269 " +" 0.923877 0 0.38269 " +" 0.980784 0 0.195097 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 19 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 0.625 0.5 " +" 0.605616 0.25 " +" 0.625 0.25 " +" 0.605616 0.5 " +" 0.593599 0.25 " +" 0.593599 0.5 " +" 0.406401 0.5 " +" 0.406401 0.25 " +" 0.394384 0.25 " +" 0.394384 0.5 " +" 0.375 0.25 " +" 0.375 0.5 " +" 0.125 0.519384 " +" 0.125 0.5 " +" 0.375 0.519384 " +" 0.125 0.531401 " +" 0.375 0.531401 " +" 0.125 0.718599 " +" 0.375 0.718599 " +" 0.125 0.730616 " +" 0.375 0.730616 " +" 0.125 0.75 " +" 0.375 0.75 " +" 0.394384 1 " +" 0.375 1 " +" 0.394384 0.75 " +" 0.406401 1 " +" 0.406401 0.75 " +" 0.593599 0.75 " +" 0.593599 1 " +" 0.605616 1 " +" 0.605616 0.75 " +" 0.625 1 " +" 0.625 0.75 " +" 0.875 0.730616 " +" 0.875 0.75 " +" 0.625 0.730616 " +" 0.875 0.718599 " +" 0.625 0.718599 " +" 0.875 0.531401 " +" 0.625 0.531401 " +" 0.875 0.519384 " +" 0.625 0.519384 " +" 0.875 0.5 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Group {" +" UniqueID 20 " +" Name \"Error\" " +" Children 1 {" +" osg::Geometry {" +" UniqueID 21 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 22 " +" DataVariance STATIC " +" AttributeList 1 {" +" osg::Material {" +" UniqueID 23 " +" Name \"ErrorLabel\" " +" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 " +" Diffuse TRUE Front 0.176208 0.176208 0.176208 1 Back 0.176208 0.176208 0.176208 1 " +" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 " +" Emission TRUE Front 0.22026 0.22026 0.22026 1 Back 0.22026 0.22026 0.22026 1 " +" Shininess TRUE Front 28.8 Back 28.8 " +" }" +" Value OFF " +" }" +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 24 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 25 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 216 {" +" 0 1 2 3 " +" 0 2 2 4 " +" 3 4 5 3 " +" 5 6 7 5 " +" 8 3 8 5 " +" 7 7 9 8 " +" 10 3 8 8 " +" 11 10 12 13 " +" 10 14 12 10 " +" 15 14 10 10 " +" 11 15 16 15 " +" 11 11 17 16 " +" 18 16 17 17 " +" 19 18 20 21 " +" 22 23 20 22 " +" 22 24 23 25 " +" 23 24 24 26 " +" 25 26 27 28 " +" 28 29 30 26 " +" 28 30 25 26 " +" 31 31 26 30 " +" 32 25 31 33 " +" 32 31 31 34 " +" 33 30 35 31 " +" 31 35 36 35 " +" 37 36 38 36 " +" 37 37 39 38 " +" 40 41 42 43 " +" 40 42 42 44 " +" 43 45 43 44 " +" 44 46 45 47 " +" 45 46 48 47 " +" 46 46 49 48 " +" 44 50 46 51 " +" 46 50 50 52 " +" 53 54 50 53 " +" 53 55 54 50 " +" 54 51 54 56 " +" 51 56 57 51 " +" 58 51 57 57 " +" 59 58 60 61 " +" 62 63 60 62 " +" 62 64 63 65 " +" 63 64 64 66 " +" 65 66 67 68 " +" 69 70 65 71 " +" 69 65 72 71 " +" 65 66 68 73 " +" 65 66 73 68 " +" 74 73 73 72 " +" 65 73 75 72 " +" 72 75 76 75 " +" 77 76 78 76 " +" 77 77 79 78 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 26 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -7.12646 -9.95049 -13.7349 " +" -6.35115 -9.95049 -14.8952 " +" -5.1908 -9.95049 -15.6705 " +" -7.39872 -9.95049 -12.3661 " +" -3.82208 -9.95049 -15.9428 " +" 3.82208 -9.95049 -15.9428 " +" 5.1908 -9.95049 -15.6705 " +" 6.35115 -9.95049 -14.8952 " +" 7.39872 -9.95049 -12.3661 " +" 7.12646 -9.95049 -13.7349 " +" -7.39872 -9.95049 33.4208 " +" 7.39872 -9.95049 33.4208 " +" -6.35115 -9.95049 35.9499 " +" -7.12647 -9.95049 34.7895 " +" -5.1908 -9.95049 36.7252 " +" -3.82208 -9.95049 36.9975 " +" 3.82208 -9.95049 36.9975 " +" 7.12646 -9.95049 34.7895 " +" 5.1908 -9.95049 36.7252 " +" 6.35115 -9.95049 35.9499 " +" -7.12646 -9.95042 -36.7346 " +" -6.35115 -9.95042 -37.8949 " +" -5.1908 -9.95042 -38.6702 " +" -7.39872 -9.95042 -35.3659 " +" -3.82208 -9.95042 -38.9425 " +" -7.39872 -9.95042 -27.7217 " +" 3.82208 -9.95042 -38.9425 " +" 5.1908 -9.95042 -38.6702 " +" 6.35115 -9.95042 -37.8949 " +" 7.12646 -9.95042 -36.7346 " +" 7.39872 -9.95042 -35.3659 " +" -3.82208 -9.95042 -24.1451 " +" -7.12647 -9.95042 -26.353 " +" -6.35115 -9.95042 -25.1926 " +" -5.1908 -9.95042 -24.4173 " +" 7.39872 -9.95042 -27.7217 " +" 3.82208 -9.95042 -24.1451 " +" 7.12646 -9.95042 -26.353 " +" 5.1908 -9.95042 -24.4173 " +" 6.35115 -9.95042 -25.1926 " +" -5.1908 9.95055 -15.6705 " +" -6.35115 9.95055 -14.8952 " +" -7.12646 9.95055 -13.7349 " +" -3.82208 9.95055 -15.9428 " +" -7.39872 9.95055 -12.3661 " +" 3.82208 9.95055 -15.9428 " +" 7.39872 9.95055 -12.3661 " +" 5.1908 9.95055 -15.6705 " +" 6.35115 9.95055 -14.8952 " +" 7.12646 9.95055 -13.7349 " +" -7.39872 9.95055 33.4208 " +" 7.39872 9.95055 33.4208 " +" -7.12646 9.95055 34.7895 " +" -6.35115 9.95056 35.9499 " +" -3.82208 9.95056 36.9975 " +" -5.1908 9.95056 36.7252 " +" 3.82208 9.95055 36.9975 " +" 5.19081 9.95055 36.7252 " +" 7.12646 9.95056 34.7895 " +" 6.35115 9.95055 35.9499 " +" -5.1908 9.95062 -38.6702 " +" -6.35115 9.95062 -37.8949 " +" -7.12646 9.95062 -36.7346 " +" -3.82208 9.95062 -38.9425 " +" -7.39872 9.95062 -35.3659 " +" 3.82208 9.95062 -38.9425 " +" -7.39872 9.95063 -27.7217 " +" -7.12646 9.95063 -26.353 " +" -6.35115 9.95063 -25.1926 " +" 6.35115 9.95062 -37.8949 " +" 5.1908 9.95062 -38.6702 " +" 7.12647 9.95062 -36.7346 " +" 7.39872 9.95062 -35.3659 " +" -3.82208 9.95063 -24.1451 " +" -5.1908 9.95063 -24.4173 " +" 3.82208 9.95063 -24.1451 " +" 7.39872 9.95062 -27.7217 " +" 5.1908 9.95063 -24.4173 " +" 7.12646 9.95063 -26.353 " +" 6.35115 9.95063 -25.1926 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 28 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 29 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0.006133 0.041706 " +" 0.023598 0.006596 " +" 0.149209 0.001714 " +" 0 0.06756 " +" 0.241707 0 " +" 0.758294 0 " +" 0.850791 0.001714 " +" 0.976402 0.006596 " +" 1 0.06756 " +" 0.993867 0.041706 " +" 0 0.93244 " +" 1 0.93244 " +" 0.023598 0.993404 " +" 0.006133 0.958294 " +" 0.149209 0.998286 " +" 0.241706 1 " +" 0.758294 1 " +" 0.993867 0.958294 " +" 0.850791 0.998286 " +" 0.976402 0.993404 " +" 0.006133 0.149209 " +" 0.023598 0.023598 " +" 0.149209 0.006133 " +" 0 0.241707 " +" 0.241707 0 " +" 0 0.758294 " +" 0.758294 0 " +" 0.850791 0.006133 " +" 0.976402 0.023598 " +" 0.993867 0.149209 " +" 1 0.241707 " +" 0.241706 1 " +" 0.006133 0.850791 " +" 0.023598 0.976402 " +" 0.149209 0.993867 " +" 1 0.758293 " +" 0.758294 1 " +" 0.993867 0.850791 " +" 0.850791 0.993867 " +" 0.976402 0.976402 " +" 0.149209 0.001714 " +" 0.023598 0.006596 " +" 0.006133 0.041706 " +" 0.241706 0 " +" 0 0.06756 " +" 0.758294 0 " +" 1 0.06756 " +" 0.850791 0.001714 " +" 0.976402 0.006596 " +" 0.993867 0.041706 " +" 0 0.93244 " +" 1 0.93244 " +" 0.006133 0.958294 " +" 0.023598 0.993404 " +" 0.241707 1 " +" 0.149209 0.998286 " +" 0.758294 1 " +" 0.850791 0.998286 " +" 0.993867 0.958294 " +" 0.976402 0.993404 " +" 0.149209 0.006133 " +" 0.023598 0.023598 " +" 0.006133 0.149209 " +" 0.241706 0 " +" 0 0.241707 " +" 0.758294 0 " +" 0 0.758294 " +" 0.006133 0.850791 " +" 0.023598 0.976402 " +" 0.976402 0.023598 " +" 0.850791 0.006133 " +" 0.993867 0.149209 " +" 1 0.241706 " +" 0.241707 1 " +" 0.149209 0.993867 " +" 0.758294 1 " +" 1 0.758294 " +" 0.850791 0.993867 " +" 0.993867 0.850791 " +" 0.976402 0.976402 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Group {" +" UniqueID 30 " +" Name \"Error\" " +" Children 1 {" +" osg::Geometry {" +" UniqueID 31 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 22 " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 32 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 33 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 240 {" +" 0 1 2 0 " +" 3 1 0 2 " +" 4 0 5 3 " +" 4 2 6 5 " +" 7 3 4 6 " +" 8 5 9 7 " +" 6 10 8 9 " +" 11 7 6 12 " +" 10 9 13 11 " +" 12 14 10 13 " +" 15 11 12 16 " +" 14 13 17 15 " +" 16 18 14 19 " +" 15 17 16 20 " +" 18 19 21 15 " +" 18 20 22 23 " +" 21 19 18 22 " +" 24 23 19 25 " +" 26 24 22 27 " +" 23 25 26 22 " +" 28 27 25 29 " +" 30 26 28 31 " +" 27 29 30 32 " +" 26 31 29 33 " +" 34 32 30 35 " +" 31 33 34 30 " +" 36 35 33 37 " +" 38 34 36 39 " +" 35 37 38 39 " +" 34 39 38 35 " +" 40 41 42 43 " +" 41 40 40 42 " +" 44 43 40 45 " +" 46 44 42 47 " +" 43 45 46 48 " +" 44 47 45 49 " +" 50 48 46 51 " +" 47 49 50 46 " +" 52 51 49 53 " +" 54 50 52 55 " +" 51 53 54 52 " +" 56 55 53 57 " +" 58 54 56 57 " +" 59 55 58 56 " +" 60 57 61 59 " +" 58 60 62 61 " +" 63 59 58 62 " +" 64 61 65 63 " +" 62 66 64 65 " +" 67 63 62 68 " +" 66 65 69 67 " +" 66 68 70 69 " +" 71 67 66 70 " +" 72 69 73 71 " +" 70 74 72 71 " +" 73 75 70 76 " +" 74 71 75 77 " +" 76 78 74 75 " +" 79 77 76 79 " +" 78 75 78 79 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 34 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -7.39872 9.95055 -12.3661 " +" -7.39872 -9.95049 -12.3661 " +" -7.39872 -9.95049 33.4208 " +" -7.12646 -9.95049 -13.7349 " +" -7.39872 9.95055 33.4208 " +" -7.12646 9.95055 -13.7349 " +" -7.12647 -9.95049 34.7895 " +" -6.35115 -9.95049 -14.8952 " +" -7.12646 9.95055 34.7895 " +" -6.35115 9.95055 -14.8952 " +" -6.35115 9.95056 35.9499 " +" -5.1908 -9.95049 -15.6705 " +" -6.35115 -9.95049 35.9499 " +" -5.1908 9.95055 -15.6705 " +" -5.1908 9.95056 36.7252 " +" -3.82208 -9.95049 -15.9428 " +" -5.1908 -9.95049 36.7252 " +" -3.82208 9.95055 -15.9428 " +" -3.82208 9.95056 36.9975 " +" 3.82208 9.95055 -15.9428 " +" -3.82208 -9.95049 36.9975 " +" 3.82208 -9.95049 -15.9428 " +" 3.82208 -9.95049 36.9975 " +" 5.1908 -9.95049 -15.6705 " +" 3.82208 9.95055 36.9975 " +" 5.1908 9.95055 -15.6705 " +" 5.19081 9.95055 36.7252 " +" 6.35115 -9.95049 -14.8952 " +" 5.1908 -9.95049 36.7252 " +" 6.35115 9.95055 -14.8952 " +" 6.35115 -9.95049 35.9499 " +" 7.12646 -9.95049 -13.7349 " +" 6.35115 9.95055 35.9499 " +" 7.12646 9.95055 -13.7349 " +" 7.12646 9.95056 34.7895 " +" 7.39872 -9.95049 -12.3661 " +" 7.12646 -9.95049 34.7895 " +" 7.39872 9.95055 -12.3661 " +" 7.39872 -9.95049 33.4208 " +" 7.39872 9.95055 33.4208 " +" -3.82208 9.95063 -24.1451 " +" -3.82208 -9.95042 -24.1451 " +" 3.82208 -9.95042 -24.1451 " +" -5.1908 -9.95042 -24.4173 " +" 3.82208 9.95063 -24.1451 " +" -5.1908 9.95063 -24.4173 " +" 5.1908 -9.95042 -24.4173 " +" -6.35115 -9.95042 -25.1926 " +" 5.1908 9.95063 -24.4173 " +" -6.35115 9.95063 -25.1926 " +" 6.35115 9.95063 -25.1926 " +" -7.12647 -9.95042 -26.353 " +" 6.35115 -9.95042 -25.1926 " +" -7.12646 9.95063 -26.353 " +" 7.12646 9.95063 -26.353 " +" -7.39872 -9.95042 -27.7217 " +" 7.12646 -9.95042 -26.353 " +" -7.39872 9.95063 -27.7217 " +" 7.39872 9.95062 -27.7217 " +" -7.39872 -9.95042 -35.3659 " +" 7.39872 -9.95042 -27.7217 " +" -7.39872 9.95062 -35.3659 " +" 7.39872 -9.95042 -35.3659 " +" -7.12646 -9.95042 -36.7346 " +" 7.39872 9.95062 -35.3659 " +" -7.12646 9.95062 -36.7346 " +" 7.12647 9.95062 -36.7346 " +" -6.35115 -9.95042 -37.8949 " +" 7.12646 -9.95042 -36.7346 " +" -6.35115 9.95062 -37.8949 " +" 6.35115 -9.95042 -37.8949 " +" -5.1908 -9.95042 -38.6702 " +" 6.35115 9.95062 -37.8949 " +" -5.1908 9.95062 -38.6702 " +" 5.1908 9.95062 -38.6702 " +" -3.82208 9.95062 -38.9425 " +" 5.1908 -9.95042 -38.6702 " +" -3.82208 -9.95042 -38.9425 " +" 3.82208 9.95062 -38.9425 " +" 3.82208 -9.95042 -38.9425 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 36 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -0.995187 -0 -0.0979987 " +" -0.995187 -0 -0.0979987 " +" -0.995187 0 0.0979987 " +" -0.923877 -0 -0.38269 " +" -0.995187 0 0.0979987 " +" -0.923877 -0 -0.38269 " +" -0.923877 0 0.38269 " +" -0.707107 -0 -0.707107 " +" -0.923877 0 0.38269 " +" -0.707107 -0 -0.707107 " +" -0.707107 0 0.707107 " +" -0.38269 -0 -0.923877 " +" -0.707107 0 0.707107 " +" -0.38269 -0 -0.923877 " +" -0.38269 0 0.923877 " +" -0.0979987 -0 -0.995187 " +" -0.38269 0 0.923877 " +" -0.0979987 -0 -0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 -0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 -0.995187 " +" 0.0979987 0 0.995187 " +" 0.38269 0 -0.923877 " +" 0.0979987 0 0.995187 " +" 0.38269 0 -0.923877 " +" 0.38269 0 0.923877 " +" 0.707107 0 -0.707107 " +" 0.38269 0 0.923877 " +" 0.707107 0 -0.707107 " +" 0.707107 0 0.707107 " +" 0.923877 0 -0.38269 " +" 0.707107 0 0.707107 " +" 0.923877 0 -0.38269 " +" 0.923877 0 0.38269 " +" 0.995187 0 -0.0979987 " +" 0.923877 0 0.38269 " +" 0.995187 0 -0.0979987 " +" 0.995187 0 0.0979987 " +" 0.995187 0 0.0979987 " +" -0.0979987 0 0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 0.995187 " +" -0.38269 0 0.923877 " +" 0.0979987 0 0.995187 " +" -0.38269 0 0.923877 " +" 0.38269 0 0.923877 " +" -0.707107 0 0.707107 " +" 0.38269 0 0.923877 " +" -0.707107 0 0.707107 " +" 0.707107 0 0.707107 " +" -0.923877 0 0.38269 " +" 0.707107 0 0.707107 " +" -0.923877 0 0.38269 " +" 0.923877 0 0.38269 " +" -0.995187 0 0.0979987 " +" 0.923877 0 0.38269 " +" -0.995187 0 0.0979987 " +" 0.995187 0 0.0979987 " +" -0.995187 -0 -0.0979987 " +" 0.995187 0 0.0979987 " +" -0.995187 -0 -0.0979987 " +" 0.995187 0 -0.0979987 " +" -0.923877 -0 -0.38269 " +" 0.995187 0 -0.0979987 " +" -0.923877 -0 -0.38269 " +" 0.923877 0 -0.38269 " +" -0.707107 -0 -0.707107 " +" 0.923877 0 -0.38269 " +" -0.707107 -0 -0.707107 " +" 0.707107 0 -0.707107 " +" -0.38269 -0 -0.923877 " +" 0.707107 0 -0.707107 " +" -0.38269 -0 -0.923877 " +" 0.38269 0 -0.923877 " +" -0.0979987 -0 -0.995187 " +" 0.38269 0 -0.923877 " +" -0.0979987 -0 -0.995187 " +" 0.0979987 0 -0.995187 " +" 0.0979987 0 -0.995187 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 37 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0 0.06756 " +" 0 0.06756 " +" 0 0.93244 " +" 0.006133 0.041706 " +" 0 0.93244 " +" 0.006133 0.041706 " +" 0.006133 0.958294 " +" 0.023598 0.006596 " +" 0.006133 0.958294 " +" 0.023598 0.006596 " +" 0.023598 0.993404 " +" 0.149209 0.001714 " +" 0.023598 0.993404 " +" 0.149209 0.001714 " +" 0.149209 0.998286 " +" 0.241706 0 " +" 0.149209 0.998286 " +" 0.241707 0 " +" 0.241706 1 " +" 0.758294 0 " +" 0.241707 1 " +" 0.758294 0 " +" 0.758294 1 " +" 0.850791 0.001714 " +" 0.758294 1 " +" 0.850791 0.001714 " +" 0.850791 0.998286 " +" 0.976402 0.006596 " +" 0.850791 0.998286 " +" 0.976402 0.006596 " +" 0.976402 0.993404 " +" 0.993867 0.041706 " +" 0.976402 0.993404 " +" 0.993867 0.041706 " +" 0.993867 0.958294 " +" 1 0.06756 " +" 0.993867 0.958294 " +" 1 0.06756 " +" 1 0.93244 " +" 1 0.93244 " +" 0.241706 1 " +" 0.241707 1 " +" 0.758294 1 " +" 0.149209 0.993867 " +" 0.758294 1 " +" 0.149209 0.993867 " +" 0.850791 0.993867 " +" 0.023598 0.976402 " +" 0.850791 0.993867 " +" 0.023598 0.976402 " +" 0.976402 0.976402 " +" 0.006133 0.850791 " +" 0.976402 0.976402 " +" 0.006133 0.850791 " +" 0.993867 0.850791 " +" 0 0.758293 " +" 0.993867 0.850791 " +" 0 0.758294 " +" 1 0.758294 " +" 0 0.241707 " +" 1 0.758294 " +" 0 0.241706 " +" 1 0.241707 " +" 0.006133 0.149209 " +" 1 0.241707 " +" 0.006133 0.149209 " +" 0.993867 0.149209 " +" 0.023598 0.023598 " +" 0.993867 0.149209 " +" 0.023598 0.023598 " +" 0.976402 0.023598 " +" 0.149209 0.006133 " +" 0.976402 0.023598 " +" 0.149209 0.006133 " +" 0.850791 0.006133 " +" 0.241707 0 " +" 0.850791 0.006133 " +" 0.241706 0 " +" 0.758294 0 " +" 0.758294 0 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Group {" +" UniqueID 38 " +" Name \"Cube\" " +" Children 1 {" +" osg::Geometry {" +" UniqueID 39 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 40 " +" DataVariance STATIC " +" AttributeList 1 {" +" osg::Material {" +" UniqueID 41 " +" Name \"Material\" " +" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 " +" Diffuse TRUE Front 0.8 0.8 0.8 1 Back 0.8 0.8 0.8 1 " +" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 " +" Emission TRUE Front 0 0 0 1 Back 0 0 0 1 " +" Shininess TRUE Front 41.344 Back 41.344 " +" }" +" Value OFF " +" }" +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 42 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 43 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 36 {" +" 0 1 2 0 " +" 2 3 4 5 " +" 6 4 6 7 " +" 8 9 10 8 " +" 10 11 12 13 " +" 14 12 14 15 " +" 16 17 18 16 " +" 18 19 20 21 " +" 22 20 22 23 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 44 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 1 1 1 " +" -1 1 1 " +" -1 -1 1 " +" 1 -1 1 " +" 1 -1 -1 " +" 1 -1 1 " +" -1 -1 1 " +" -1 -1 -1 " +" -1 -1 -1 " +" -1 -1 1 " +" -1 1 1 " +" -1 1 -1 " +" -1 1 -1 " +" 1 1 -1 " +" 1 -1 -1 " +" -1 -1 -1 " +" 1 1 -1 " +" 1 1 1 " +" 1 -1 1 " +" 1 -1 -1 " +" -1 1 -1 " +" -1 1 1 " +" 1 1 1 " +" 1 1 -1 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 46 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 0 0 1 " +" 0 0 1 " +" 0 0 1 " +" 0 0 1 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" -1 0 0 " +" -1 0 0 " +" -1 0 0 " +" -1 0 0 " +" 0 0 -1 " +" 0 0 -1 " +" 0 0 -1 " +" 0 0 -1 " +" 1 0 0 " +" 1 0 0 " +" 1 0 0 " +" 1 0 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 47 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 0.625 0.5 " +" 0.875 0.5 " +" 0.875 0.75 " +" 0.625 0.75 " +" 0.375 0.75 " +" 0.625 0.75 " +" 0.625 1 " +" 0.375 1 " +" 0.375 0 " +" 0.625 0 " +" 0.625 0.25 " +" 0.375 0.25 " +" 0.125 0.5 " +" 0.375 0.5 " +" 0.375 0.75 " +" 0.125 0.75 " +" 0.375 0.5 " +" 0.625 0.5 " +" 0.625 0.75 " +" 0.375 0.75 " +" 0.375 0.25 " +" 0.625 0.25 " +" 0.625 0.5 " +" 0.375 0.5 " +" }" +" }" +" }" +" }" +" }" +" }" +" }" +"}"; + +} diff --git a/components/misc/errorMarker.hpp b/components/misc/errorMarker.hpp new file mode 100644 index 0000000000..05e1fa9557 --- /dev/null +++ b/components/misc/errorMarker.hpp @@ -0,0 +1,11 @@ +#ifndef OPENMW_COMPONENTS_MISC_ERRORMARKER_H +#define OPENMW_COMPONENTS_MISC_ERRORMARKER_H + +#include + +namespace Misc +{ + extern const std::string errorMarker; +} + +#endif diff --git a/components/misc/frameratelimiter.hpp b/components/misc/frameratelimiter.hpp new file mode 100644 index 0000000000..e3689cbcf4 --- /dev/null +++ b/components/misc/frameratelimiter.hpp @@ -0,0 +1,57 @@ +#ifndef OPENMW_COMPONENTS_MISC_FRAMERATELIMITER_H +#define OPENMW_COMPONENTS_MISC_FRAMERATELIMITER_H + +#include +#include + +namespace Misc +{ + class FrameRateLimiter + { + public: + template + explicit FrameRateLimiter(std::chrono::duration maxFrameDuration, + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) + : mMaxFrameDuration(std::chrono::duration_cast(maxFrameDuration)) + , mLastMeasurement(now) + , mLastFrameDuration(0) + {} + + std::chrono::steady_clock::duration getLastFrameDuration() const + { + return mLastFrameDuration; + } + + void limit(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) + { + const auto passed = now - mLastMeasurement; + const auto left = mMaxFrameDuration - passed; + if (left > left.zero()) + { + std::this_thread::sleep_for(left); + mLastMeasurement = now + left; + mLastFrameDuration = mMaxFrameDuration; + } + else + { + mLastMeasurement = now; + mLastFrameDuration = passed; + } + } + + private: + std::chrono::steady_clock::duration mMaxFrameDuration; + std::chrono::steady_clock::time_point mLastMeasurement; + std::chrono::steady_clock::duration mLastFrameDuration; + }; + + inline Misc::FrameRateLimiter makeFrameRateLimiter(float frameRateLimit) + { + if (frameRateLimit > 0.0f) + return Misc::FrameRateLimiter(std::chrono::duration(1.0f / frameRateLimit)); + else + return Misc::FrameRateLimiter(std::chrono::steady_clock::duration::zero()); + } +} + +#endif diff --git a/components/misc/guarded.hpp b/components/misc/guarded.hpp index 55a2c670cf..a06789773d 100644 --- a/components/misc/guarded.hpp +++ b/components/misc/guarded.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Misc { @@ -11,28 +12,28 @@ namespace Misc class Locked { public: - Locked(std::mutex& mutex, T& value) + Locked(std::mutex& mutex, std::remove_reference_t& value) : mLock(mutex), mValue(value) {} - T& get() const + std::remove_reference_t& get() const { return mValue.get(); } - T* operator ->() const + std::remove_reference_t* operator ->() const { - return std::addressof(get()); + return &get(); } - T& operator *() const + std::remove_reference_t& operator *() const { return get(); } private: std::unique_lock mLock; - std::reference_wrapper mValue; + std::reference_wrapper> mValue; }; template @@ -75,7 +76,7 @@ namespace Misc return Locked(mMutex, mValue); } - Locked lockConst() + Locked lockConst() const { return Locked(mMutex, mValue); } @@ -88,7 +89,7 @@ namespace Misc } private: - std::mutex mMutex; + mutable std::mutex mMutex; T mValue; }; } diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp new file mode 100644 index 0000000000..861df73772 --- /dev/null +++ b/components/misc/hash.hpp @@ -0,0 +1,20 @@ +#ifndef MISC_HASH_H +#define MISC_HASH_H + +#include +#include +#include + +namespace Misc +{ + /// Implemented similar to the boost::hash_combine + template + inline void hashCombine(Seed& seed, const T& v) + { + static_assert(sizeof(Seed) >= sizeof(std::size_t), "Resulting hash will be truncated"); + std::hash hasher; + seed ^= static_cast(hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2)); + } +} + +#endif diff --git a/components/misc/math.hpp b/components/misc/math.hpp new file mode 100644 index 0000000000..27e29675f2 --- /dev/null +++ b/components/misc/math.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_COMPONENTS_MISC_MATH_H +#define OPENMW_COMPONENTS_MISC_MATH_H + +#include + +namespace Misc +{ + inline osg::Vec3f getVectorToLine(const osg::Vec3f& position, const osg::Vec3f& a, const osg::Vec3f& b) + { + osg::Vec3f direction = b - a; + direction.normalize(); + return (position - a) ^ direction; + } +} + +#endif diff --git a/components/misc/messageformatparser.cpp b/components/misc/messageformatparser.cpp index 6f0e471325..a40dcccd52 100644 --- a/components/misc/messageformatparser.cpp +++ b/components/misc/messageformatparser.cpp @@ -4,7 +4,7 @@ namespace Misc { MessageFormatParser::~MessageFormatParser() {} - void MessageFormatParser::process(const std::string& m) + void MessageFormatParser::process(std::string_view m) { for (unsigned int i = 0; i < m.size(); ++i) { diff --git a/components/misc/messageformatparser.hpp b/components/misc/messageformatparser.hpp index db2a8b0af4..4ceac2be2f 100644 --- a/components/misc/messageformatparser.hpp +++ b/components/misc/messageformatparser.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_MISC_MESSAGEFORMATPARSER_H #define OPENMW_COMPONENTS_MISC_MESSAGEFORMATPARSER_H -#include +#include namespace Misc { @@ -28,7 +28,7 @@ namespace Misc public: virtual ~MessageFormatParser(); - virtual void process(const std::string& message); + virtual void process(std::string_view message); }; } diff --git a/components/misc/notnullptr.hpp b/components/misc/notnullptr.hpp new file mode 100644 index 0000000000..a7e02c3614 --- /dev/null +++ b/components/misc/notnullptr.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_COMPONENTS_MISC_NOTNULLPTR_H +#define OPENMW_COMPONENTS_MISC_NOTNULLPTR_H + +#include +#include +#include + +namespace Misc +{ + template + class NotNullPtr + { + public: + NotNullPtr(T* value) + : mValue(value) + { + assert(mValue != nullptr); + } + + NotNullPtr(std::nullptr_t) = delete; + + operator T*() const { return mValue; } + + T* operator->() const { return mValue; } + + T& operator*() const { return *mValue; } + + private: + T* mValue; + }; +} + +#endif diff --git a/components/misc/osguservalues.cpp b/components/misc/osguservalues.cpp new file mode 100644 index 0000000000..b70647a63e --- /dev/null +++ b/components/misc/osguservalues.cpp @@ -0,0 +1,8 @@ +#include "osguservalues.hpp" + +namespace Misc +{ + const std::string OsgUserValues::sFileHash = "fileHash"; + const std::string OsgUserValues::sExtraData = "xData"; + const std::string OsgUserValues::sXSoftEffect = "xSoftEffect"; +} diff --git a/components/misc/osguservalues.hpp b/components/misc/osguservalues.hpp new file mode 100644 index 0000000000..443e6132e1 --- /dev/null +++ b/components/misc/osguservalues.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H +#define OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H + +#include + +namespace Misc +{ + struct OsgUserValues + { + static const std::string sFileHash; + static const std::string sExtraData; + static const std::string sXSoftEffect; + }; +} + +#endif diff --git a/components/misc/pathhelpers.hpp b/components/misc/pathhelpers.hpp new file mode 100644 index 0000000000..ee6ba8e0d5 --- /dev/null +++ b/components/misc/pathhelpers.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHHELPERS_H +#define OPENMW_COMPONENTS_MISC_PATHHELPERS_H + +#include + +namespace Misc +{ + inline std::string_view getFileExtension(std::string_view file) + { + if (auto extPos = file.find_last_of('.'); extPos != std::string::npos) + { + file.remove_prefix(extPos + 1); + return file; + } + return {}; + } + + inline std::string_view getFileName(std::string_view path) + { + if (auto namePos = path.find_last_of("/\\"); namePos != std::string::npos) + { + path.remove_prefix(namePos + 1); + } + + return path; + } + + inline std::string_view stemFile(std::string_view path) + { + path = getFileName(path); + + if (auto extPos = path.find_last_of("."); extPos != std::string::npos) + { + path.remove_suffix(path.size() - extPos); + } + + return path; + } +} + +#endif diff --git a/components/misc/progressreporter.hpp b/components/misc/progressreporter.hpp new file mode 100644 index 0000000000..30d9b0ae2e --- /dev/null +++ b/components/misc/progressreporter.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_MISC_PROGRESSREPORTER_H +#define OPENMW_COMPONENTS_MISC_PROGRESSREPORTER_H + +#include +#include +#include +#include +#include + +namespace Misc +{ + template + class ProgressReporter + { + public: + explicit ProgressReporter(Report&& report = Report {}) + : mReport(std::forward(report)) + {} + + explicit ProgressReporter(std::chrono::steady_clock::duration interval, Report&& report = Report {}) + : mInterval(interval) + , mReport(std::forward(report)) + {} + + void operator()(std::size_t provided, std::size_t expected) + { + expected = std::max(expected, provided); + const bool shouldReport = [&] + { + const std::lock_guard lock(mMutex); + const auto now = std::chrono::steady_clock::now(); + if (mNextReport > now || provided == expected) + return false; + if (mInterval.count() > 0) + mNextReport = mNextReport + mInterval * ((now - mNextReport + mInterval).count() / mInterval.count()); + return true; + } (); + if (shouldReport) + mReport(provided, expected); + } + + private: + const std::chrono::steady_clock::duration mInterval = std::chrono::seconds(1); + Report mReport; + std::mutex mMutex; + std::chrono::steady_clock::time_point mNextReport {std::chrono::steady_clock::now() + mInterval}; + }; +} + +#endif diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5cf4378b85..73a1d961e7 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -1,6 +1,7 @@ #include "resourcehelpers.hpp" #include +#include #include @@ -29,17 +30,22 @@ namespace } -bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +bool changeExtension(std::string &path, std::string_view ext) { std::string::size_type pos = path.rfind('.'); - if(pos != std::string::npos && path.compare(pos, path.length() - pos, ".dds") != 0) + if(pos != std::string::npos && path.compare(pos, path.length() - pos, ext) != 0) { - path.replace(pos, path.length(), ".dds"); + path.replace(pos, path.length(), ext); return true; } return false; } +bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +{ + return changeExtension(path, ".dds"); +} + std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath, const VFS::Manager* vfs) { /* Bethesda at some point converted all their BSA @@ -138,3 +144,24 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP } return mdlname; } + +std::string Misc::ResourceHelpers::correctMeshPath(const std::string &resPath, const VFS::Manager* vfs) +{ + return "meshes\\" + resPath; +} + +std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath, const VFS::Manager* vfs) +{ + std::string sound = resPath; + // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. + if (!vfs->exists(sound)) + changeExtension(sound, ".mp3"); + + return vfs->normalizeFilename(sound); + +} + +bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id) +{ + return Misc::StringUtils::ciEqual(id, "prisonmarker") || Misc::StringUtils::ciEqual(id, "divinemarker") || Misc::StringUtils::ciEqual(id, "templemarker") || Misc::StringUtils::ciEqual(id, "northmarker"); +} diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index fa50cce228..0be1076d7d 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -2,6 +2,7 @@ #define MISC_RESOURCEHELPERS_H #include +#include namespace VFS { @@ -23,6 +24,12 @@ namespace Misc std::string correctBookartPath(const std::string &resPath, int width, int height, const VFS::Manager* vfs); /// Use "xfoo.nif" instead of "foo.nif" if available std::string correctActorModelPath(const std::string &resPath, const VFS::Manager* vfs); + std::string correctMeshPath(const std::string &resPath, const VFS::Manager* vfs); + + std::string correctSoundPath(const std::string& resPath, const VFS::Manager* vfs); + + /// marker objects that have a hardcoded function in the game logic, should be hidden from the player + bool isHiddenMarker(std::string_view id); } } diff --git a/components/misc/rng.cpp b/components/misc/rng.cpp index 23d8204482..c1ed4ed95b 100644 --- a/components/misc/rng.cpp +++ b/components/misc/rng.cpp @@ -2,49 +2,85 @@ #include #include +#include -namespace -{ - Misc::Rng::Seed sSeed; -} +#include -namespace Misc +namespace Misc::Rng { + static Generator sGenerator; + + Generator& getGenerator() + { + return sGenerator; + } + + std::string serialize(const Generator& prng) + { + std::stringstream ss; + ss << prng; + + return ss.str(); + } + + void deserialize(std::string_view data, Generator& prng) + { + std::stringstream ss; + ss << data; - Rng::Seed::Seed() {} + ss.seekg(0); + ss >> prng; + } + + unsigned int generateDefaultSeed() + { + auto res = static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + return res; + } - Rng::Seed::Seed(unsigned int seed) + void init(unsigned int seed) { - mGenerator.seed(seed); + sGenerator.seed(seed); } - Rng::Seed& Rng::getSeed() + float rollProbability() { - return sSeed; + return std::uniform_real_distribution(0, 1 - std::numeric_limits::epsilon())(getGenerator()); } - void Rng::init(unsigned int seed) + float rollProbability(Generator& prng) { - sSeed.mGenerator.seed(seed); + return std::uniform_real_distribution(0, 1 - std::numeric_limits::epsilon())(prng); } - float Rng::rollProbability(Seed& seed) + float rollClosedProbability() { - return std::uniform_real_distribution(0, 1 - std::numeric_limits::epsilon())(seed.mGenerator); + return std::uniform_real_distribution(0, 1)(getGenerator()); } - float Rng::rollClosedProbability(Seed& seed) + float rollClosedProbability(Generator& prng) { - return std::uniform_real_distribution(0, 1)(seed.mGenerator); + return std::uniform_real_distribution(0, 1)(prng); } - int Rng::rollDice(int max, Seed& seed) + int rollDice(int max) { - return max > 0 ? std::uniform_int_distribution(0, max - 1)(seed.mGenerator) : 0; + return max > 0 ? std::uniform_int_distribution(0, max - 1)(getGenerator()) : 0; } - unsigned int Rng::generateDefaultSeed() + int rollDice(int max, Generator& prng) { - return static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + return max > 0 ? std::uniform_int_distribution(0, max - 1)(prng) : 0; } + + float deviate(float mean, float deviation) + { + return std::uniform_real_distribution(mean - deviation, mean + deviation)(getGenerator()); + } + + float deviate(float mean, float deviation, Generator& prng) + { + return std::uniform_real_distribution(mean - deviation, mean + deviation)(prng); + } + } diff --git a/components/misc/rng.hpp b/components/misc/rng.hpp index 8efca438d1..5663b86a02 100644 --- a/components/misc/rng.hpp +++ b/components/misc/rng.hpp @@ -3,47 +3,45 @@ #include #include - -namespace Misc -{ +#include /* Provides central implementation of the RNG logic */ -class Rng +namespace Misc::Rng { -public: - class Seed - { - std::mt19937 mGenerator; - public: - Seed(); - Seed(const Seed&) = delete; - Seed(unsigned int seed); - friend class Rng; - }; - - static Seed& getSeed(); + /// The use of a rather minimalistic prng is preferred to avoid saving a lot of state in the save game. + using Generator = std::minstd_rand; + + Generator& getGenerator(); + + std::string serialize(const Generator& prng); + void deserialize(std::string_view data, Generator& prng); + + /// returns default seed for RNG + unsigned int generateDefaultSeed(); /// seed the RNG - static void init(unsigned int seed = generateDefaultSeed()); + void init(unsigned int seed = generateDefaultSeed()); /// return value in range [0.0f, 1.0f) <- note open upper range. - static float rollProbability(Seed& seed = getSeed()); + float rollProbability(); + float rollProbability(Generator& prng); /// return value in range [0.0f, 1.0f] <- note closed upper range. - static float rollClosedProbability(Seed& seed = getSeed()); + float rollClosedProbability(); + float rollClosedProbability(Generator& prng); /// return value in range [0, max) <- note open upper range. - static int rollDice(int max, Seed& seed = getSeed()); + int rollDice(int max); + int rollDice(int max, Generator& prng); /// return value in range [0, 99] - static int roll0to99(Seed& seed = getSeed()) { return rollDice(100, seed); } - - /// returns default seed for RNG - static unsigned int generateDefaultSeed(); -}; + inline int roll0to99(Generator& prng) { return rollDice(100, prng); } + inline int roll0to99() { return rollDice(100); } + float deviate(float mean, float deviation); + float deviate(float mean, float deviation, Generator& prng); } #endif diff --git a/components/misc/span.hpp b/components/misc/span.hpp new file mode 100644 index 0000000000..83a424a674 --- /dev/null +++ b/components/misc/span.hpp @@ -0,0 +1,36 @@ +#ifndef OPENMW_COMPONENTS_MISC_SPAN_H +#define OPENMW_COMPONENTS_MISC_SPAN_H + +#include + +namespace Misc +{ + template + class Span + { + public: + constexpr Span() = default; + + constexpr Span(T* pointer, std::size_t size) + : mPointer(pointer) + , mSize(size) + {} + + template + constexpr Span(Range& range) + : Span(range.data(), range.size()) + {} + + constexpr T* begin() const { return mPointer; } + + constexpr T* end() const { return mPointer + mSize; } + + constexpr std::size_t size() const { return mSize; } + + private: + T* mPointer = nullptr; + std::size_t mSize = 0; + }; +} + +#endif diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index aa2ae105e1..3fa82a27fe 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -4,8 +4,28 @@ #include #include #include - -#include "utf8stream.hpp" +#include +#include +#include + +/* Mapping table to go from uppercase to lowercase for plain ASCII.*/ +static constexpr unsigned char tolowermap[256] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, + 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255 +}; namespace Misc { @@ -22,6 +42,7 @@ class StringUtils template static T argument(T value) noexcept { + static_assert(!std::is_same_v, "std::string_view is not supported"); return value; } @@ -36,97 +57,50 @@ public: /// Plain and simple locale-unaware toLower. Anything from A to Z is lower-cased, multibyte characters are unchanged. /// Don't use std::tolower(char, locale&) because that is abysmally slow. /// Don't use tolower(int) because that depends on global locale. - static char toLower(char c) + static constexpr char toLower(char c) { - return (c >= 'A' && c <= 'Z') ? c + 'a' - 'A' : c; + return tolowermap[static_cast(c)]; } - static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) + static bool ciLess(std::string_view x, std::string_view y) { - // Russian alphabet - if (ch >= 0x0410 && ch < 0x0430) - return ch += 0x20; - - // Cyrillic IO character - if (ch == 0x0401) - return ch += 0x50; - - // Latin alphabet - if (ch >= 0x41 && ch < 0x60) - return ch += 0x20; - - // Deutch characters - if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) - return ch += 0x20; - if (ch == 0x1e9e) - return 0xdf; - - // TODO: probably we will need to support characters from other languages - - return ch; + return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } - static std::string lowerCaseUtf8(const std::string str) + template + static bool ciEqual(const X& x, const Y& y) { - if (str.empty()) - return str; - - // Decode string as utf8 characters, convert to lower case and pack them to string - std::string out; - Utf8Stream stream (str.c_str()); - while (!stream.eof ()) - { - Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); - - if (character <= 0x7f) - out.append(1, static_cast(character)); - else if (character <= 0x7ff) - { - out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else if (character <= 0xffff) - { - out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else - { - out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); - out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - - stream.consume(); - } + if (std::size(x) != std::size(y)) + return false; + return std::equal(std::begin(x), std::end(x), std::begin(y), + [] (char l, char r) { return toLower(l) == toLower(r); }); + } - return out; + template + static auto ciEqual(const char(& x)[n], const char(& y)[n]) + { + static_assert(n > 0); + return ciEqual(std::string_view(x, n - 1), std::string_view(y, n - 1)); } - static bool ciLess(const std::string &x, const std::string &y) { - return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); + template + static auto ciEqual(const char(& x)[n], const T& y) + { + static_assert(n > 0); + return ciEqual(std::string_view(x, n - 1), y); } - static bool ciEqual(const std::string &x, const std::string &y) { - if (x.size() != y.size()) { - return false; - } - std::string::const_iterator xit = x.begin(); - std::string::const_iterator yit = y.begin(); - for (; xit != x.end(); ++xit, ++yit) { - if (toLower(*xit) != toLower(*yit)) { - return false; - } - } - return true; + template + static auto ciEqual(const T& x, const char(& y)[n]) + { + static_assert(n > 0); + return ciEqual(x, std::string_view(y, n - 1)); } - static int ciCompareLen(const std::string &x, const std::string &y, size_t len) + static int ciCompareLen(std::string_view x, std::string_view y, std::size_t len) { - std::string::const_iterator xit = x.begin(); - std::string::const_iterator yit = y.begin(); + std::string_view::const_iterator xit = x.begin(); + std::string_view::const_iterator yit = y.begin(); for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len) { char left = *xit; @@ -157,70 +131,53 @@ public: } /// Returns lower case copy of input string - static std::string lowerCase(const std::string &in) + static std::string lowerCase(std::string_view in) { - std::string out = in; + std::string out(in); lowerCaseInPlace(out); return out; } - struct CiComp + struct CiEqual { - bool operator()(const std::string& left, const std::string& right) const + bool operator()(std::string_view left, std::string_view right) const { - return ciLess(left, right); + return ciEqual(left, right); } }; - - /// Performs a binary search on a sorted container for a string that 'key' starts with - template - static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + struct CiHash { - const Iterator notFound = end; - - while(begin < end) + std::size_t operator()(std::string str) const { - const Iterator middle = begin + (std::distance(begin, end) / 2); - - int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); - - if(comp == 0) - return middle; - else if(comp > 0) - end = middle; - else - begin = middle + 1; + lowerCaseInPlace(str); + return std::hash{}(str); } + }; - return notFound; - } + struct CiComp + { + bool operator()(std::string_view left, std::string_view right) const + { + return ciLess(left, right); + } + }; /** @brief Replaces all occurrences of a string in another string. * * @param str The string to operate on. * @param what The string to replace. * @param with The replacement string. - * @param whatLen The length of the string to replace. - * @param withLen The length of the replacement string. - * * @return A reference to the string passed in @p str. */ - static std::string &replaceAll(std::string &str, const char *what, const char *with, - std::size_t whatLen=std::string::npos, std::size_t withLen=std::string::npos) + static std::string &replaceAll(std::string &str, std::string_view what, std::string_view with) { - if (whatLen == std::string::npos) - whatLen = strlen(what); - - if (withLen == std::string::npos) - withLen = strlen(with); - std::size_t found; std::size_t offset = 0; - while((found = str.find(what, offset, whatLen)) != std::string::npos) + while((found = str.find(what, offset)) != std::string::npos) { - str.replace(found, whatLen, with, withLen); - offset = found + withLen; + str.replace(found, what.size(), with); + offset = found + with.size(); } return str; } @@ -249,55 +206,45 @@ public: static inline void trim(std::string &s) { - // left trim - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) + const auto notSpace = [](char ch) { - return !std::isspace(ch); - })); + // TODO Do we care about multibyte whitespace? + return !std::isspace(static_cast(ch)); + }; + // left trim + s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace)); // right trim - s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) - { - return !std::isspace(ch); - }).base(), s.end()); + s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end()); } template - static inline void split(const std::string& str, Container& cont, const std::string& delims = " ") + static inline void split(std::string_view str, Container& cont, std::string_view delims = " ") { - std::size_t current, previous = 0; - current = str.find_first_of(delims); + std::size_t current = str.find_first_of(delims); + std::size_t previous = 0; while (current != std::string::npos) { - cont.push_back(str.substr(previous, current - previous)); + cont.emplace_back(str.substr(previous, current - previous)); previous = current + 1; current = str.find_first_of(delims, previous); } - cont.push_back(str.substr(previous, current - previous)); + cont.emplace_back(str.substr(previous, current - previous)); } - // TODO: use the std::string_view once we will use the C++17. - // It should allow us to avoid data copying while we still will support both string and literal arguments. - - static inline void replaceAll(std::string& data, std::string toSearch, std::string replaceStr) + static inline void replaceLast(std::string& str, std::string_view substr, std::string_view with) { - size_t pos = data.find(toSearch); - - while( pos != std::string::npos) - { - data.replace(pos, toSearch.size(), replaceStr); - pos = data.find(toSearch, pos + replaceStr.size()); - } + size_t pos = str.rfind(substr); + if (pos == std::string::npos) + return; + str.replace(pos, substr.size(), with); } - static inline void replaceLast(std::string& str, std::string substr, std::string with) - { - size_t pos = str.rfind(substr); - if (pos == std::string::npos) - return; - - str.replace(pos, substr.size(), with); - } + static inline bool ciEndsWith(std::string_view s, std::string_view suffix) + { + return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(), + [](char l, char r) { return toLower(l) == toLower(r); }); + }; }; } diff --git a/components/misc/strongtypedef.hpp b/components/misc/strongtypedef.hpp new file mode 100644 index 0000000000..2a9e4c3a8b --- /dev/null +++ b/components/misc/strongtypedef.hpp @@ -0,0 +1,38 @@ +#ifndef OPENMW_COMPONENTS_MISC_STRONGTYPEDEF_H +#define OPENMW_COMPONENTS_MISC_STRONGTYPEDEF_H + +#include + +namespace Misc +{ + template + struct StrongTypedef + { + T mValue; + + StrongTypedef() = default; + + explicit StrongTypedef(const T& value) : mValue(value) {} + + explicit StrongTypedef(T&& value) : mValue(std::move(value)) {} + + operator const T&() const { return mValue; } + + operator T&() { return mValue; } + + StrongTypedef& operator++() + { + ++mValue; + return *this; + } + + StrongTypedef operator++(int) + { + StrongTypedef copy(*this); + operator++(); + return copy; + } + }; +} + +#endif diff --git a/components/misc/thread.cpp b/components/misc/thread.cpp new file mode 100644 index 0000000000..ca811fdd2f --- /dev/null +++ b/components/misc/thread.cpp @@ -0,0 +1,70 @@ +#include "thread.hpp" + +#include + +#include +#include + +#ifdef __linux__ + +#include +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + sched_param param; + param.sched_priority = 0; + if (pthread_setschedparam(pthread_self(), SCHED_IDLE, ¶m) == 0) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << std::strerror(errno); + } +} + +#elif defined(WIN32) + +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST)) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << GetLastError(); + } +} + +#elif defined(__FreeBSD__) + +#include +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + struct rtprio prio; + prio.type = RTP_PRIO_IDLE; + prio.prio = RTP_PRIO_MAX; + if (rtprio_thread(RTP_SET, 0, &prio) == 0) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << std::strerror(errno); + } +} + +#else + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + Log(Debug::Warning) << "Idle thread priority is not supported on this system"; + } +} + +#endif diff --git a/components/misc/thread.hpp b/components/misc/thread.hpp new file mode 100644 index 0000000000..191c43bba1 --- /dev/null +++ b/components/misc/thread.hpp @@ -0,0 +1,11 @@ +#ifndef OPENMW_COMPONENTS_MISC_THREAD_H +#define OPENMW_COMPONENTS_MISC_THREAD_H + +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority(); +} + +#endif diff --git a/components/misc/timer.hpp b/components/misc/timer.hpp new file mode 100644 index 0000000000..910d45bc03 --- /dev/null +++ b/components/misc/timer.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_COMPONENTS_MISC_TIMER_H +#define OPENMW_COMPONENTS_MISC_TIMER_H + +#include "rng.hpp" + +namespace Misc +{ + enum class TimerStatus + { + Waiting, + Elapsed, + }; + + class DeviatingPeriodicTimer + { + public: + explicit DeviatingPeriodicTimer(float period, float deviation, float timeLeft) + : mPeriod(period), mDeviation(deviation), mTimeLeft(timeLeft) + {} + + TimerStatus update(float duration, Rng::Generator& prng) + { + if (mTimeLeft > 0) + { + mTimeLeft -= duration; + return TimerStatus::Waiting; + } + + mTimeLeft = Rng::deviate(mPeriod, mDeviation, prng); + return TimerStatus::Elapsed; + } + + void reset(float timeLeft) { mTimeLeft = timeLeft; } + + private: + const float mPeriod; + const float mDeviation; + float mTimeLeft; + }; +} + +#endif diff --git a/components/misc/typetraits.hpp b/components/misc/typetraits.hpp new file mode 100644 index 0000000000..4c6a7e731b --- /dev/null +++ b/components/misc/typetraits.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_COMPONENTS_MISC_TYPETRAITS_H +#define OPENMW_COMPONENTS_MISC_TYPETRAITS_H + +#include +#include + +namespace Misc +{ + template + struct IsOptional : std::false_type {}; + + template + struct IsOptional> : std::true_type {}; + + template + inline constexpr bool isOptional = IsOptional::value; +} + +#endif diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index e499d15e60..847d73ceb7 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -2,6 +2,8 @@ #define MISC_UTF8ITER_HPP #include +#include +#include #include class Utf8Stream @@ -20,7 +22,10 @@ public: } Utf8Stream (const char * str) : - cur ((unsigned char*) str), nxt ((unsigned char*) str), end ((unsigned char*) str + strlen(str)), val(Utf8Stream::sBadChar()) + cur (reinterpret_cast(str)), + nxt (reinterpret_cast(str)), + end (reinterpret_cast(str) + strlen(str)), + val(Utf8Stream::sBadChar()) { } @@ -29,6 +34,11 @@ public: { } + Utf8Stream (std::string_view str) : + Utf8Stream (reinterpret_cast(str.data()), reinterpret_cast(str.data() + str.size())) + { + } + bool eof () const { return cur == end; @@ -87,6 +97,70 @@ public: return std::make_pair (chr, cur); } + static UnicodeChar toLowerUtf8(UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch + 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch + 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch + 0x20; + + // German characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch + 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string& str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + private: static std::pair octet_count (unsigned char octet) diff --git a/components/misc/weakcache.hpp b/components/misc/weakcache.hpp index 022a722dba..3bbb0812cd 100644 --- a/components/misc/weakcache.hpp +++ b/components/misc/weakcache.hpp @@ -22,8 +22,8 @@ namespace Misc public: iterator(WeakCache* cache, typename Map::iterator current, typename Map::iterator end); iterator& operator++(); - bool operator==(const iterator& other); - bool operator!=(const iterator& other); + bool operator==(const iterator& other) const; + bool operator!=(const iterator& other) const; StrongPtr operator*(); private: WeakCache* mCache; @@ -74,13 +74,13 @@ namespace Misc } template - bool WeakCache::iterator::operator==(const iterator& other) + bool WeakCache::iterator::operator==(const iterator& other) const { return mCurrent == other.mCurrent; } template - bool WeakCache::iterator::operator!=(const iterator& other) + bool WeakCache::iterator::operator!=(const iterator& other) const { return !(*this == other); } diff --git a/components/myguiplatform/myguidatamanager.cpp b/components/myguiplatform/myguidatamanager.cpp index c90e092215..70af0774f7 100644 --- a/components/myguiplatform/myguidatamanager.cpp +++ b/components/myguiplatform/myguidatamanager.cpp @@ -1,12 +1,30 @@ #include "myguidatamanager.hpp" +#include +#include + #include -#include -#include +#include +#include #include +namespace +{ + class DataStream final : public MyGUI::DataStream + { + public: + explicit DataStream(std::unique_ptr&& stream) + : MyGUI::DataStream(stream.get()) + , mOwnedStream(std::move(stream)) + {} + + private: + std::unique_ptr mOwnedStream; + }; +} + namespace osgMyGUI { @@ -15,18 +33,14 @@ void DataManager::setResourcePath(const std::string &path) mResourcePath = path; } -MyGUI::IDataStream *DataManager::getData(const std::string &name) +DataManager::DataManager(const VFS::Manager* vfs) + : mVfs(vfs) { - std::string fullpath = getDataPath(name); - std::unique_ptr stream; - stream.reset(new boost::filesystem::ifstream); - stream->open(fullpath, std::ios::binary); - if (stream->fail()) - { - Log(Debug::Error) << "DataManager::getData: Failed to open '" << name << "'"; - return nullptr; - } - return new MyGUI::DataFileStream(stream.release()); +} + +MyGUI::IDataStream *DataManager::getData(const std::string &name) const +{ + return new DataStream(mVfs->get(mResourcePath + "/" + name)); } void DataManager::freeData(MyGUI::IDataStream *data) @@ -34,29 +48,27 @@ void DataManager::freeData(MyGUI::IDataStream *data) delete data; } -bool DataManager::isDataExist(const std::string &name) +bool DataManager::isDataExist(const std::string &name) const { - std::string fullpath = mResourcePath + "/" + name; - return boost::filesystem::exists(fullpath); + return mVfs->exists(mResourcePath + "/" + name); } -const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern) +const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern) const { - // TODO: pattern matching (unused?) - static MyGUI::VectorString strings; - strings.clear(); - strings.push_back(getDataPath(pattern)); - return strings; + throw std::runtime_error("DataManager::getDataListNames is not implemented - VFS is used"); } -const std::string &DataManager::getDataPath(const std::string &name) +const std::string &DataManager::getDataPath(const std::string &name) const { static std::string result; result.clear(); + + if (name.empty()) + return mResourcePath; + if (!isDataExist(name)) - { return result; - } + result = mResourcePath + "/" + name; return result; } diff --git a/components/myguiplatform/myguidatamanager.hpp b/components/myguiplatform/myguidatamanager.hpp index a97c6ad2e0..281aedd137 100644 --- a/components/myguiplatform/myguidatamanager.hpp +++ b/components/myguiplatform/myguidatamanager.hpp @@ -3,22 +3,27 @@ #include +#include + +#include + namespace osgMyGUI { - class DataManager : public MyGUI::DataManager { public: void initialise() {} void shutdown() {} + DataManager(const VFS::Manager* vfs); + void setResourcePath(const std::string& path); /** Get data stream from specified resource name. @param _name Resource name (usually file name). */ - MyGUI::IDataStream* getData(const std::string& _name) override; + MyGUI::IDataStream* getData(const std::string& _name) const override; /** Free data stream. @param _data Data stream. @@ -28,21 +33,23 @@ public: /** Is data with specified name exist. @param _name Resource name. */ - bool isDataExist(const std::string& _name) override; + bool isDataExist(const std::string& _name) const override; /** Get all data names with names that matches pattern. @param _pattern Pattern to match (for example "*.layout"). */ - const MyGUI::VectorString& getDataListNames(const std::string& _pattern) override; + const MyGUI::VectorString& getDataListNames(const std::string& _pattern) const override; /** Get full path to data. @param _name Resource name. @return Return full path to specified data. */ - const std::string& getDataPath(const std::string& _name) override; + const std::string& getDataPath(const std::string& _name) const override; private: std::string mResourcePath; + + const VFS::Manager* mVfs; }; } diff --git a/components/myguiplatform/myguiloglistener.cpp b/components/myguiplatform/myguiloglistener.cpp index a22ac8fc5d..74b4b30813 100644 --- a/components/myguiplatform/myguiloglistener.cpp +++ b/components/myguiplatform/myguiloglistener.cpp @@ -2,11 +2,15 @@ #include +#include + namespace osgMyGUI { void CustomLogListener::open() { mStream.open(boost::filesystem::path(mFileName), std::ios_base::out); + if (!mStream.is_open()) + Log(Debug::Error) << "Unable to create MyGUI log with path " << mFileName; } void CustomLogListener::close() diff --git a/components/myguiplatform/myguiplatform.cpp b/components/myguiplatform/myguiplatform.cpp index dfb2e6539d..a2964692a6 100644 --- a/components/myguiplatform/myguiplatform.cpp +++ b/components/myguiplatform/myguiplatform.cpp @@ -7,7 +7,7 @@ namespace osgMyGUI { -Platform::Platform(osgViewer::Viewer *viewer, osg::Group *guiRoot, Resource::ImageManager *imageManager, float uiScalingFactor) +Platform::Platform(osgViewer::Viewer *viewer, osg::Group *guiRoot, Resource::ImageManager *imageManager, const VFS::Manager* vfs, float uiScalingFactor) : mRenderManager(nullptr) , mDataManager(nullptr) , mLogManager(nullptr) @@ -15,7 +15,7 @@ Platform::Platform(osgViewer::Viewer *viewer, osg::Group *guiRoot, Resource::Ima { mLogManager = new MyGUI::LogManager(); mRenderManager = new RenderManager(viewer, guiRoot, imageManager, uiScalingFactor); - mDataManager = new DataManager(); + mDataManager = new DataManager(vfs); } Platform::~Platform() diff --git a/components/myguiplatform/myguiplatform.hpp b/components/myguiplatform/myguiplatform.hpp index 5ffbe0be70..e8d3378e66 100644 --- a/components/myguiplatform/myguiplatform.hpp +++ b/components/myguiplatform/myguiplatform.hpp @@ -3,6 +3,8 @@ #include +#include + namespace osgViewer { class Viewer; @@ -30,7 +32,7 @@ namespace osgMyGUI class Platform { public: - Platform(osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ImageManager* imageManager, float uiScalingFactor); + Platform(osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ImageManager* imageManager, const VFS::Manager* vfs, float uiScalingFactor); ~Platform(); diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index bba7df702b..9d48a0d35a 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -1,10 +1,8 @@ #include "myguirendermanager.hpp" -#include #include #include -#include #include #include @@ -13,6 +11,8 @@ #include #include +#include +#include #include "myguitexture.hpp" @@ -46,7 +46,7 @@ class Drawable : public osg::Drawable { public: // Stage 0: update widget animations and controllers. Run during the Update traversal. - class FrameUpdate : public osg::Drawable::UpdateCallback + class FrameUpdate : public SceneUtil::NodeCallback { public: FrameUpdate() @@ -59,10 +59,9 @@ public: mRenderManager = renderManager; } - void update(osg::NodeVisitor*, osg::Drawable*) override + void operator()(osg::Node*, osg::NodeVisitor*) { - if (mRenderManager) - mRenderManager->update(); + mRenderManager->update(); } private: @@ -70,7 +69,7 @@ public: }; // Stage 1: collect draw calls. Run during the Cull traversal. - class CollectDrawCalls : public osg::Drawable::CullCallback + class CollectDrawCalls : public SceneUtil::NodeCallback { public: CollectDrawCalls() @@ -83,13 +82,9 @@ public: mRenderManager = renderManager; } - bool cull(osg::NodeVisitor*, osg::Drawable*, osg::State*) const override + void operator()(osg::Node*, osg::NodeVisitor*) { - if (!mRenderManager) - return false; - mRenderManager->collectDrawCalls(); - return false; } private: @@ -123,11 +118,15 @@ public: state->apply(); } + // A GUI element without an associated texture would be extremely rare. + // It is worth it to use a dummy 1x1 black texture sampler instead of either adding a conditional or relinking shaders. osg::Texture2D* texture = batch.mTexture; if(texture) state->applyTextureAttribute(0, texture); + else + state->applyTextureAttribute(0, mDummyTexture); - osg::GLBufferObject* bufferobject = state->isVertexBufferObjectSupported() ? vbo->getOrCreateGLBufferObject(state->getContextID()) : 0; + osg::GLBufferObject* bufferobject = state->isVertexBufferObjectSupported() ? vbo->getOrCreateGLBufferObject(state->getContextID()) : nullptr; if (bufferobject) { state->bindVertexBufferObject(bufferobject); @@ -138,9 +137,9 @@ public: } else { - glVertexPointer(3, GL_FLOAT, sizeof(MyGUI::Vertex), (char*)vbo->getArray(0)->getDataPointer()); - glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(MyGUI::Vertex), (char*)vbo->getArray(0)->getDataPointer() + 12); - glTexCoordPointer(2, GL_FLOAT, sizeof(MyGUI::Vertex), (char*)vbo->getArray(0)->getDataPointer() + 16); + glVertexPointer(3, GL_FLOAT, sizeof(MyGUI::Vertex), reinterpret_cast(vbo->getArray(0)->getDataPointer())); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(MyGUI::Vertex), reinterpret_cast(vbo->getArray(0)->getDataPointer()) + 12); + glTexCoordPointer(2, GL_FLOAT, sizeof(MyGUI::Vertex), reinterpret_cast(vbo->getArray(0)->getDataPointer()) + 16); } glDrawArrays(GL_TRIANGLES, 0, batch.mVertexCount); @@ -185,6 +184,10 @@ public: mStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); mStateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + mDummyTexture = new osg::Texture2D; + mDummyTexture->setInternalFormat(GL_RGB); + mDummyTexture->setTextureSize(1,1); + // need to flip tex coords since MyGUI uses DirectX convention of top left image origin osg::Matrix flipMat; flipMat.preMultTranslate(osg::Vec3f(0,1,0)); @@ -197,6 +200,7 @@ public: , mStateSet(copy.mStateSet) , mWriteTo(0) , mReadFrom(0) + , mDummyTexture(copy.mDummyTexture) { } @@ -227,6 +231,11 @@ public: mBatchVector[mWriteTo].clear(); } + osg::StateSet* getDrawableStateSet() + { + return mStateSet; + } + META_Object(osgMyGUI, Drawable) private: @@ -238,6 +247,8 @@ private: int mWriteTo; mutable int mReadFrom; + + osg::ref_ptr mDummyTexture; }; class OSGVertexBuffer : public MyGUI::IVertexBuffer @@ -263,7 +274,7 @@ public: osg::VertexBufferObject* getVertexBuffer(); void setVertexCount(size_t count) override; - size_t getVertexCount() override; + size_t getVertexCount() const override; MyGUI::Vertex *lock() override; void unlock() override; @@ -290,7 +301,7 @@ void OSGVertexBuffer::setVertexCount(size_t count) mNeedVertexCount = count; } -size_t OSGVertexBuffer::getVertexCount() +size_t OSGVertexBuffer::getVertexCount() const { return mNeedVertexCount; } @@ -413,6 +424,16 @@ void RenderManager::shutdown() mSceneRoot->removeChild(mGuiRoot); } +void RenderManager::enableShaders(Shader::ShaderManager& shaderManager) +{ + auto vertexShader = shaderManager.getShader("gui_vertex.glsl", {}, osg::Shader::VERTEX); + auto fragmentShader = shaderManager.getShader("gui_fragment.glsl", {}, osg::Shader::FRAGMENT); + auto program = shaderManager.getProgram(vertexShader, fragmentShader); + + mDrawable->getDrawableStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON); + mDrawable->getDrawableStateSet()->addUniform(new osg::Uniform("diffuseMap", 0)); +} + MyGUI::IVertexBuffer* RenderManager::createVertexBuffer() { return new OSGVertexBuffer(); @@ -438,11 +459,14 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text batch.mVertexBuffer = static_cast(buffer)->getVertexBuffer(); batch.mArray = static_cast(buffer)->getVertexArray(); static_cast(buffer)->markUsed(); - if (texture) + + if (OSGTexture* osgtexture = static_cast(texture)) { - batch.mTexture = static_cast(texture)->getTexture(); + batch.mTexture = osgtexture->getTexture(); if (batch.mTexture->getDataVariance() == osg::Object::DYNAMIC) mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin() + if (!mInjectState && osgtexture->getInjectState()) + batch.mStateSet = osgtexture->getInjectState(); } if (mInjectState) batch.mStateSet = mInjectState; @@ -560,4 +584,12 @@ bool RenderManager::checkTexture(MyGUI::ITexture* _texture) return true; } +void RenderManager::registerShader( + const std::string& _shaderName, + const std::string& _vertexProgramFile, + const std::string& _fragmentProgramFile) +{ + MYGUI_PLATFORM_LOG(Warning, "osgMyGUI::RenderManager::registerShader is not implemented"); +} + } diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp index 72abebd186..0d1ad4fb41 100644 --- a/components/myguiplatform/myguirendermanager.hpp +++ b/components/myguiplatform/myguirendermanager.hpp @@ -10,6 +10,11 @@ namespace Resource class ImageManager; } +namespace Shader +{ + class ShaderManager; +} + namespace osgViewer { class Viewer; @@ -60,6 +65,8 @@ public: void initialise(); void shutdown(); + void enableShaders(Shader::ShaderManager& shaderManager); + void setScalingFactor(float factor); static RenderManager& getInstance() { return *getInstancePtr(); } @@ -70,7 +77,8 @@ public: const MyGUI::IntSize& getViewSize() const override { return mViewSize; } /** @see RenderManager::getVertexFormat */ - MyGUI::VertexColourType getVertexFormat() override { return mVertexFormat; } + MyGUI::VertexColourType getVertexFormat() const override + { return mVertexFormat; } /** @see RenderManager::isFormatSupported */ bool isFormatSupported(MyGUI::PixelFormat format, MyGUI::TextureUsage usage) override; @@ -102,16 +110,13 @@ public: void setInjectState(osg::StateSet* stateSet); /** @see IRenderTarget::getInfo */ - const MyGUI::RenderTargetInfo& getInfo() override { return mInfo; } + const MyGUI::RenderTargetInfo& getInfo() const override { return mInfo; } bool checkTexture(MyGUI::ITexture* _texture); - // setViewSize() is a part of MyGUI::RenderManager interface since 3.4.0 release -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,4,0) - void setViewSize(int width, int height); -#else void setViewSize(int width, int height) override; -#endif + + void registerShader(const std::string& _shaderName, const std::string& _vertexProgramFile, const std::string& _fragmentProgramFile) override; /*internal:*/ diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index 598f5a14e9..d120cb52f3 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -21,9 +22,10 @@ namespace osgMyGUI { } - OSGTexture::OSGTexture(osg::Texture2D *texture) + OSGTexture::OSGTexture(osg::Texture2D *texture, osg::StateSet *injectState) : mImageManager(nullptr) , mTexture(texture) + , mInjectState(injectState) , mFormat(MyGUI::PixelFormat::Unknow) , mUsage(MyGUI::TextureUsage::Default) , mNumElemBytes(0) @@ -115,16 +117,6 @@ namespace osgMyGUI Log(Debug::Warning) << "Would save image to file " << fname; } - int OSGTexture::getWidth() - { - return mWidth; - } - - int OSGTexture::getHeight() - { - return mHeight; - } - void *OSGTexture::lock(MyGUI::TextureUsage /*access*/) { if (!mTexture.valid()) @@ -167,15 +159,12 @@ namespace osgMyGUI mLockedImage = nullptr; } - bool OSGTexture::isLocked() - { - return mLockedImage.valid(); - } - // Render-to-texture not currently implemented. MyGUI::IRenderTarget* OSGTexture::getRenderTarget() { return nullptr; } + void OSGTexture::setShader(const std::string& _shaderName) + { Log(Debug::Warning) << "OSGTexture::setShader is not implemented"; } } diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp index 6baeb74591..4f7ff8f116 100644 --- a/components/myguiplatform/myguitexture.hpp +++ b/components/myguiplatform/myguitexture.hpp @@ -9,6 +9,7 @@ namespace osg { class Image; class Texture2D; + class StateSet; } namespace Resource @@ -25,6 +26,7 @@ namespace osgMyGUI osg::ref_ptr mLockedImage; osg::ref_ptr mTexture; + osg::ref_ptr mInjectState; MyGUI::PixelFormat mFormat; MyGUI::TextureUsage mUsage; size_t mNumElemBytes; @@ -34,9 +36,11 @@ namespace osgMyGUI public: OSGTexture(const std::string &name, Resource::ImageManager* imageManager); - OSGTexture(osg::Texture2D* texture); + OSGTexture(osg::Texture2D* texture, osg::StateSet* injectState = nullptr); virtual ~OSGTexture(); + osg::StateSet* getInjectState() { return mInjectState; } + const std::string& getName() const override { return mName; } void createManual(int width, int height, MyGUI::TextureUsage usage, MyGUI::PixelFormat format) override; @@ -47,17 +51,19 @@ namespace osgMyGUI void* lock(MyGUI::TextureUsage access) override; void unlock() override; - bool isLocked() override; + bool isLocked() const override { return mLockedImage.valid(); } - int getWidth() override; - int getHeight() override; + int getWidth() const override { return mWidth; } + int getHeight() const override { return mHeight; } - MyGUI::PixelFormat getFormat() override { return mFormat; } - MyGUI::TextureUsage getUsage() override { return mUsage; } - size_t getNumElemBytes() override { return mNumElemBytes; } + MyGUI::PixelFormat getFormat() const override { return mFormat; } + MyGUI::TextureUsage getUsage() const override { return mUsage; } + size_t getNumElemBytes() const override { return mNumElemBytes; } MyGUI::IRenderTarget *getRenderTarget() override; + void setShader(const std::string& _shaderName) override; + /*internal:*/ osg::Texture2D *getTexture() const { return mTexture.get(); } }; diff --git a/components/myguiplatform/scalinglayer.cpp b/components/myguiplatform/scalinglayer.cpp index 07a5161b2d..75a149c810 100644 --- a/components/myguiplatform/scalinglayer.cpp +++ b/components/myguiplatform/scalinglayer.cpp @@ -37,7 +37,7 @@ namespace osgMyGUI mTarget->doRender(_buffer, _texture, _count); } - const MyGUI::RenderTargetInfo& getInfo() override + const MyGUI::RenderTargetInfo& getInfo() const override { mInfo = mTarget->getInfo(); mInfo.hOffset = mHOffset; @@ -51,7 +51,7 @@ namespace osgMyGUI MyGUI::IRenderTarget* mTarget; MyGUI::IntSize mViewSize; float mHOffset, mVOffset; - MyGUI::RenderTargetInfo mInfo; + mutable MyGUI::RenderTargetInfo mInfo; }; MyGUI::ILayerItem *ScalingLayer::getLayerItemByPoint(int _left, int _top) const @@ -72,8 +72,8 @@ namespace osgMyGUI _left -= globalViewSize.width/2; _top -= globalViewSize.height/2; - _left /= scale; - _top /= scale; + _left = static_cast(_left/scale); + _top = static_cast(_top/scale); _left += mViewSize.width/2; _top += mViewSize.height/2; @@ -82,8 +82,8 @@ namespace osgMyGUI float ScalingLayer::getScaleFactor() const { MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - float w = viewSize.width; - float h = viewSize.height; + float w = static_cast(viewSize.width); + float h = static_cast(viewSize.height); float heightScale = (h / mViewSize.height); float widthScale = (w / mViewSize.width); @@ -101,8 +101,8 @@ namespace osgMyGUI MyGUI::IntSize globalViewSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntSize viewSize = globalViewSize; float scale = getScaleFactor(); - viewSize.width /= scale; - viewSize.height /= scale; + viewSize.width = static_cast(viewSize.width / scale); + viewSize.height = static_cast(viewSize.height / scale); float hoffset = (globalViewSize.width - mViewSize.width*getScaleFactor())/2.f / static_cast(globalViewSize.width); float voffset = (globalViewSize.height - mViewSize.height*getScaleFactor())/2.f / static_cast(globalViewSize.height); diff --git a/components/navmeshtool/protocol.cpp b/components/navmeshtool/protocol.cpp new file mode 100644 index 0000000000..656d5ab4d6 --- /dev/null +++ b/components/navmeshtool/protocol.cpp @@ -0,0 +1,159 @@ +#include "protocol.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace NavMeshTool +{ + namespace + { + template + struct Format : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Message>> + { + if constexpr (mode == Serialization::Mode::Write) + visitor(*this, messageMagic); + else + { + static_assert(mode == Serialization::Mode::Read); + char magic[std::size(messageMagic)]; + visitor(*this, magic); + if (std::memcmp(magic, messageMagic, sizeof(magic)) != 0) + throw BadMessageMagic(); + } + visitor(*this, value.mType); + visitor(*this, value.mSize); + if constexpr (mode == Serialization::Mode::Write) + visitor(*this, value.mData, value.mSize); + else + visitor(*this, value.mData); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, ExpectedCells>> + { + visitor(*this, value.mCount); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, ProcessedCells>> + { + visitor(*this, value.mCount); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, ExpectedTiles>> + { + visitor(*this, value.mCount); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, GeneratedTiles>> + { + visitor(*this, value.mCount); + } + }; + + template + std::vector serializeToVector(const T& value) + { + constexpr Format format; + Serialization::SizeAccumulator sizeAccumulator; + format(sizeAccumulator, value); + std::vector buffer(sizeAccumulator.value()); + format(Serialization::BinaryWriter(buffer.data(), buffer.data() + buffer.size()), value); + return buffer; + } + + template + std::vector serializeImpl(const T& value) + { + const auto data = serializeToVector(value); + const Message message {static_cast(T::sMessageType), static_cast(data.size()), data.data()}; + return serializeToVector(message); + } + } + + std::vector serialize(const ExpectedCells& value) + { + return serializeImpl(value); + } + + std::vector serialize(const ProcessedCells& value) + { + return serializeImpl(value); + } + + std::vector serialize(const ExpectedTiles& value) + { + return serializeImpl(value); + } + + std::vector serialize(const GeneratedTiles& value) + { + return serializeImpl(value); + } + + const std::byte* deserialize(const std::byte* begin, const std::byte* end, Message& message) + { + try + { + constexpr Format format; + Serialization::BinaryReader reader(begin, end); + format(reader, message); + return message.mData + message.mSize; + } + catch (const Serialization::NotEnoughData&) + { + return begin; + } + } + + TypedMessage decode(const Message& message) + { + constexpr Format format; + Serialization::BinaryReader reader(message.mData, message.mData + message.mSize); + switch (static_cast(message.mType)) + { + case MessageType::ExpectedCells: + { + ExpectedCells value; + format(reader, value); + return value; + } + case MessageType::ProcessedCells: + { + ProcessedCells value; + format(reader, value); + return value; + } + case MessageType::ExpectedTiles: + { + ExpectedTiles value; + format(reader, value); + return value; + } + case MessageType::GeneratedTiles: + { + GeneratedTiles value; + format(reader, value); + return value; + } + } + throw std::logic_error("Unsupported message type: " + std::to_string(message.mType)); + } +} diff --git a/components/navmeshtool/protocol.hpp b/components/navmeshtool/protocol.hpp new file mode 100644 index 0000000000..ddb68a716c --- /dev/null +++ b/components/navmeshtool/protocol.hpp @@ -0,0 +1,78 @@ +#ifndef OPENMW_COMPONENTS_NAVMESHTOOL_PROTOCOL_H +#define OPENMW_COMPONENTS_NAVMESHTOOL_PROTOCOL_H + +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + inline constexpr char messageMagic[] = {'n', 'v', 't', 'm'}; + + struct BadMessageMagic : std::runtime_error + { + BadMessageMagic() : std::runtime_error("Bad Message magic") {} + }; + + enum class MessageType : std::uint64_t + { + ExpectedCells = 1, + ProcessedCells = 2, + ExpectedTiles = 3, + GeneratedTiles = 4, + }; + + struct Message + { + std::uint64_t mType = 0; + std::uint64_t mSize = 0; + const std::byte* mData = nullptr; + }; + + struct ExpectedCells + { + static constexpr MessageType sMessageType = MessageType::ExpectedCells; + std::uint64_t mCount = 0; + }; + + struct ProcessedCells + { + static constexpr MessageType sMessageType = MessageType::ProcessedCells; + std::uint64_t mCount = 0; + }; + + struct ExpectedTiles + { + static constexpr MessageType sMessageType = MessageType::ExpectedTiles; + std::uint64_t mCount = 0; + }; + + struct GeneratedTiles + { + static constexpr MessageType sMessageType = MessageType::GeneratedTiles; + std::uint64_t mCount = 0; + }; + + using TypedMessage = std::variant< + ExpectedCells, + ProcessedCells, + ExpectedTiles, + GeneratedTiles + >; + + std::vector serialize(const ExpectedCells& value); + + std::vector serialize(const ProcessedCells& value); + + std::vector serialize(const ExpectedTiles& value); + + std::vector serialize(const GeneratedTiles& value); + + const std::byte* deserialize(const std::byte* begin, const std::byte* end, Message& message); + + TypedMessage decode(const Message& message); +} + +#endif diff --git a/components/nif/base.hpp b/components/nif/base.hpp index 711272abb0..3c7ee160ea 100644 --- a/components/nif/base.hpp +++ b/components/nif/base.hpp @@ -11,11 +11,11 @@ namespace Nif { // An extra data record. All the extra data connected to an object form a linked list. -class Extra : public Record +struct Extra : public Record { -public: std::string name; ExtraPtr next; // Next extra data record in the list + unsigned int recordSize{0u}; void read(NIFStream *nif) override { @@ -24,16 +24,27 @@ public: else if (nif->getVersion() <= NIFStream::generateVersion(4,2,2,0)) { next.read(nif); - nif->getUInt(); // Size of the record + recordSize = nif->getUInt(); } } void post(NIFFile *nif) override { next.post(nif); } }; -class Controller : public Record +struct Controller : public Record { -public: + enum Flags { + Flag_Active = 0x8 + }; + + enum ExtrapolationMode + { + Cycle = 0, + Reverse = 2, + Constant = 4, + Mask = 6 + }; + ControllerPtr next; int flags; float frequency, phase; @@ -42,12 +53,14 @@ public: void read(NIFStream *nif) override; void post(NIFFile *nif) override; + + bool isActive() const { return flags & Flag_Active; } + ExtrapolationMode extrapolationMode() const { return static_cast(flags & Mask); } }; /// Has name, extra-data and controller -class Named : public Record +struct Named : public Record { -public: std::string name; ExtraPtr extra; ExtraList extralist; diff --git a/components/nif/controlled.cpp b/components/nif/controlled.cpp index ab2b8dc173..0491dd7d98 100644 --- a/components/nif/controlled.cpp +++ b/components/nif/controlled.cpp @@ -34,11 +34,12 @@ namespace Nif mipmap = nif->getUInt(); alpha = nif->getUInt(); - nif->getChar(); // always 1 + // Renderer hints, typically of no use for us + /* bool mIsStatic = */nif->getChar(); if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,103)) - nif->getBoolean(); // Direct rendering + /* bool mDirectRendering = */nif->getBoolean(); if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,4)) - nif->getBoolean(); // NiPersistentSrcTextureRendererData is used instead of NiPixelData + /* bool mPersistRenderData = */nif->getBoolean(); } void NiSourceTexture::post(NIFFile *nif) @@ -47,6 +48,11 @@ namespace Nif data.post(nif); } + void BSShaderTextureSet::read(NIFStream *nif) + { + nif->getSizedStrings(textures, nif->getUInt()); + } + void NiParticleModifier::read(NIFStream *nif) { next.read(nif); @@ -94,11 +100,11 @@ namespace Nif NiParticleModifier::read(nif); mBounceFactor = nif->getFloat(); - if (nif->getVersion() >= NIFStream::generateVersion(4,2,2,0)) + if (nif->getVersion() >= NIFStream::generateVersion(4,2,0,2)) { // Unused in NifSkope. Need to figure out what these do. - /*bool spawnOnCollision = */nif->getBoolean(); - /*bool dieOnCollision = */nif->getBoolean(); + /*bool mSpawnOnCollision = */nif->getBoolean(); + /*bool mDieOnCollision = */nif->getBoolean(); } } @@ -106,11 +112,10 @@ namespace Nif { NiParticleCollider::read(nif); - /*unknown*/nif->getFloat(); - - for (int i=0;i<10;++i) - /*unknown*/nif->getFloat(); - + mExtents = nif->getVector2(); + mPosition = nif->getVector3(); + mXVector = nif->getVector3(); + mYVector = nif->getVector3(); mPlaneNormal = nif->getVector3(); mPlaneDistance = nif->getFloat(); } @@ -119,12 +124,9 @@ namespace Nif { NiParticleModifier::read(nif); - /* - byte (0 or 1) - float (1) - float*3 - */ - nif->skip(17); + /* bool mRandomInitialAxis = */nif->getChar(); + /* osg::Vec3f mInitialAxis = */nif->getVector3(); + /* float mRotationSpeed = */nif->getFloat(); } void NiSphericalCollider::read(NIFStream* nif) diff --git a/components/nif/controlled.hpp b/components/nif/controlled.hpp index 57d538f839..5094f1fd87 100644 --- a/components/nif/controlled.hpp +++ b/components/nif/controlled.hpp @@ -29,9 +29,8 @@ namespace Nif { -class NiSourceTexture : public Named +struct NiSourceTexture : public Named { -public: // Is this an external (references a separate texture file) or // internal (data is inside the nif itself) texture? bool external; @@ -66,6 +65,24 @@ public: void post(NIFFile *nif) override; }; +struct BSShaderTextureSet : public Record +{ + enum TextureType + { + TextureType_Base = 0, + TextureType_Normal = 1, + TextureType_Glow = 2, + TextureType_Parallax = 3, + TextureType_Env = 4, + TextureType_EnvMask = 5, + TextureType_Subsurface = 6, + TextureType_BackLighting = 7 + }; + std::vector textures; + + void read(NIFStream *nif) override; +}; + struct NiParticleModifier : public Record { NiParticleModifierPtr next; @@ -75,27 +92,24 @@ struct NiParticleModifier : public Record void post(NIFFile *nif) override; }; -class NiParticleGrowFade : public NiParticleModifier +struct NiParticleGrowFade : public NiParticleModifier { -public: float growTime; float fadeTime; void read(NIFStream *nif) override; }; -class NiParticleColorModifier : public NiParticleModifier +struct NiParticleColorModifier : public NiParticleModifier { -public: NiColorDataPtr data; void read(NIFStream *nif) override; void post(NIFFile *nif) override; }; -class NiGravity : public NiParticleModifier +struct NiGravity : public NiParticleModifier { -public: float mForce; /* 0 - Wind (fixed direction) * 1 - Point (fixed origin) @@ -115,27 +129,27 @@ struct NiParticleCollider : public NiParticleModifier }; // NiPinaColada -class NiPlanarCollider : public NiParticleCollider +struct NiPlanarCollider : public NiParticleCollider { -public: - void read(NIFStream *nif) override; - + osg::Vec2f mExtents; + osg::Vec3f mPosition; + osg::Vec3f mXVector, mYVector; osg::Vec3f mPlaneNormal; float mPlaneDistance; + + void read(NIFStream *nif) override; }; -class NiSphericalCollider : public NiParticleCollider +struct NiSphericalCollider : public NiParticleCollider { -public: float mRadius; osg::Vec3f mCenter; void read(NIFStream *nif) override; }; -class NiParticleRotation : public NiParticleModifier +struct NiParticleRotation : public NiParticleModifier { -public: void read(NIFStream *nif) override; }; diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp index 1e909120e4..16e6b5e40f 100644 --- a/components/nif/controller.cpp +++ b/components/nif/controller.cpp @@ -270,6 +270,15 @@ namespace Nif nif->getUInt(); // Zero } + void NiControllerManager::read(NIFStream *nif) + { + Controller::read(nif); + mCumulative = nif->getBoolean(); + unsigned int numSequences = nif->getUInt(); + nif->skip(4 * numSequences); // Controller sequences + nif->skip(4); // Object palette + } + void NiPoint3Interpolator::read(NIFStream *nif) { defaultVal = nif->getVector3(); @@ -325,4 +334,15 @@ namespace Nif data.post(nif); } + void NiColorInterpolator::read(NIFStream *nif) + { + defaultVal = nif->getVector4(); + data.read(nif); + } + + void NiColorInterpolator::post(NIFFile *nif) + { + data.post(nif); + } + } diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index 6b84d3749b..a3df63f913 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -29,15 +29,19 @@ namespace Nif { -class NiParticleSystemController : public Controller +struct NiParticleSystemController : public Controller { -public: + enum BSPArrayController { + BSPArrayController_AtNode = 0x8, + BSPArrayController_AtVertex = 0x10 + }; + struct Particle { osg::Vec3f velocity; float lifetime; float lifespan; float timestamp; - int vertex; + unsigned short vertex; }; float velocity; @@ -58,9 +62,9 @@ public: enum EmitFlags { - NoAutoAdjust = 0x1 // If this flag is set, we use the emitRate value. Otherwise, - // we calculate an emit rate so that the maximum number of particles - // in the system (numParticles) is never exceeded. + EmitFlag_NoAutoAdjust = 0x1 // If this flag is set, we use the emitRate value. Otherwise, + // we calculate an emit rate so that the maximum number of particles + // in the system (numParticles) is never exceeded. }; int emitFlags; @@ -77,12 +81,14 @@ public: void read(NIFStream *nif) override; void post(NIFFile *nif) override; + + bool noAutoAdjust() const { return emitFlags & EmitFlag_NoAutoAdjust; } + bool emitAtVertex() const { return flags & BSPArrayController_AtVertex; } }; using NiBSPArrayController = NiParticleSystemController; -class NiMaterialColorController : public Controller +struct NiMaterialColorController : public Controller { -public: NiPoint3InterpolatorPtr interpolator; NiPosDataPtr data; unsigned int targetColor; @@ -91,9 +97,8 @@ public: void post(NIFFile *nif) override; }; -class NiPathController : public Controller +struct NiPathController : public Controller { -public: NiPosDataPtr posData; NiFloatDataPtr floatData; @@ -115,9 +120,8 @@ public: void post(NIFFile *nif) override; }; -class NiLookAtController : public Controller +struct NiLookAtController : public Controller { -public: NodePtr target; unsigned short lookAtFlags{0}; @@ -125,9 +129,8 @@ public: void post(NIFFile *nif) override; }; -class NiUVController : public Controller +struct NiUVController : public Controller { -public: NiUVDataPtr data; unsigned int uvSet; @@ -135,9 +138,8 @@ public: void post(NIFFile *nif) override; }; -class NiKeyframeController : public Controller +struct NiKeyframeController : public Controller { -public: NiKeyframeDataPtr data; NiTransformInterpolatorPtr interpolator; @@ -154,12 +156,11 @@ struct NiFloatInterpController : public Controller void post(NIFFile *nif) override; }; -class NiAlphaController : public NiFloatInterpController { }; -class NiRollController : public NiFloatInterpController { }; +struct NiAlphaController : public NiFloatInterpController { }; +struct NiRollController : public NiFloatInterpController { }; -class NiGeomMorpherController : public Controller +struct NiGeomMorpherController : public Controller { -public: NiMorphDataPtr data; NiFloatInterpolatorList interpolators; @@ -167,18 +168,16 @@ public: void post(NIFFile *nif) override; }; -class NiVisController : public Controller +struct NiVisController : public Controller { -public: NiVisDataPtr data; void read(NIFStream *nif) override; void post(NIFFile *nif) override; }; -class NiFlipController : public Controller +struct NiFlipController : public Controller { -public: NiFloatInterpolatorPtr mInterpolator; int mTexSlot; // NiTexturingProperty::TextureType float mDelta; // Time between two flips. delta = (start_time - stop_time) / num_sources @@ -193,6 +192,12 @@ struct bhkBlendController : public Controller void read(NIFStream *nif) override; }; +struct NiControllerManager : public Controller +{ + bool mCumulative; + void read(NIFStream *nif) override; +}; + struct Interpolator : public Record { }; struct NiPoint3Interpolator : public Interpolator @@ -230,5 +235,13 @@ struct NiTransformInterpolator : public Interpolator void post(NIFFile *nif) override; }; +struct NiColorInterpolator : public Interpolator +{ + osg::Vec4f defaultVal; + NiColorDataPtr data; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + } // Namespace #endif diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 1eb5c40fe6..36a123fdfa 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -34,6 +34,13 @@ void NiSkinInstance::post(NIFFile *nif) } } +void BSDismemberSkinInstance::read(NIFStream *nif) +{ + NiSkinInstance::read(nif); + unsigned int numPartitions = nif->getUInt(); + nif->skip(4 * numPartitions); // Body part information +} + void NiGeometryData::read(NIFStream *nif) { if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,114)) @@ -184,7 +191,7 @@ void NiLinesData::read(NIFStream *nif) } } -void NiAutoNormalParticlesData::read(NIFStream *nif) +void NiParticlesData::read(NIFStream *nif) { NiGeometryData::read(nif); @@ -216,7 +223,7 @@ void NiAutoNormalParticlesData::read(NIFStream *nif) void NiRotatingParticlesData::read(NIFStream *nif) { - NiAutoNormalParticlesData::read(nif); + NiParticlesData::read(nif); if (nif->getVersion() <= NIFStream::generateVersion(4,2,2,0) && nif->getBoolean()) nif->getQuaternions(rotations, vertices.size()); @@ -363,11 +370,11 @@ void NiSkinPartition::read(NIFStream *nif) void NiSkinPartition::Partition::read(NIFStream *nif) { - unsigned short numVertices = nif->getUShort(); - unsigned short numTriangles = nif->getUShort(); - unsigned short numBones = nif->getUShort(); - unsigned short numStrips = nif->getUShort(); - unsigned short bonesPerVertex = nif->getUShort(); + size_t numVertices = nif->getUShort(); + size_t numTriangles = nif->getUShort(); + size_t numBones = nif->getUShort(); + size_t numStrips = nif->getUShort(); + size_t bonesPerVertex = nif->getUShort(); if (numBones) nif->getUShorts(bones, numBones); @@ -395,7 +402,7 @@ void NiSkinPartition::Partition::read(NIFStream *nif) if (numStrips) { strips.resize(numStrips); - for (unsigned short i = 0; i < numStrips; i++) + for (size_t i = 0; i < numStrips; i++) nif->getUShorts(strips[i], stripLengths[i]); } else if (numTriangles) @@ -421,7 +428,7 @@ void NiMorphData::read(NIFStream *nif) for(int i = 0;i < morphCount;i++) { mMorphs[i].mKeyFrames = std::make_shared(); - mMorphs[i].mKeyFrames->read(nif, true, /*morph*/true); + mMorphs[i].mKeyFrames->read(nif, /*morph*/true); nif->getVector3s(mMorphs[i].mVertices, vertCount); } } @@ -432,15 +439,14 @@ void NiKeyframeData::read(NIFStream *nif) mRotations->read(nif); if(mRotations->mInterpolationType == InterpolationType_XYZ) { - //Chomp unused float if (nif->getVersion() <= NIFStream::generateVersion(10,1,0,0)) - nif->getFloat(); + mAxisOrder = static_cast(nif->getInt()); mXRotations = std::make_shared(); mYRotations = std::make_shared(); mZRotations = std::make_shared(); - mXRotations->read(nif, true); - mYRotations->read(nif, true); - mZRotations->read(nif, true); + mXRotations->read(nif); + mYRotations->read(nif); + mZRotations->read(nif); } mTranslations = std::make_shared(); mTranslations->read(nif); @@ -460,21 +466,9 @@ void NiPalette::read(NIFStream *nif) void NiStringPalette::read(NIFStream *nif) { - unsigned int size = nif->getUInt(); - if (!size) - return; - std::vector source; - nif->getChars(source, size); - if (nif->getUInt() != size) + palette = nif->getString(); + if (nif->getUInt() != palette.size()) nif->file->warn("Failed size check in NiStringPalette"); - if (source[source.size()-1] != '\0') - source.emplace_back('\0'); - const char* buffer = source.data(); - while (static_cast(buffer - source.data()) < source.size()) - { - palette.emplace_back(buffer); - buffer += palette.back().size() + 1; - } } void NiBoolData::read(NIFStream *nif) diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 4d13afb9d0..919e442665 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -32,9 +32,8 @@ namespace Nif { // Common ancestor for several data classes -class NiGeometryData : public Record +struct NiGeometryData : public Record { -public: std::vector vertices, normals, tangents, bitangents; std::vector colors; std::vector< std::vector > uvlist; @@ -44,18 +43,16 @@ public: void read(NIFStream *nif) override; }; -class NiTriShapeData : public NiGeometryData +struct NiTriShapeData : public NiGeometryData { -public: // Triangles, three vertex indices per triangle std::vector triangles; void read(NIFStream *nif) override; }; -class NiTriStripsData : public NiGeometryData +struct NiTriStripsData : public NiGeometryData { -public: // Triangle strips, series of vertex indices. std::vector> strips; @@ -70,12 +67,11 @@ struct NiLinesData : public NiGeometryData void read(NIFStream *nif) override; }; -class NiAutoNormalParticlesData : public NiGeometryData +struct NiParticlesData : public NiGeometryData { -public: int numParticles{0}; - int activeCount; + int activeCount{0}; std::vector particleRadii, sizes, rotationAngles; std::vector rotations; @@ -84,39 +80,34 @@ public: void read(NIFStream *nif) override; }; -class NiRotatingParticlesData : public NiAutoNormalParticlesData +struct NiRotatingParticlesData : public NiParticlesData { -public: void read(NIFStream *nif) override; }; -class NiPosData : public Record +struct NiPosData : public Record { -public: Vector3KeyMapPtr mKeyList; void read(NIFStream *nif) override; }; -class NiUVData : public Record +struct NiUVData : public Record { -public: FloatKeyMapPtr mKeyList[4]; void read(NIFStream *nif) override; }; -class NiFloatData : public Record +struct NiFloatData : public Record { -public: FloatKeyMapPtr mKeyList; void read(NIFStream *nif) override; }; -class NiPixelData : public Record +struct NiPixelData : public Record { -public: enum Format { NIPXFMT_RGB8, @@ -128,14 +119,14 @@ public: NIPXFMT_DXT5, NIPXFMT_DXT5_ALT }; - Format fmt; + Format fmt{NIPXFMT_RGB8}; - unsigned int colorMask[4]; - unsigned int bpp, pixelTiling{0}; + unsigned int colorMask[4]{0}; + unsigned int bpp{0}, pixelTiling{0}; bool sRGB{false}; NiPalettePtr palette; - unsigned int numberOfMipmaps; + unsigned int numberOfMipmaps{0}; struct Mipmap { @@ -150,17 +141,15 @@ public: void post(NIFFile *nif) override; }; -class NiColorData : public Record +struct NiColorData : public Record { -public: Vector4KeyMapPtr mKeyMap; void read(NIFStream *nif) override; }; -class NiVisData : public Record +struct NiVisData : public Record { -public: struct VisData { float time; bool isSet; @@ -170,9 +159,8 @@ public: void read(NIFStream *nif) override; }; -class NiSkinInstance : public Record +struct NiSkinInstance : public Record { -public: NiSkinDataPtr data; NiSkinPartitionPtr partitions; NodePtr root; @@ -182,9 +170,13 @@ public: void post(NIFFile *nif) override; }; -class NiSkinData : public Record +struct BSDismemberSkinInstance : public NiSkinInstance +{ + void read(NIFStream *nif) override; +}; + +struct NiSkinData : public Record { -public: struct VertWeight { unsigned short vertex; @@ -248,12 +240,26 @@ struct NiKeyframeData : public Record Vector3KeyMapPtr mTranslations; FloatKeyMapPtr mScales; + enum class AxisOrder + { + Order_XYZ = 0, + Order_XZY = 1, + Order_YZX = 2, + Order_YXZ = 3, + Order_ZXY = 4, + Order_ZYX = 5, + Order_XYX = 6, + Order_YZY = 7, + Order_ZXZ = 8 + }; + + AxisOrder mAxisOrder{AxisOrder::Order_XYZ}; + void read(NIFStream *nif) override; }; -class NiPalette : public Record +struct NiPalette : public Record { -public: // 32-bit RGBA colors that correspond to 8-bit indices std::vector colors; @@ -262,7 +268,7 @@ public: struct NiStringPalette : public Record { - std::vector palette; + std::string palette; void read(NIFStream *nif) override; }; diff --git a/components/nif/effect.hpp b/components/nif/effect.hpp index 32eb3d80d8..4c546a1aef 100644 --- a/components/nif/effect.hpp +++ b/components/nif/effect.hpp @@ -96,6 +96,9 @@ struct NiTextureEffect : NiDynamicEffect void read(NIFStream *nif) override; void post(NIFFile *nif) override; + + bool wrapT() const { return clamp & 1; } + bool wrapS() const { return (clamp >> 1) & 1; } }; } // Namespace diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp index eeaf9d3ac4..04217995ac 100644 --- a/components/nif/extra.cpp +++ b/components/nif/extra.cpp @@ -3,6 +3,13 @@ namespace Nif { +void NiExtraData::read(NIFStream *nif) +{ + Extra::read(nif); + if (recordSize) + nif->getChars(data, recordSize); +} + void NiStringExtraData::read(NIFStream *nif) { Extra::read(nif); @@ -87,4 +94,38 @@ void BSBound::read(NIFStream *nif) halfExtents = nif->getVector3(); } +void BSFurnitureMarker::LegacyFurniturePosition::read(NIFStream *nif) +{ + mOffset = nif->getVector3(); + mOrientation = nif->getUShort(); + mPositionRef = nif->getChar(); + nif->skip(1); // Position ref 2 +} + +void BSFurnitureMarker::FurniturePosition::read(NIFStream *nif) +{ + mOffset = nif->getVector3(); + mHeading = nif->getFloat(); + mType = nif->getUShort(); + mEntryPoint = nif->getUShort(); +} + +void BSFurnitureMarker::read(NIFStream *nif) +{ + Extra::read(nif); + unsigned int num = nif->getUInt(); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + { + mLegacyMarkers.resize(num); + for (auto& marker : mLegacyMarkers) + marker.read(nif); + } + else + { + mMarkers.resize(num); + for (auto& marker : mMarkers) + marker.read(nif); + } +} + } diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 5d7aa0c3b2..cbcb3cabe6 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -29,15 +29,20 @@ namespace Nif { -class NiVertWeightsExtraData : public Extra +struct NiExtraData : public Extra { -public: + std::vector data; + void read(NIFStream *nif) override; }; -class NiTextKeyExtraData : public Extra +struct NiVertWeightsExtraData : public Extra +{ + void read(NIFStream *nif) override; +}; + +struct NiTextKeyExtraData : public Extra { -public: struct TextKey { float time; @@ -48,12 +53,12 @@ public: void read(NIFStream *nif) override; }; -class NiStringExtraData : public Extra +struct NiStringExtraData : public Extra { -public: - /* Two known meanings: + /* Known meanings: "MRK" - marker, only visible in the editor, not rendered in-game - "NCO" - no collision + "NCC" - no collision except with the camera + Anything else starting with "NC" - no collision */ std::string string; @@ -116,5 +121,30 @@ struct BSBound : public Extra void read(NIFStream *nif) override; }; +struct BSFurnitureMarker : public Extra +{ + struct LegacyFurniturePosition + { + osg::Vec3f mOffset; + uint16_t mOrientation; + uint8_t mPositionRef; + void read(NIFStream *nif); + }; + + struct FurniturePosition + { + osg::Vec3f mOffset; + float mHeading; + uint16_t mType; + uint16_t mEntryPoint; + void read(NIFStream *nif); + }; + + std::vector mLegacyMarkers; + std::vector mMarkers; + + void read(NIFStream *nif) override; +}; + } // Namespace #endif diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 7915908d1b..4e0fd5a614 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -1,137 +1,162 @@ #include "niffile.hpp" #include "effect.hpp" +#include + #include #include #include +#include namespace Nif { /// Open a NIF stream. The name is used for error messages. -NIFFile::NIFFile(Files::IStreamPtr stream, const std::string &name) +NIFFile::NIFFile(Files::IStreamPtr&& stream, const std::string &name) : filename(name) { - parse(stream); + parse(std::move(stream)); } -NIFFile::~NIFFile() +template +static std::unique_ptr construct() { - for (Record* record : records) - delete record; + auto result = std::make_unique(); + result->recType = recordType; + return result; } -template static Record* construct() { return new NodeType; } - -struct RecordFactoryEntry { - - using create_t = Record* (*)(); - - create_t mCreate; - RecordType mType; - -}; +using CreateRecord = std::unique_ptr (*)(); ///These are all the record types we know how to read. -static std::map makeFactory() +static std::map makeFactory() { - std::map factory; - factory["NiNode"] = {&construct , RC_NiNode }; - factory["NiSwitchNode"] = {&construct , RC_NiSwitchNode }; - factory["NiLODNode"] = {&construct , RC_NiLODNode }; - factory["AvoidNode"] = {&construct , RC_AvoidNode }; - factory["NiCollisionSwitch"] = {&construct , RC_NiCollisionSwitch }; - factory["NiBSParticleNode"] = {&construct , RC_NiBSParticleNode }; - factory["NiBSAnimationNode"] = {&construct , RC_NiBSAnimationNode }; - factory["NiBillboardNode"] = {&construct , RC_NiBillboardNode }; - factory["NiTriShape"] = {&construct , RC_NiTriShape }; - factory["NiTriStrips"] = {&construct , RC_NiTriStrips }; - factory["NiLines"] = {&construct , RC_NiLines }; - factory["NiRotatingParticles"] = {&construct , RC_NiRotatingParticles }; - factory["NiAutoNormalParticles"] = {&construct , RC_NiAutoNormalParticles }; - factory["NiCamera"] = {&construct , RC_NiCamera }; - factory["RootCollisionNode"] = {&construct , RC_RootCollisionNode }; - factory["NiTexturingProperty"] = {&construct , RC_NiTexturingProperty }; - factory["NiFogProperty"] = {&construct , RC_NiFogProperty }; - factory["NiMaterialProperty"] = {&construct , RC_NiMaterialProperty }; - factory["NiZBufferProperty"] = {&construct , RC_NiZBufferProperty }; - factory["NiAlphaProperty"] = {&construct , RC_NiAlphaProperty }; - factory["NiVertexColorProperty"] = {&construct , RC_NiVertexColorProperty }; - factory["NiShadeProperty"] = {&construct , RC_NiShadeProperty }; - factory["NiDitherProperty"] = {&construct , RC_NiDitherProperty }; - factory["NiWireframeProperty"] = {&construct , RC_NiWireframeProperty }; - factory["NiSpecularProperty"] = {&construct , RC_NiSpecularProperty }; - factory["NiStencilProperty"] = {&construct , RC_NiStencilProperty }; - factory["NiVisController"] = {&construct , RC_NiVisController }; - factory["NiGeomMorpherController"] = {&construct , RC_NiGeomMorpherController }; - factory["NiKeyframeController"] = {&construct , RC_NiKeyframeController }; - factory["NiAlphaController"] = {&construct , RC_NiAlphaController }; - factory["NiRollController"] = {&construct , RC_NiRollController }; - factory["NiUVController"] = {&construct , RC_NiUVController }; - factory["NiPathController"] = {&construct , RC_NiPathController }; - factory["NiMaterialColorController"] = {&construct , RC_NiMaterialColorController }; - factory["NiBSPArrayController"] = {&construct , RC_NiBSPArrayController }; - factory["NiParticleSystemController"] = {&construct , RC_NiParticleSystemController }; - factory["NiFlipController"] = {&construct , RC_NiFlipController }; - factory["NiAmbientLight"] = {&construct , RC_NiLight }; - factory["NiDirectionalLight"] = {&construct , RC_NiLight }; - factory["NiPointLight"] = {&construct , RC_NiLight }; - factory["NiSpotLight"] = {&construct , RC_NiLight }; - factory["NiTextureEffect"] = {&construct , RC_NiTextureEffect }; - factory["NiVertWeightsExtraData"] = {&construct , RC_NiVertWeightsExtraData }; - factory["NiTextKeyExtraData"] = {&construct , RC_NiTextKeyExtraData }; - factory["NiStringExtraData"] = {&construct , RC_NiStringExtraData }; - factory["NiGravity"] = {&construct , RC_NiGravity }; - factory["NiPlanarCollider"] = {&construct , RC_NiPlanarCollider }; - factory["NiSphericalCollider"] = {&construct , RC_NiSphericalCollider }; - factory["NiParticleGrowFade"] = {&construct , RC_NiParticleGrowFade }; - factory["NiParticleColorModifier"] = {&construct , RC_NiParticleColorModifier }; - factory["NiParticleRotation"] = {&construct , RC_NiParticleRotation }; - factory["NiFloatData"] = {&construct , RC_NiFloatData }; - factory["NiTriShapeData"] = {&construct , RC_NiTriShapeData }; - factory["NiTriStripsData"] = {&construct , RC_NiTriStripsData }; - factory["NiLinesData"] = {&construct , RC_NiLinesData }; - factory["NiVisData"] = {&construct , RC_NiVisData }; - factory["NiColorData"] = {&construct , RC_NiColorData }; - factory["NiPixelData"] = {&construct , RC_NiPixelData }; - factory["NiMorphData"] = {&construct , RC_NiMorphData }; - factory["NiKeyframeData"] = {&construct , RC_NiKeyframeData }; - factory["NiSkinData"] = {&construct , RC_NiSkinData }; - factory["NiUVData"] = {&construct , RC_NiUVData }; - factory["NiPosData"] = {&construct , RC_NiPosData }; - factory["NiRotatingParticlesData"] = {&construct , RC_NiRotatingParticlesData }; - factory["NiAutoNormalParticlesData"] = {&construct , RC_NiAutoNormalParticlesData }; - factory["NiSequenceStreamHelper"] = {&construct , RC_NiSequenceStreamHelper }; - factory["NiSourceTexture"] = {&construct , RC_NiSourceTexture }; - factory["NiSkinInstance"] = {&construct , RC_NiSkinInstance }; - factory["NiLookAtController"] = {&construct , RC_NiLookAtController }; - factory["NiPalette"] = {&construct , RC_NiPalette }; - factory["NiIntegerExtraData"] = {&construct , RC_NiIntegerExtraData }; - factory["NiIntegersExtraData"] = {&construct , RC_NiIntegersExtraData }; - factory["NiBinaryExtraData"] = {&construct , RC_NiBinaryExtraData }; - factory["NiBooleanExtraData"] = {&construct , RC_NiBooleanExtraData }; - factory["NiVectorExtraData"] = {&construct , RC_NiVectorExtraData }; - factory["NiColorExtraData"] = {&construct , RC_NiColorExtraData }; - factory["NiFloatExtraData"] = {&construct , RC_NiFloatExtraData }; - factory["NiFloatsExtraData"] = {&construct , RC_NiFloatsExtraData }; - factory["NiStringPalette"] = {&construct , RC_NiStringPalette }; - factory["NiBoolData"] = {&construct , RC_NiBoolData }; - factory["NiSkinPartition"] = {&construct , RC_NiSkinPartition }; - factory["BSXFlags"] = {&construct , RC_BSXFlags }; - factory["BSBound"] = {&construct , RC_BSBound }; - factory["NiTransformData"] = {&construct , RC_NiKeyframeData }; - factory["BSFadeNode"] = {&construct , RC_NiNode }; - factory["bhkBlendController"] = {&construct , RC_bhkBlendController }; - factory["NiFloatInterpolator"] = {&construct , RC_NiFloatInterpolator }; - factory["NiBoolInterpolator"] = {&construct , RC_NiBoolInterpolator }; - factory["NiPoint3Interpolator"] = {&construct , RC_NiPoint3Interpolator }; - factory["NiTransformController"] = {&construct , RC_NiKeyframeController }; - factory["NiTransformInterpolator"] = {&construct , RC_NiTransformInterpolator }; - return factory; + return + { + {"NiNode" , &construct }, + {"NiSwitchNode" , &construct }, + {"NiLODNode" , &construct }, + {"NiFltAnimationNode" , &construct }, + {"AvoidNode" , &construct }, + {"NiCollisionSwitch" , &construct }, + {"NiBSParticleNode" , &construct }, + {"NiBSAnimationNode" , &construct }, + {"NiBillboardNode" , &construct }, + {"NiTriShape" , &construct }, + {"NiTriStrips" , &construct }, + {"NiLines" , &construct }, + {"NiParticles" , &construct }, + {"NiRotatingParticles" , &construct }, + {"NiAutoNormalParticles" , &construct }, + {"NiCamera" , &construct }, + {"RootCollisionNode" , &construct }, + {"NiTexturingProperty" , &construct }, + {"NiFogProperty" , &construct }, + {"NiMaterialProperty" , &construct }, + {"NiZBufferProperty" , &construct }, + {"NiAlphaProperty" , &construct }, + {"NiVertexColorProperty" , &construct }, + {"NiShadeProperty" , &construct }, + {"NiDitherProperty" , &construct }, + {"NiWireframeProperty" , &construct }, + {"NiSpecularProperty" , &construct }, + {"NiStencilProperty" , &construct }, + {"NiVisController" , &construct }, + {"NiGeomMorpherController" , &construct }, + {"NiKeyframeController" , &construct }, + {"NiAlphaController" , &construct }, + {"NiRollController" , &construct }, + {"NiUVController" , &construct }, + {"NiPathController" , &construct }, + {"NiMaterialColorController" , &construct }, + {"NiBSPArrayController" , &construct }, + {"NiParticleSystemController" , &construct }, + {"NiFlipController" , &construct }, + {"NiAmbientLight" , &construct }, + {"NiDirectionalLight" , &construct }, + {"NiPointLight" , &construct }, + {"NiSpotLight" , &construct }, + {"NiTextureEffect" , &construct }, + {"NiExtraData" , &construct }, + {"NiVertWeightsExtraData" , &construct }, + {"NiTextKeyExtraData" , &construct }, + {"NiStringExtraData" , &construct }, + {"NiGravity" , &construct }, + {"NiPlanarCollider" , &construct }, + {"NiSphericalCollider" , &construct }, + {"NiParticleGrowFade" , &construct }, + {"NiParticleColorModifier" , &construct }, + {"NiParticleRotation" , &construct }, + {"NiFloatData" , &construct }, + {"NiTriShapeData" , &construct }, + {"NiTriStripsData" , &construct }, + {"NiLinesData" , &construct }, + {"NiVisData" , &construct }, + {"NiColorData" , &construct }, + {"NiPixelData" , &construct }, + {"NiMorphData" , &construct }, + {"NiKeyframeData" , &construct }, + {"NiSkinData" , &construct }, + {"NiUVData" , &construct }, + {"NiPosData" , &construct }, + {"NiParticlesData" , &construct }, + {"NiRotatingParticlesData" , &construct }, + {"NiAutoNormalParticlesData" , &construct }, + {"NiSequenceStreamHelper" , &construct }, + {"NiSourceTexture" , &construct }, + {"NiSkinInstance" , &construct }, + {"NiLookAtController" , &construct }, + {"NiPalette" , &construct }, + {"NiIntegerExtraData" , &construct }, + {"NiIntegersExtraData" , &construct }, + {"NiBinaryExtraData" , &construct }, + {"NiBooleanExtraData" , &construct }, + {"NiVectorExtraData" , &construct }, + {"NiColorExtraData" , &construct }, + {"NiFloatExtraData" , &construct }, + {"NiFloatsExtraData" , &construct }, + {"NiStringPalette" , &construct }, + {"NiBoolData" , &construct }, + {"NiSkinPartition" , &construct }, + {"BSXFlags" , &construct }, + {"BSBound" , &construct }, + {"NiTransformData" , &construct }, + {"BSFadeNode" , &construct }, + {"bhkBlendController" , &construct }, + {"NiFloatInterpolator" , &construct }, + {"NiBoolInterpolator" , &construct }, + {"NiPoint3Interpolator" , &construct }, + {"NiTransformController" , &construct }, + {"NiTransformInterpolator" , &construct }, + {"NiColorInterpolator" , &construct }, + {"BSShaderTextureSet" , &construct }, + {"BSLODTriShape" , &construct }, + {"BSShaderProperty" , &construct }, + {"BSShaderPPLightingProperty" , &construct }, + {"BSShaderNoLightingProperty" , &construct }, + {"BSFurnitureMarker" , &construct }, + {"NiCollisionObject" , &construct }, + {"bhkCollisionObject" , &construct }, + {"BSDismemberSkinInstance" , &construct }, + {"NiControllerManager" , &construct }, + {"bhkMoppBvTreeShape" , &construct }, + {"bhkNiTriStripsShape" , &construct }, + {"bhkPackedNiTriStripsShape" , &construct }, + {"hkPackedNiTriStripsData" , &construct }, + {"bhkConvexVerticesShape" , &construct }, + {"bhkBoxShape" , &construct }, + {"bhkListShape" , &construct }, + {"bhkRigidBody" , &construct }, + {"bhkRigidBodyT" , &construct }, + {"BSLightingShaderProperty" , &construct }, + {"NiSortAdjustNode" , &construct }, + {"NiClusterAccumulator" , &construct }, + {"NiAlphaAccumulator" , &construct }, + }; } ///Make the factory map used for parsing the file -static const std::map factories = makeFactory(); +static const std::map factories = makeFactory(); std::string NIFFile::printVersion(unsigned int version) { @@ -145,9 +170,12 @@ std::string NIFFile::printVersion(unsigned int version) return stream.str(); } -void NIFFile::parse(Files::IStreamPtr stream) +void NIFFile::parse(Files::IStreamPtr&& stream) { - NIFStream nif (this, stream); + const std::array fileHash = Files::getHash(filename, *stream); + hash.append(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)); + + NIFStream nif (this, std::move(stream)); // Check the header string std::string head = nif.getVersionString(); @@ -156,18 +184,11 @@ void NIFFile::parse(Files::IStreamPtr stream) "NetImmerse File Format", "Gamebryo File Format" }; - bool supported = false; - for (const std::string& verString : verStrings) - { - supported = (head.compare(0, verString.size(), verString) == 0); - if (supported) - break; - } - if (!supported) + const bool supportedHeader = std::any_of(verStrings.begin(), verStrings.end(), + [&] (const std::string& verString) { return head.compare(0, verString.size(), verString) == 0; }); + if (!supportedHeader) fail("Invalid NIF header: " + head); - supported = false; - // Get BCD version ver = nif.getUInt(); // 4.0.0.0 is an older, practically identical version of the format. @@ -177,13 +198,8 @@ void NIFFile::parse(Files::IStreamPtr stream) NIFStream::generateVersion(4,0,0,0), VER_MW }; - for (uint32_t supportedVer : supportedVers) - { - supported = (ver == supportedVer); - if (supported) - break; - } - if (!supported) + const bool supportedVersion = std::find(supportedVers.begin(), supportedVers.end(), ver) != supportedVers.end(); + if (!supportedVersion) { if (sLoadUnsupportedFiles) warn("Unsupported NIF version: " + printVersion(ver) + ". Proceed with caution!"); @@ -204,7 +220,7 @@ void NIFFile::parse(Files::IStreamPtr stream) userVer = nif.getUInt(); // Number of records - unsigned int recNum = nif.getUInt(); + const std::size_t recNum = nif.getUInt(); records.resize(recNum); // Bethesda stream header @@ -243,7 +259,7 @@ void NIFFile::parse(Files::IStreamPtr stream) std::vector recSizes; // Currently unused nif.getUInts(recSizes, recNum); } - unsigned int stringNum = nif.getUInt(); + const std::size_t stringNum = nif.getUInt(); nif.getUInt(); // Max string length if (stringNum) nif.getSizedStrings(strings, stringNum); @@ -256,9 +272,9 @@ void NIFFile::parse(Files::IStreamPtr stream) } const bool hasRecordSeparators = ver >= NIFStream::generateVersion(10,0,0,0) && ver < NIFStream::generateVersion(10,2,0,0); - for (unsigned int i = 0; i < recNum; i++) + for (std::size_t i = 0; i < recNum; i++) { - Record *r = nullptr; + std::unique_ptr r; std::string rec = hasRecTypeListings ? recTypes[recTypeIndices[i]] : nif.getString(); if(rec.empty()) @@ -279,47 +295,44 @@ void NIFFile::parse(Files::IStreamPtr stream) } } - std::map::const_iterator entry = factories.find(rec); + const auto entry = factories.find(rec); - if (entry != factories.end()) - { - r = entry->second.mCreate (); - r->recType = entry->second.mType; - } - else + if (entry == factories.end()) fail("Unknown record type " + rec); - if (!supported) + r = entry->second(); + + if (!supportedVersion) Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i << " (" << filename << ")"; assert(r != nullptr); assert(r->recType != RC_MISSING); r->recName = rec; r->recIndex = i; - records[i] = r; r->read(&nif); + records[i] = std::move(r); } - unsigned int rootNum = nif.getUInt(); + const std::size_t rootNum = nif.getUInt(); roots.resize(rootNum); //Determine which records are roots - for (unsigned int i = 0; i < rootNum; i++) + for (std::size_t i = 0; i < rootNum; i++) { int idx = nif.getInt(); - if (idx >= 0 && idx < int(records.size())) + if (idx >= 0 && static_cast(idx) < records.size()) { - roots[i] = records[idx]; + roots[i] = records[idx].get(); } else { roots[i] = nullptr; - warn("Null Root found"); + warn("Root " + std::to_string(i + 1) + " does not point to a record: index " + std::to_string(idx)); } } // Once parsing is done, do post-processing. - for (Record* record : records) + for (const auto& record : records) record->post(this); } @@ -333,7 +346,7 @@ bool NIFFile::getUseSkinning() const return mUseSkinning; } -bool NIFFile::sLoadUnsupportedFiles = false; +std::atomic_bool NIFFile::sLoadUnsupportedFiles = false; void NIFFile::setLoadUnsupportedFiles(bool load) { diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index c6dd8af756..358428e94c 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -5,9 +5,11 @@ #include #include +#include +#include #include -#include +#include #include "record.hpp" @@ -34,6 +36,8 @@ struct File virtual std::string getFilename() const = 0; + virtual std::string getHash() const = 0; + virtual unsigned int getVersion() const = 0; virtual unsigned int getUserVersion() const = 0; @@ -50,9 +54,10 @@ class NIFFile final : public File /// File name, used for error messages and opening the file std::string filename; + std::string hash; /// Record list - std::vector records; + std::vector> records; /// Root list. This is a select portion of the pointers from records std::vector roots; @@ -62,10 +67,10 @@ class NIFFile final : public File bool mUseSkinning = false; - static bool sLoadUnsupportedFiles; + static std::atomic_bool sLoadUnsupportedFiles; /// Parse the file - void parse(Files::IStreamPtr stream); + void parse(Files::IStreamPtr&& stream); /// Get the file's version in a human readable form ///\returns A string containing a human readable NIF version number @@ -92,11 +97,9 @@ public: }; /// Used if file parsing fails - void fail(const std::string &msg) const + [[noreturn]] void fail(const std::string &msg) const { - std::string err = " NIFFile Error: " + msg; - err += "\nFile: " + filename; - throw std::runtime_error(err); + throw std::runtime_error(" NIFFile Error: " + msg + "\nFile: " + filename); } /// Used when something goes wrong, but not catastrophically so void warn(const std::string &msg) const @@ -105,14 +108,12 @@ public: } /// Open a NIF stream. The name is used for error messages. - NIFFile(Files::IStreamPtr stream, const std::string &name); - ~NIFFile(); + NIFFile(Files::IStreamPtr&& stream, const std::string &name); /// Get a given record Record *getRecord(size_t index) const override { - Record *res = records.at(index); - return res; + return records.at(index).get(); } /// Number of records size_t numRecords() const override { return records.size(); } @@ -143,6 +144,8 @@ public: /// Get the name of the file std::string getFilename() const override { return filename; } + std::string getHash() const override { return hash; } + /// Get the version of the NIF format used unsigned int getVersion() const override { return ver; } diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index de2fa31c89..bcf186f333 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -5,7 +5,6 @@ #include "nifstream.hpp" -#include #include #include "niffile.hpp" @@ -48,92 +47,69 @@ struct KeyMapT { using ValueType = T; using KeyType = KeyT; - unsigned int mInterpolationType = InterpolationType_Linear; + unsigned int mInterpolationType = InterpolationType_Unknown; MapType mKeys; //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) - void read(NIFStream *nif, bool force = false, bool morph = false) + void read(NIFStream *nif, bool morph = false) { assert(nif); - mInterpolationType = InterpolationType_Unknown; - if (morph && nif->getVersion() >= NIFStream::generateVersion(10,1,0,106)) nif->getString(); // Frame name size_t count = nif->getUInt(); - if (count == 0 && !force && !morph) - return; - - if (morph && nif->getVersion() > NIFStream::generateVersion(10,1,0,0)) - { - if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,104) && - nif->getVersion() <= NIFStream::generateVersion(20,1,0,2) && nif->getBethVersion() < 10) - nif->getFloat(); // Legacy weight - return; - } - mKeys.clear(); + if (count != 0 || morph) + mInterpolationType = nif->getUInt(); - mInterpolationType = nif->getUInt(); + KeyType key = {}; - KeyT key; - NIFStream &nifReference = *nif; - - if (mInterpolationType == InterpolationType_Linear - || mInterpolationType == InterpolationType_Constant) + if (mInterpolationType == InterpolationType_Linear || mInterpolationType == InterpolationType_Constant) { - for(size_t i = 0;i < count;i++) + for (size_t i = 0;i < count;i++) { float time = nif->getFloat(); - readValue(nifReference, key); + readValue(*nif, key); mKeys[time] = key; } } else if (mInterpolationType == InterpolationType_Quadratic) { - for(size_t i = 0;i < count;i++) + for (size_t i = 0;i < count;i++) { float time = nif->getFloat(); - readQuadratic(nifReference, key); + readQuadratic(*nif, key); mKeys[time] = key; } } else if (mInterpolationType == InterpolationType_TBC) { - for(size_t i = 0;i < count;i++) + for (size_t i = 0;i < count;i++) { float time = nif->getFloat(); - readTBC(nifReference, key); + readTBC(*nif, key); mKeys[time] = key; } } - //XYZ keys aren't actually read here. - //data.hpp sees that the last type read was InterpolationType_XYZ and: - // Eats a floating point number, then - // Re-runs the read function 3 more times. - // When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ. - else if(mInterpolationType == InterpolationType_XYZ) + else if (mInterpolationType == InterpolationType_XYZ) { - //Don't try to read XYZ keys into the wrong part - if ( count != 1 ) - { - std::stringstream error; - error << "XYZ_ROTATION_KEY count should always be '1' . Retrieved Value: " - << count; - nif->file->fail(error.str()); - } + //XYZ keys aren't actually read here. + //data.cpp sees that the last type read was InterpolationType_XYZ and: + // Eats a floating point number, then + // Re-runs the read function 3 more times. + // When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ. } - else if (mInterpolationType == InterpolationType_Unknown) + else if (count != 0) { - if (count != 0) - nif->file->fail("Interpolation type 0 doesn't work with keys"); + nif->file->fail("Unhandled interpolation type: " + std::to_string(mInterpolationType)); } - else + + if (morph && nif->getVersion() > NIFStream::generateVersion(10,1,0,0)) { - std::stringstream error; - error << "Unhandled interpolation type: " << mInterpolationType; - nif->file->fail(error.str()); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,104) && + nif->getVersion() <= NIFStream::generateVersion(20,1,0,2) && nif->getBethVersion() < 10) + nif->getFloat(); // Legacy weight } } diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 69f1a905b0..6129a17394 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -7,7 +7,7 @@ namespace Nif osg::Quat NIFStream::getQuaternion() { float f[4]; - readLittleEndianBufferOfType<4, float,uint32_t>(inp, (float*)&f); + readLittleEndianBufferOfType<4, float>(inp, f); osg::Quat quat; quat.w() = f[0]; quat.x() = f[1]; diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index 97075c288c..acf1dd4704 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -7,8 +7,12 @@ #include #include #include +#include +#include +#include -#include +#include +#include #include #include @@ -21,62 +25,32 @@ namespace Nif class NIFFile; -/* - readLittleEndianBufferOfType: This template should only be used with non POD data types -*/ -template inline void readLittleEndianBufferOfType(Files::IStreamPtr &pIStream, T* dest) +template inline void readLittleEndianBufferOfType(Files::IStreamPtr &pIStream, T* dest) { -#if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) + static_assert(std::is_arithmetic_v, "Buffer element type is not arithmetic"); pIStream->read((char*)dest, numInstances * sizeof(T)); -#else - uint8_t* destByteBuffer = (uint8_t*)dest; - pIStream->read((char*)dest, numInstances * sizeof(T)); - /* - Due to the loop iterations being known at compile time, - this nested loop will most likely be unrolled - For example, for 2 instances of a 4 byte data type, you should get the below result - */ - union { - IntegerT i; - T t; - } u; - for (uint32_t i = 0; i < numInstances; i++) - { - u = { 0 }; - for (uint32_t byte = 0; byte < sizeof(T); byte++) - u.i |= (((IntegerT)destByteBuffer[i * sizeof(T) + byte]) << (byte * 8)); - dest[i] = u.t; - } -#endif + if (pIStream->bad()) + throw std::runtime_error("Failed to read little endian typed (" + std::string(typeid(T).name()) + ") buffer of " + + std::to_string(numInstances) + " instances"); + if constexpr (Misc::IS_BIG_ENDIAN) + for (std::size_t i = 0; i < numInstances; i++) + Misc::swapEndiannessInplace(dest[i]); } -/* - readLittleEndianDynamicBufferOfType: This template should only be used with non POD data types -*/ -template inline void readLittleEndianDynamicBufferOfType(Files::IStreamPtr &pIStream, T* dest, uint32_t numInstances) +template inline void readLittleEndianDynamicBufferOfType(Files::IStreamPtr &pIStream, T* dest, std::size_t numInstances) { -#if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) - pIStream->read((char*)dest, numInstances * sizeof(T)); -#else - uint8_t* destByteBuffer = (uint8_t*)dest; + static_assert(std::is_arithmetic_v, "Buffer element type is not arithmetic"); pIStream->read((char*)dest, numInstances * sizeof(T)); - union { - IntegerT i; - T t; - } u; - for (uint32_t i = 0; i < numInstances; i++) - { - u.i = 0; - for (uint32_t byte = 0; byte < sizeof(T); byte++) - u.i |= ((IntegerT)destByteBuffer[i * sizeof(T) + byte]) << (byte * 8); - dest[i] = u.t; - } -#endif + if (pIStream->bad()) + throw std::runtime_error("Failed to read little endian dynamic buffer of " + std::to_string(numInstances) + " instances"); + if constexpr (Misc::IS_BIG_ENDIAN) + for (std::size_t i = 0; i < numInstances; i++) + Misc::swapEndiannessInplace(dest[i]); } -template type inline readLittleEndianType(Files::IStreamPtr &pIStream) +template type inline readLittleEndianType(Files::IStreamPtr &pIStream) { type val; - readLittleEndianBufferOfType<1,type,IntegerT>(pIStream, (type*)&val); + readLittleEndianBufferOfType<1, type>(pIStream, &val); return val; } @@ -89,65 +63,65 @@ public: NIFFile * const file; - NIFStream (NIFFile * file, Files::IStreamPtr inp): inp (inp), file (file) {} + NIFStream (NIFFile * file, Files::IStreamPtr&& inp): inp (std::move(inp)), file (file) {} void skip(size_t size) { inp->ignore(size); } char getChar() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } short getShort() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } unsigned short getUShort() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } int getInt() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } unsigned int getUInt() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } float getFloat() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } osg::Vec2f getVector2() { osg::Vec2f vec; - readLittleEndianBufferOfType<2,float,uint32_t>(inp, (float*)&vec._v[0]); + readLittleEndianBufferOfType<2,float>(inp, vec._v); return vec; } osg::Vec3f getVector3() { osg::Vec3f vec; - readLittleEndianBufferOfType<3, float,uint32_t>(inp, (float*)&vec._v[0]); + readLittleEndianBufferOfType<3, float>(inp, vec._v); return vec; } osg::Vec4f getVector4() { osg::Vec4f vec; - readLittleEndianBufferOfType<4, float,uint32_t>(inp, (float*)&vec._v[0]); + readLittleEndianBufferOfType<4, float>(inp, vec._v); return vec; } Matrix3 getMatrix3() { Matrix3 mat; - readLittleEndianBufferOfType<9, float,uint32_t>(inp, (float*)&mat.mValues); + readLittleEndianBufferOfType<9, float>(inp, (float*)&mat.mValues); return mat; } @@ -172,23 +146,26 @@ public: ///Read in a string of the given length std::string getSizedString(size_t length) { - std::vector str(length + 1, 0); - + std::string str(length, '\0'); inp->read(str.data(), length); - - return str.data(); + if (inp->bad()) + throw std::runtime_error("Failed to read sized string of " + std::to_string(length) + " chars"); + size_t end = str.find('\0'); + if (end != std::string::npos) + str.erase(end); + return str; } ///Read in a string of the length specified in the file std::string getSizedString() { - size_t size = readLittleEndianType(inp); + size_t size = readLittleEndianType(inp); return getSizedString(size); } ///Specific to Bethesda headers, uses a byte for length std::string getExportString() { - size_t size = static_cast(readLittleEndianType(inp)); + size_t size = static_cast(readLittleEndianType(inp)); return getSizedString(size); } @@ -197,64 +174,66 @@ public: { std::string result; std::getline(*inp, result); + if (inp->bad()) + throw std::runtime_error("Failed to read version string"); return result; } void getChars(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getUChars(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getUShorts(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getFloats(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getInts(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getUInts(std::vector &vec, size_t size) { vec.resize(size); - readLittleEndianDynamicBufferOfType(inp, vec.data(), size); + readLittleEndianDynamicBufferOfType(inp, vec.data(), size); } void getVector2s(std::vector &vec, size_t size) { vec.resize(size); /* The packed storage of each Vec2f is 2 floats exactly */ - readLittleEndianDynamicBufferOfType(inp,(float*)vec.data(), size*2); + readLittleEndianDynamicBufferOfType(inp,(float*)vec.data(), size*2); } void getVector3s(std::vector &vec, size_t size) { vec.resize(size); /* The packed storage of each Vec3f is 3 floats exactly */ - readLittleEndianDynamicBufferOfType(inp, (float*)vec.data(), size*3); + readLittleEndianDynamicBufferOfType(inp, (float*)vec.data(), size*3); } void getVector4s(std::vector &vec, size_t size) { vec.resize(size); /* The packed storage of each Vec4f is 4 floats exactly */ - readLittleEndianDynamicBufferOfType(inp, (float*)vec.data(), size*4); + readLittleEndianDynamicBufferOfType(inp, (float*)vec.data(), size*4); } void getQuaternions(std::vector &quat, size_t size) diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 72adfe06cd..29c0f03e78 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_COMPONENTS_NIF_NODE_HPP #define OPENMW_COMPONENTS_NIF_NODE_HPP +#include + #include "controlled.hpp" #include "extra.hpp" #include "data.hpp" @@ -8,6 +10,7 @@ #include "niftypes.hpp" #include "controller.hpp" #include "base.hpp" +#include "physics.hpp" #include @@ -16,24 +19,149 @@ namespace Nif struct NiNode; +struct NiBoundingVolume +{ + enum Type + { + BASE_BV = 0xFFFFFFFF, + SPHERE_BV = 0, + BOX_BV = 1, + CAPSULE_BV = 2, + LOZENGE_BV = 3, + UNION_BV = 4, + HALFSPACE_BV = 5 + }; + + struct NiSphereBV + { + osg::Vec3f center; + float radius{0.f}; + }; + + struct NiBoxBV + { + osg::Vec3f center; + Matrix3 axes; + osg::Vec3f extents; + }; + + struct NiCapsuleBV + { + osg::Vec3f center, axis; + float extent{0.f}, radius{0.f}; + }; + + struct NiLozengeBV + { + float radius{0.f}, extent0{0.f}, extent1{0.f}; + osg::Vec3f center, axis0, axis1; + }; + + struct NiHalfSpaceBV + { + osg::Plane plane; + osg::Vec3f origin; + }; + + unsigned int type; + NiSphereBV sphere; + NiBoxBV box; + NiCapsuleBV capsule; + NiLozengeBV lozenge; + std::vector children; + NiHalfSpaceBV halfSpace; + void read(NIFStream* nif) + { + type = nif->getUInt(); + switch (type) + { + case BASE_BV: + break; + case SPHERE_BV: + { + sphere.center = nif->getVector3(); + sphere.radius = nif->getFloat(); + break; + } + case BOX_BV: + { + box.center = nif->getVector3(); + box.axes = nif->getMatrix3(); + box.extents = nif->getVector3(); + break; + } + case CAPSULE_BV: + { + capsule.center = nif->getVector3(); + capsule.axis = nif->getVector3(); + capsule.extent = nif->getFloat(); + capsule.radius = nif->getFloat(); + break; + } + case LOZENGE_BV: + { + lozenge.radius = nif->getFloat(); + if (nif->getVersion() >= NIFStream::generateVersion(4,2,1,0)) + { + lozenge.extent0 = nif->getFloat(); + lozenge.extent1 = nif->getFloat(); + } + lozenge.center = nif->getVector3(); + lozenge.axis0 = nif->getVector3(); + lozenge.axis1 = nif->getVector3(); + break; + } + case UNION_BV: + { + unsigned int numChildren = nif->getUInt(); + if (numChildren == 0) + break; + children.resize(numChildren); + for (NiBoundingVolume& child : children) + child.read(nif); + break; + } + case HALFSPACE_BV: + { + halfSpace.plane = osg::Plane(nif->getVector4()); + if (nif->getVersion() >= NIFStream::generateVersion(4,2,1,0)) + halfSpace.origin = nif->getVector3(); + break; + } + default: + { + nif->file->fail("Unhandled NiBoundingVolume type: " + std::to_string(type)); + } + } + } +}; + /** A Node is an object that's part of the main NIF tree. It has parent node (unless it's the root), and transformation (location and rotation) relative to it's parent. */ -class Node : public Named +struct Node : public Named { -public: + enum Flags { + Flag_Hidden = 0x0001, + Flag_MeshCollision = 0x0002, + Flag_BBoxCollision = 0x0004, + Flag_ActiveCollision = 0x0020 + }; + // Node flags. Interpretation depends somewhat on the type of node. unsigned int flags; + Transformation trafo; osg::Vec3f velocity; // Unused? Might be a run-time game state PropertyList props; // Bounding box info bool hasBounds{false}; - osg::Vec3f boundPos; - Matrix3 boundRot; - osg::Vec3f boundXYZ; // Box size + NiBoundingVolume bounds; + + // Collision object info + NiCollisionObjectPtr collision; void read(NIFStream *nif) override { @@ -48,18 +176,13 @@ public: if (nif->getVersion() <= NIFStream::generateVersion(4,2,2,0)) hasBounds = nif->getBoolean(); - if(hasBounds) - { - nif->getInt(); // always 1 - boundPos = nif->getVector3(); - boundRot = nif->getMatrix3(); - boundXYZ = nif->getVector3(); - } + if (hasBounds) + bounds.read(nif); // Reference to the collision object in Gamebryo files. if (nif->getVersion() >= NIFStream::generateVersion(10,0,1,0)) - nif->skip(4); + collision.read(nif); - parent = nullptr; + parents.clear(); isBone = false; } @@ -68,18 +191,24 @@ public: { Named::post(nif); props.post(nif); + collision.post(nif); } // Parent node, or nullptr for the root node. As far as I'm aware, only // NiNodes (or types derived from NiNodes) can be parents. - NiNode *parent; + std::vector parents; - bool isBone; + bool isBone{false}; void setBone() { isBone = true; } + + bool isHidden() const { return flags & Flag_Hidden; } + bool hasMeshCollision() const { return flags & Flag_MeshCollision; } + bool hasBBoxCollision() const { return flags & Flag_BBoxCollision; } + bool collisionActive() const { return flags & Flag_ActiveCollision; } }; struct NiNode : Node @@ -87,12 +216,6 @@ struct NiNode : Node NodeList children; NodeList effects; - enum Flags { - Flag_Hidden = 0x0001, - Flag_MeshCollision = 0x0002, - Flag_BBoxCollision = 0x0004, - Flag_ActiveCollision = 0x0020 - }; enum BSAnimFlags { AnimFlag_AutoPlay = 0x0020 }; @@ -100,9 +223,6 @@ struct NiNode : Node ParticleFlag_AutoPlay = 0x0020, ParticleFlag_LocalSpace = 0x0080 }; - enum ControllerFlags { - ControllerFlag_Active = 0x8 - }; void read(NIFStream *nif) override { @@ -114,9 +234,10 @@ struct NiNode : Node // Discard transformations for the root node, otherwise some meshes // occasionally get wrong orientation. Only for NiNode-s for now, but // can be expanded if needed. + // FIXME: if node 0 is *not* the only root node, this must not happen. if (0 == recIndex && !Misc::StringUtils::ciEqual(name, "bip01")) { - static_cast(this)->trafo = Nif::Transformation::getIdentity(); + trafo = Nif::Transformation::getIdentity(); } } @@ -130,87 +251,64 @@ struct NiNode : Node { // Why would a unique list of children contain empty refs? if(!children[i].empty()) - children[i]->parent = this; + children[i]->parents.push_back(this); } } }; struct NiGeometry : Node { + /* Possible flags: + 0x40 - mesh has no vertex normals ? + + Only flags included in 0x47 (ie. 0x01, 0x02, 0x04 and 0x40) have + been observed so far. + */ + struct MaterialData { - std::vector materialNames; - std::vector materialExtraData; - unsigned int activeMaterial{0}; - bool materialNeedsUpdate{false}; + std::vector names; + std::vector extra; + unsigned int active{0}; + bool needsUpdate{false}; void read(NIFStream *nif) { if (nif->getVersion() <= NIFStream::generateVersion(10,0,1,0)) return; - unsigned int numMaterials = 0; + unsigned int num = 0; if (nif->getVersion() <= NIFStream::generateVersion(20,1,0,3)) - numMaterials = nif->getBoolean(); // Has Shader + num = nif->getBoolean(); // Has Shader else if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) - numMaterials = nif->getUInt(); - if (numMaterials) + num = nif->getUInt(); + if (num) { - nif->getStrings(materialNames, numMaterials); - nif->getInts(materialExtraData, numMaterials); + nif->getStrings(names, num); + nif->getInts(extra, num); } if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) - activeMaterial = nif->getUInt(); + active = nif->getUInt(); if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) - { - materialNeedsUpdate = nif->getBoolean(); - if (nif->getVersion() == NIFFile::NIFVersion::VER_BGS && nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) - nif->skip(8); - } + needsUpdate = nif->getBoolean(); } }; + NiGeometryDataPtr data; NiSkinInstancePtr skin; - MaterialData materialData; -}; - -struct NiTriShape : NiGeometry -{ - /* Possible flags: - 0x40 - mesh has no vertex normals ? - - Only flags included in 0x47 (ie. 0x01, 0x02, 0x04 and 0x40) have - been observed so far. - */ - - NiTriShapeDataPtr data; - - void read(NIFStream *nif) override - { - Node::read(nif); - data.read(nif); - skin.read(nif); - materialData.read(nif); - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - skin.post(nif); - if (!skin.empty()) - nif->setUseSkinning(true); - } -}; - -struct NiTriStrips : NiGeometry -{ - NiTriStripsDataPtr data; + MaterialData material; + BSShaderPropertyPtr shaderprop; + NiAlphaPropertyPtr alphaprop; void read(NIFStream *nif) override { Node::read(nif); data.read(nif); skin.read(nif); - materialData.read(nif); + material.read(nif); + if (nif->getVersion() == NIFFile::NIFVersion::VER_BGS && nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) + { + shaderprop.read(nif); + alphaprop.read(nif); + } } void post(NIFFile *nif) override @@ -218,31 +316,28 @@ struct NiTriStrips : NiGeometry Node::post(nif); data.post(nif); skin.post(nif); - if (!skin.empty()) + shaderprop.post(nif); + alphaprop.post(nif); + if (recType != RC_NiParticles && !skin.empty()) nif->setUseSkinning(true); } }; -struct NiLines : NiGeometry +struct NiTriShape : NiGeometry {}; +struct BSLODTriShape : NiTriShape { - NiLinesDataPtr data; - + unsigned int lod0, lod1, lod2; void read(NIFStream *nif) override { - Node::read(nif); - data.read(nif); - skin.read(nif); - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - skin.post(nif); - if (!skin.empty()) - nif->setUseSkinning(true); + NiTriShape::read(nif); + lod0 = nif->getUInt(); + lod1 = nif->getUInt(); + lod2 = nif->getUInt(); } }; +struct NiTriStrips : NiGeometry {}; +struct NiLines : NiGeometry {}; +struct NiParticles : NiGeometry { }; struct NiCamera : Node { @@ -297,47 +392,11 @@ struct NiCamera : Node } }; -struct NiAutoNormalParticles : Node -{ - NiAutoNormalParticlesDataPtr data; - - void read(NIFStream *nif) override - { - Node::read(nif); - data.read(nif); - nif->getInt(); // -1 - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - } -}; - -struct NiRotatingParticles : Node -{ - NiRotatingParticlesDataPtr data; - - void read(NIFStream *nif) override - { - Node::read(nif); - data.read(nif); - nif->getInt(); // -1 - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - } -}; - // A node used as the base to switch between child nodes, such as for LOD levels. struct NiSwitchNode : public NiNode { unsigned int switchFlags{0}; - unsigned int initialIndex; + unsigned int initialIndex{0}; void read(NIFStream *nif) override { @@ -381,5 +440,57 @@ struct NiLODNode : public NiSwitchNode } }; +struct NiFltAnimationNode : public NiSwitchNode +{ + float mDuration; + enum Flags + { + Flag_Swing = 0x40 + }; + + void read(NIFStream *nif) override + { + NiSwitchNode::read(nif); + mDuration = nif->getFloat(); + } + + bool swing() const { return flags & Flag_Swing; } +}; + +// Abstract +struct NiAccumulator : Record +{ + void read(NIFStream *nif) override {} +}; + +// Node children sorters +struct NiClusterAccumulator : NiAccumulator {}; +struct NiAlphaAccumulator : NiClusterAccumulator {}; + +struct NiSortAdjustNode : NiNode +{ + enum SortingMode + { + SortingMode_Inherit, + SortingMode_Off, + SortingMode_Subsort + }; + + int mMode; + NiAccumulatorPtr mSubSorter; + void read(NIFStream *nif) override + { + NiNode::read(nif); + mMode = nif->getInt(); + if (nif->getVersion() <= NIFStream::generateVersion(20,0,0,3)) + mSubSorter.read(nif); + } + void post(NIFFile *nif) override + { + NiNode::post(nif); + mSubSorter.post(nif); + } +}; + } // Namespace #endif diff --git a/components/nif/parent.hpp b/components/nif/parent.hpp new file mode 100644 index 0000000000..afa540c8ec --- /dev/null +++ b/components/nif/parent.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_COMPONENTS_NIF_PARENT_HPP +#define OPENMW_COMPONENTS_NIF_PARENT_HPP + +namespace Nif +{ + struct NiNode; + + struct Parent + { + const NiNode& mNiNode; + const Parent* mParent; + }; +} + +#endif diff --git a/components/nif/physics.cpp b/components/nif/physics.cpp new file mode 100644 index 0000000000..9bbeb148dd --- /dev/null +++ b/components/nif/physics.cpp @@ -0,0 +1,313 @@ +#include "physics.hpp" +#include "node.hpp" + +namespace Nif +{ + + /// Non-record data types + + void bhkWorldObjCInfoProperty::read(NIFStream *nif) + { + mData = nif->getUInt(); + mSize = nif->getUInt(); + mCapacityAndFlags = nif->getUInt(); + } + + void bhkWorldObjectCInfo::read(NIFStream *nif) + { + nif->skip(4); // Unused + mPhaseType = static_cast(nif->getChar()); + nif->skip(3); // Unused + mProperty.read(nif); + } + + void HavokMaterial::read(NIFStream *nif) + { + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD) + nif->skip(4); // Unknown + mMaterial = nif->getUInt(); + } + + void HavokFilter::read(NIFStream *nif) + { + mLayer = nif->getChar(); + mFlags = nif->getChar(); + mGroup = nif->getUShort(); + } + + void hkSubPartData::read(NIFStream *nif) + { + mHavokFilter.read(nif); + mNumVertices = nif->getUInt(); + mHavokMaterial.read(nif); + } + + void hkpMoppCode::read(NIFStream *nif) + { + unsigned int size = nif->getUInt(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + mOffset = nif->getVector4(); + if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) + nif->getChar(); // MOPP data build type + if (size) + nif->getChars(mData, size); + } + + void bhkEntityCInfo::read(NIFStream *nif) + { + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + + void TriangleData::read(NIFStream *nif) + { + for (int i = 0; i < 3; i++) + mTriangle[i] = nif->getUShort(); + mWeldingInfo = nif->getUShort(); + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB) + mNormal = nif->getVector3(); + } + + void bhkRigidBodyCInfo::read(NIFStream *nif) + { + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + { + nif->skip(4); // Unused + mHavokFilter.read(nif); + nif->skip(4); // Unused + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + { + if (nif->getBethVersion() >= 83) + nif->skip(4); // Unused + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + } + if (nif->getBethVersion() < 83) + nif->skip(4); // Unused + mTranslation = nif->getVector4(); + mRotation = nif->getQuaternion(); + mLinearVelocity = nif->getVector4(); + mAngularVelocity = nif->getVector4(); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 4; j++) + mInertiaTensor[i][j] = nif->getFloat(); + mCenter = nif->getVector4(); + mMass = nif->getFloat(); + mLinearDamping = nif->getFloat(); + mAngularDamping = nif->getFloat(); + if (nif->getBethVersion() >= 83) + { + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + mTimeFactor = nif->getFloat(); + mGravityFactor = nif->getFloat(); + } + mFriction = nif->getFloat(); + if (nif->getBethVersion() >= 83) + mRollingFrictionMult = nif->getFloat(); + mRestitution = nif->getFloat(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + { + mMaxLinearVelocity = nif->getFloat(); + mMaxAngularVelocity = nif->getFloat(); + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + mPenetrationDepth = nif->getFloat(); + } + mMotionType = static_cast(nif->getChar()); + if (nif->getBethVersion() < 83) + mDeactivatorType = static_cast(nif->getChar()); + else + mEnableDeactivation = nif->getBoolean(); + mSolverDeactivation = static_cast(nif->getChar()); + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4) + { + nif->skip(1); + mPenetrationDepth = nif->getFloat(); + mTimeFactor = nif->getFloat(); + nif->skip(4); + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + mQualityType = static_cast(nif->getChar()); + if (nif->getBethVersion() >= 83) + { + mAutoRemoveLevel = nif->getChar(); + mResponseModifierFlags = nif->getChar(); + mNumContactPointShapeKeys = nif->getChar(); + mForceCollidedOntoPPU = nif->getBoolean(); + } + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4) + nif->skip(3); // Unused + else + nif->skip(12); // Unused + } + + /// Record types + + void bhkCollisionObject::read(NIFStream *nif) + { + NiCollisionObject::read(nif); + mFlags = nif->getUShort(); + mBody.read(nif); + } + + void bhkWorldObject::read(NIFStream *nif) + { + mShape.read(nif); + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD) + nif->skip(4); // Unknown + mHavokFilter.read(nif); + mWorldObjectInfo.read(nif); + } + + void bhkWorldObject::post(NIFFile *nif) + { + mShape.post(nif); + } + + void bhkEntity::read(NIFStream *nif) + { + bhkWorldObject::read(nif); + mInfo.read(nif); + } + + void bhkBvTreeShape::read(NIFStream *nif) + { + mShape.read(nif); + } + + void bhkBvTreeShape::post(NIFFile *nif) + { + mShape.post(nif); + } + + void bhkMoppBvTreeShape::read(NIFStream *nif) + { + bhkBvTreeShape::read(nif); + nif->skip(12); // Unused + mScale = nif->getFloat(); + mMopp.read(nif); + } + + void bhkNiTriStripsShape::read(NIFStream *nif) + { + mHavokMaterial.read(nif); + mRadius = nif->getFloat(); + nif->skip(20); // Unused + mGrowBy = nif->getUInt(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + mScale = nif->getVector4(); + mData.read(nif); + unsigned int numFilters = nif->getUInt(); + nif->getUInts(mFilters, numFilters); + } + + void bhkNiTriStripsShape::post(NIFFile *nif) + { + mData.post(nif); + } + + void bhkPackedNiTriStripsShape::read(NIFStream *nif) + { + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB) + { + mSubshapes.resize(nif->getUShort()); + for (hkSubPartData& subshape : mSubshapes) + subshape.read(nif); + } + mUserData = nif->getUInt(); + nif->skip(4); // Unused + mRadius = nif->getFloat(); + nif->skip(4); // Unused + mScale = nif->getVector4(); + nif->skip(20); // Duplicates of the two previous fields + mData.read(nif); + } + + void bhkPackedNiTriStripsShape::post(NIFFile *nif) + { + mData.post(nif); + } + + void hkPackedNiTriStripsData::read(NIFStream *nif) + { + unsigned int numTriangles = nif->getUInt(); + mTriangles.resize(numTriangles); + for (unsigned int i = 0; i < numTriangles; i++) + mTriangles[i].read(nif); + + unsigned int numVertices = nif->getUInt(); + bool compressed = false; + if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) + compressed = nif->getBoolean(); + if (!compressed) + nif->getVector3s(mVertices, numVertices); + else + nif->skip(6 * numVertices); // Half-precision vectors are not currently supported + if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) + { + mSubshapes.resize(nif->getUShort()); + for (hkSubPartData& subshape : mSubshapes) + subshape.read(nif); + } + } + + void bhkSphereRepShape::read(NIFStream *nif) + { + mHavokMaterial.read(nif); + } + + void bhkConvexShape::read(NIFStream *nif) + { + bhkSphereRepShape::read(nif); + mRadius = nif->getFloat(); + } + + void bhkConvexVerticesShape::read(NIFStream *nif) + { + bhkConvexShape::read(nif); + mVerticesProperty.read(nif); + mNormalsProperty.read(nif); + unsigned int numVertices = nif->getUInt(); + if (numVertices) + nif->getVector4s(mVertices, numVertices); + unsigned int numNormals = nif->getUInt(); + if (numNormals) + nif->getVector4s(mNormals, numNormals); + } + + void bhkBoxShape::read(NIFStream *nif) + { + bhkConvexShape::read(nif); + nif->skip(8); // Unused + mExtents = nif->getVector3(); + nif->skip(4); // Unused + } + + void bhkListShape::read(NIFStream *nif) + { + mSubshapes.read(nif); + mHavokMaterial.read(nif); + mChildShapeProperty.read(nif); + mChildFilterProperty.read(nif); + unsigned int numFilters = nif->getUInt(); + mHavokFilters.resize(numFilters); + for (HavokFilter& filter : mHavokFilters) + filter.read(nif); + } + + void bhkRigidBody::read(NIFStream *nif) + { + bhkEntity::read(nif); + mInfo.read(nif); + mConstraints.read(nif); + if (nif->getBethVersion() < 76) + mBodyFlags = nif->getUInt(); + else + mBodyFlags = nif->getUShort(); + } + +} // Namespace \ No newline at end of file diff --git a/components/nif/physics.hpp b/components/nif/physics.hpp new file mode 100644 index 0000000000..613ec0ba43 --- /dev/null +++ b/components/nif/physics.hpp @@ -0,0 +1,332 @@ +#ifndef OPENMW_COMPONENTS_NIF_PHYSICS_HPP +#define OPENMW_COMPONENTS_NIF_PHYSICS_HPP + +#include "base.hpp" + +// This header contains certain record definitions +// specific to Bethesda implementation of Havok physics +namespace Nif +{ + +/// Non-record data types + +struct bhkWorldObjCInfoProperty +{ + unsigned int mData; + unsigned int mSize; + unsigned int mCapacityAndFlags; + void read(NIFStream *nif); +}; + +enum class BroadPhaseType : uint8_t +{ + BroadPhase_Invalid = 0, + BroadPhase_Entity = 1, + BroadPhase_Phantom = 2, + BroadPhase_Border = 3 +}; + +struct bhkWorldObjectCInfo +{ + BroadPhaseType mPhaseType; + bhkWorldObjCInfoProperty mProperty; + void read(NIFStream *nif); +}; + +struct HavokMaterial +{ + unsigned int mMaterial; + void read(NIFStream *nif); +}; + +struct HavokFilter +{ + unsigned char mLayer; + unsigned char mFlags; + unsigned short mGroup; + void read(NIFStream *nif); +}; + +struct hkSubPartData +{ + HavokMaterial mHavokMaterial; + unsigned int mNumVertices; + HavokFilter mHavokFilter; + void read(NIFStream *nif); +}; + +enum class hkResponseType : uint8_t +{ + Response_Invalid = 0, + Response_SimpleContact = 1, + Response_Reporting = 2, + Response_None = 3 +}; + +struct bhkEntityCInfo +{ + hkResponseType mResponseType; + unsigned short mProcessContactDelay; + void read(NIFStream *nif); +}; + +struct hkpMoppCode +{ + osg::Vec4f mOffset; + std::vector mData; + void read(NIFStream *nif); +}; + +struct TriangleData +{ + unsigned short mTriangle[3]; + unsigned short mWeldingInfo; + osg::Vec3f mNormal; + void read(NIFStream *nif); +}; + +enum class hkMotionType : uint8_t +{ + Motion_Invalid = 0, + Motion_Dynamic = 1, + Motion_SphereInertia = 2, + Motion_SphereStabilized = 3, + Motion_BoxInertia = 4, + Motion_BoxStabilized = 5, + Motion_Keyframed = 6, + Motion_Fixed = 7, + Motion_ThinBox = 8, + Motion_Character = 9 +}; + +enum class hkDeactivatorType : uint8_t +{ + Deactivator_Invalid = 0, + Deactivator_Never = 1, + Deactivator_Spatial = 2 +}; + +enum class hkSolverDeactivation : uint8_t +{ + SolverDeactivation_Invalid = 0, + SolverDeactivation_Off = 1, + SolverDeactivation_Low = 2, + SolverDeactivation_Medium = 3, + SolverDeactivation_High = 4, + SolverDeactivation_Max = 5 +}; + +enum class hkQualityType : uint8_t +{ + Quality_Invalid = 0, + Quality_Fixed = 1, + Quality_Keyframed = 2, + Quality_Debris = 3, + Quality_Moving = 4, + Quality_Critical = 5, + Quality_Bullet = 6, + Quality_User = 7, + Quality_Character = 8, + Quality_KeyframedReport = 9 +}; + +struct bhkRigidBodyCInfo +{ + HavokFilter mHavokFilter; + hkResponseType mResponseType; + unsigned short mProcessContactDelay; + osg::Vec4f mTranslation; + osg::Quat mRotation; + osg::Vec4f mLinearVelocity; + osg::Vec4f mAngularVelocity; + float mInertiaTensor[3][4]; + osg::Vec4f mCenter; + float mMass; + float mLinearDamping; + float mAngularDamping; + float mTimeFactor{1.f}; + float mGravityFactor{1.f}; + float mFriction; + float mRollingFrictionMult; + float mRestitution; + float mMaxLinearVelocity; + float mMaxAngularVelocity; + float mPenetrationDepth; + hkMotionType mMotionType; + hkDeactivatorType mDeactivatorType; + bool mEnableDeactivation{true}; + hkSolverDeactivation mSolverDeactivation; + hkQualityType mQualityType; + unsigned char mAutoRemoveLevel; + unsigned char mResponseModifierFlags; + unsigned char mNumContactPointShapeKeys; + bool mForceCollidedOntoPPU; + void read(NIFStream *nif); +}; + +/// Record types + +// Abstract Bethesda Havok object +struct bhkRefObject : public Record {}; + +// Abstract serializable Bethesda Havok object +struct bhkSerializable : public bhkRefObject {}; + +// Abstract narrowphase collision detection object +struct bhkShape : public bhkSerializable {}; + +// Abstract bhkShape collection +struct bhkShapeCollection : public bhkShape {}; + +// Generic collision object +struct NiCollisionObject : public Record +{ + // The node that references this object + NodePtr mTarget; + + void read(NIFStream *nif) override + { + mTarget.read(nif); + } + void post(NIFFile *nif) override + { + mTarget.post(nif); + } +}; + +// Bethesda Havok-specific collision object +struct bhkCollisionObject : public NiCollisionObject +{ + unsigned short mFlags; + bhkWorldObjectPtr mBody; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override + { + NiCollisionObject::post(nif); + mBody.post(nif); + } +}; + +// Abstract Havok shape info record +struct bhkWorldObject : public bhkSerializable +{ + bhkShapePtr mShape; + HavokFilter mHavokFilter; + bhkWorldObjectCInfo mWorldObjectInfo; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// Abstract +struct bhkEntity : public bhkWorldObject +{ + bhkEntityCInfo mInfo; + void read(NIFStream *nif) override; +}; + +// Bethesda extension of hkpBvTreeShape +// hkpBvTreeShape adds a bounding volume tree to an hkpShapeCollection +struct bhkBvTreeShape : public bhkShape +{ + bhkShapePtr mShape; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// bhkBvTreeShape with Havok MOPP code +struct bhkMoppBvTreeShape : public bhkBvTreeShape +{ + float mScale; + hkpMoppCode mMopp; + void read(NIFStream *nif) override; +}; + +// Bethesda triangle strip-based Havok shape collection +struct bhkNiTriStripsShape : public bhkShape +{ + HavokMaterial mHavokMaterial; + float mRadius; + unsigned int mGrowBy; + osg::Vec4f mScale{1.f, 1.f, 1.f, 0.f}; + NiTriStripsDataList mData; + std::vector mFilters; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// Bethesda packed triangle strip-based Havok shape collection +struct bhkPackedNiTriStripsShape : public bhkShapeCollection +{ + std::vector mSubshapes; + unsigned int mUserData; + float mRadius; + osg::Vec4f mScale; + hkPackedNiTriStripsDataPtr mData; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// bhkPackedNiTriStripsShape data block +struct hkPackedNiTriStripsData : public bhkShapeCollection +{ + std::vector mTriangles; + std::vector mVertices; + std::vector mSubshapes; + void read(NIFStream *nif) override; +}; + +// Abstract +struct bhkSphereRepShape : public bhkShape +{ + HavokMaterial mHavokMaterial; + void read(NIFStream *nif) override; +}; + +// Abstract +struct bhkConvexShape : public bhkSphereRepShape +{ + float mRadius; + void read(NIFStream *nif) override; +}; + +// A convex shape built from vertices +struct bhkConvexVerticesShape : public bhkConvexShape +{ + bhkWorldObjCInfoProperty mVerticesProperty; + bhkWorldObjCInfoProperty mNormalsProperty; + std::vector mVertices; + std::vector mNormals; + void read(NIFStream *nif) override; +}; + +// A box +struct bhkBoxShape : public bhkConvexShape +{ + osg::Vec3f mExtents; + void read(NIFStream *nif) override; +}; + +// A list of shapes +struct bhkListShape : public bhkShapeCollection +{ + bhkShapeList mSubshapes; + HavokMaterial mHavokMaterial; + bhkWorldObjCInfoProperty mChildShapeProperty; + bhkWorldObjCInfoProperty mChildFilterProperty; + std::vector mHavokFilters; + void read(NIFStream *nif) override; +}; + +struct bhkRigidBody : public bhkEntity +{ + bhkRigidBodyCInfo mInfo; + bhkSerializableList mConstraints; + unsigned int mBodyFlags; + + void read(NIFStream *nif) override; +}; + +} // Namespace +#endif \ No newline at end of file diff --git a/components/nif/property.cpp b/components/nif/property.cpp index e6ae71c241..ee47e8ccbe 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -99,6 +99,109 @@ void NiTexturingProperty::post(NIFFile *nif) shaderTextures[i].post(nif); } +void BSShaderProperty::read(NIFStream *nif) +{ + NiShadeProperty::read(nif); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + { + type = nif->getUInt(); + flags1 = nif->getUInt(); + flags2 = nif->getUInt(); + envMapIntensity = nif->getFloat(); + } +} + +void BSShaderLightingProperty::read(NIFStream *nif) +{ + BSShaderProperty::read(nif); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + clamp = nif->getUInt(); +} + +void BSShaderPPLightingProperty::read(NIFStream *nif) +{ + BSShaderLightingProperty::read(nif); + textureSet.read(nif); + if (nif->getBethVersion() <= 14) + return; + refraction.strength = nif->getFloat(); + refraction.period = nif->getInt(); + if (nif->getBethVersion() <= 24) + return; + parallax.passes = nif->getFloat(); + parallax.scale = nif->getFloat(); +} + +void BSShaderPPLightingProperty::post(NIFFile *nif) +{ + BSShaderLightingProperty::post(nif); + textureSet.post(nif); +} + +void BSShaderNoLightingProperty::read(NIFStream *nif) +{ + BSShaderLightingProperty::read(nif); + filename = nif->getSizedString(); + if (nif->getBethVersion() >= 27) + falloffParams = nif->getVector4(); +} + +void BSLightingShaderProperty::read(NIFStream *nif) +{ + type = nif->getUInt(); + BSShaderProperty::read(nif); + flags1 = nif->getUInt(); + flags2 = nif->getUInt(); + nif->skip(8); // UV offset + nif->skip(8); // UV scale + mTextureSet.read(nif); + mEmissive = nif->getVector3(); + mEmissiveMult = nif->getFloat(); + mClamp = nif->getUInt(); + mAlpha = nif->getFloat(); + nif->getFloat(); // Refraction strength + mGlossiness = nif->getFloat(); + mSpecular = nif->getVector3(); + mSpecStrength = nif->getFloat(); + nif->skip(8); // Lighting effects + switch (static_cast(type)) + { + case BSLightingShaderType::ShaderType_EnvMap: + nif->skip(4); // Environment map scale + break; + case BSLightingShaderType::ShaderType_SkinTint: + case BSLightingShaderType::ShaderType_HairTint: + nif->skip(12); // Tint color + break; + case BSLightingShaderType::ShaderType_ParallaxOcc: + nif->skip(4); // Max passes + nif->skip(4); // Scale + break; + case BSLightingShaderType::ShaderType_MultiLayerParallax: + nif->skip(4); // Inner layer thickness + nif->skip(4); // Refraction scale + nif->skip(8); // Inner layer texture scale + nif->skip(4); // Environment map strength + break; + case BSLightingShaderType::ShaderType_SparkleSnow: + nif->skip(16); // Sparkle parameters + break; + case BSLightingShaderType::ShaderType_EyeEnvmap: + nif->skip(4); // Cube map scale + nif->skip(12); // Left eye cube map offset + nif->skip(12); // Right eye cube map offset + break; + default: + break; + } +} + +void BSLightingShaderProperty::post(NIFFile *nif) +{ + BSShaderProperty::post(nif); + mTextureSet.post(nif); +} + void NiFogProperty::read(NIFStream *nif) { Property::read(nif); @@ -118,8 +221,8 @@ void S_MaterialProperty::read(NIFStream *nif) emissive = nif->getVector3(); glossiness = nif->getFloat(); alpha = nif->getFloat(); - if (nif->getBethVersion() > 21) - emissive *= nif->getFloat(); + if (nif->getBethVersion() >= 22) + emissiveMult = nif->getFloat(); } void S_VertexColorProperty::read(NIFStream *nif) diff --git a/components/nif/property.hpp b/components/nif/property.hpp index c821b7c37e..fd7d47402f 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -29,11 +29,10 @@ namespace Nif { -class Property : public Named { }; +struct Property : public Named { }; -class NiTexturingProperty : public Property +struct NiTexturingProperty : public Property { -public: unsigned short flags{0u}; // A sub-texture @@ -53,6 +52,9 @@ public: void read(NIFStream *nif); void post(NIFFile *nif); + + bool wrapT() const { return clamp & 1; } + bool wrapS() const { return (clamp >> 1) & 1; } }; /* Apply mode: @@ -62,7 +64,7 @@ public: 3 - hilight // These two are for PS2 only? 4 - hilight2 */ - unsigned int apply; + unsigned int apply{0}; /* * The textures in this list are as follows: @@ -96,9 +98,8 @@ public: void post(NIFFile *nif) override; }; -class NiFogProperty : public Property +struct NiFogProperty : public Property { -public: unsigned short mFlags; float mFogDepth; osg::Vec3f mColour; @@ -118,6 +119,101 @@ struct NiShadeProperty : public Property } }; + +enum class BSShaderType : unsigned int +{ + ShaderType_TallGrass = 0, + ShaderType_Default = 1, + ShaderType_Sky = 10, + ShaderType_Skin = 14, + ShaderType_Water = 17, + ShaderType_Lighting30 = 29, + ShaderType_Tile = 32, + ShaderType_NoLighting = 33 +}; + +struct BSShaderProperty : public NiShadeProperty +{ + unsigned int type{0u}, flags1{0u}, flags2{0u}; + float envMapIntensity{0.f}; + void read(NIFStream *nif) override; +}; + +struct BSShaderLightingProperty : public BSShaderProperty +{ + unsigned int clamp{0u}; + void read(NIFStream *nif) override; + + bool wrapT() const { return clamp & 1; } + bool wrapS() const { return (clamp >> 1) & 1; } +}; + +struct BSShaderPPLightingProperty : public BSShaderLightingProperty +{ + BSShaderTextureSetPtr textureSet; + struct RefractionSettings + { + float strength{0.f}; + int period{0}; + }; + struct ParallaxSettings + { + float passes{0.f}; + float scale{0.f}; + }; + RefractionSettings refraction; + ParallaxSettings parallax; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +struct BSShaderNoLightingProperty : public BSShaderLightingProperty +{ + std::string filename; + osg::Vec4f falloffParams; + + void read(NIFStream *nif) override; +}; + +enum class BSLightingShaderType : unsigned int +{ + ShaderType_Default = 0, + ShaderType_EnvMap = 1, + ShaderType_Glow = 2, + ShaderType_Parallax = 3, + ShaderType_FaceTint = 4, + ShaderType_SkinTint = 5, + ShaderType_HairTint = 6, + ShaderType_ParallaxOcc = 7, + ShaderType_MultitexLand = 8, + ShaderType_LODLand = 9, + ShaderType_Snow = 10, + ShaderType_MultiLayerParallax = 11, + ShaderType_TreeAnim = 12, + ShaderType_LODObjects = 13, + ShaderType_SparkleSnow = 14, + ShaderType_LODObjectsHD = 15, + ShaderType_EyeEnvmap = 16, + ShaderType_Cloud = 17, + ShaderType_LODNoise = 18, + ShaderType_MultitexLandLODBlend = 19, + ShaderType_Dismemberment = 20 +}; + +struct BSLightingShaderProperty : public BSShaderProperty +{ + BSShaderTextureSetPtr mTextureSet; + unsigned int mClamp{0u}; + float mAlpha; + float mGlossiness; + osg::Vec3f mEmissive, mSpecular; + float mEmissiveMult, mSpecStrength; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + struct NiDitherProperty : public Property { unsigned short flags; @@ -140,6 +236,10 @@ struct NiZBufferProperty : public Property if (nif->getVersion() >= NIFStream::generateVersion(4,1,0,12) && nif->getVersion() <= NIFFile::NIFVersion::VER_OB) testFunction = nif->getUInt(); } + + bool depthTest() const { return flags & 1; } + + bool depthWrite() const { return (flags >> 1) & 1; } }; struct NiSpecularProperty : public Property @@ -150,6 +250,8 @@ struct NiSpecularProperty : public Property Property::read(nif); flags = nif->getUShort(); } + + bool isEnabled() const { return flags & 1; } }; struct NiWireframeProperty : public Property @@ -160,6 +262,8 @@ struct NiWireframeProperty : public Property Property::read(nif); flags = nif->getUShort(); } + + bool isEnabled() const { return flags & 1; } }; // The rest are all struct-based @@ -182,7 +286,7 @@ struct S_MaterialProperty // The vector components are R,G,B osg::Vec3f ambient{1.f,1.f,1.f}, diffuse{1.f,1.f,1.f}; osg::Vec3f specular, emissive; - float glossiness, alpha; + float glossiness{0.f}, alpha{0.f}, emissiveMult{1.f}; void read(NIFStream *nif); }; @@ -206,16 +310,7 @@ struct S_VertexColorProperty struct S_AlphaProperty { /* - In NiAlphaProperty, the flags have the following meaning: - - Bit 0 : alpha blending enable - Bits 1-4 : source blend mode - Bits 5-8 : destination blend mode - Bit 9 : alpha test enable - Bit 10-12 : alpha test mode - Bit 13 : no sorter flag ( disables triangle sorting ) - - blend modes (glBlendFunc): + NiAlphaProperty blend modes (glBlendFunc): 0000 GL_ONE 0001 GL_ZERO 0010 GL_SRC_COLOR @@ -294,8 +389,18 @@ struct S_StencilProperty void read(NIFStream *nif); }; -class NiAlphaProperty : public StructPropT { }; -class NiVertexColorProperty : public StructPropT { }; +struct NiAlphaProperty : public StructPropT +{ + bool useAlphaBlending() const { return flags & 1; } + int sourceBlendMode() const { return (flags >> 1) & 0xF; } + int destinationBlendMode() const { return (flags >> 5) & 0xF; } + bool noSorter() const { return (flags >> 13) & 1; } + + bool useAlphaTesting() const { return (flags >> 9) & 1; } + int alphaTestMode() const { return (flags >> 10) & 0x7; } +}; + +struct NiVertexColorProperty : public StructPropT { }; struct NiStencilProperty : public Property { S_StencilProperty data; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index c5773643a0..37084af44e 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -38,14 +38,14 @@ enum RecordType RC_NiNode, RC_NiSwitchNode, RC_NiLODNode, + RC_NiFltAnimationNode, RC_NiBillboardNode, RC_AvoidNode, RC_NiCollisionSwitch, RC_NiTriShape, RC_NiTriStrips, RC_NiLines, - RC_NiRotatingParticles, - RC_NiAutoNormalParticles, + RC_NiParticles, RC_NiBSParticleNode, RC_NiCamera, RC_NiTexturingProperty, @@ -73,6 +73,7 @@ enum RecordType RC_NiBSAnimationNode, RC_NiLight, RC_NiTextureEffect, + RC_NiExtraData, RC_NiVertWeightsExtraData, RC_NiTextKeyExtraData, RC_NiStringExtraData, @@ -94,7 +95,7 @@ enum RecordType RC_NiUVData, RC_NiPosData, RC_NiRotatingParticlesData, - RC_NiAutoNormalParticlesData, + RC_NiParticlesData, RC_NiSequenceStreamHelper, RC_NiSourceTexture, RC_NiSkinInstance, @@ -120,13 +121,37 @@ enum RecordType RC_NiPoint3Interpolator, RC_NiBoolInterpolator, RC_NiTransformInterpolator, + RC_NiColorInterpolator, + RC_BSShaderTextureSet, + RC_BSLODTriShape, + RC_BSShaderProperty, + RC_BSShaderPPLightingProperty, + RC_BSShaderNoLightingProperty, + RC_BSFurnitureMarker, + RC_NiCollisionObject, + RC_bhkCollisionObject, + RC_BSDismemberSkinInstance, + RC_NiControllerManager, + RC_bhkMoppBvTreeShape, + RC_bhkNiTriStripsShape, + RC_bhkPackedNiTriStripsShape, + RC_hkPackedNiTriStripsData, + RC_bhkConvexVerticesShape, + RC_bhkBoxShape, + RC_bhkListShape, + RC_bhkRigidBody, + RC_bhkRigidBodyT, + RC_BSLightingShaderProperty, + RC_NiClusterAccumulator, + RC_NiAlphaAccumulator, + RC_NiSortAdjustNode }; /// Base class for all records struct Record { // Record type and type name - int recType{RC_MISSING}; + RecordType recType{RC_MISSING}; std::string recName; unsigned int recIndex{~0u}; diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index b40a175838..5ec00b0c92 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -120,34 +120,40 @@ public: }; -class Node; -class Extra; -class Property; -class NiUVData; -class NiPosData; -class NiVisData; -class Controller; -class Named; -class NiSkinData; -class NiFloatData; +struct Node; +struct Extra; +struct Property; +struct NiUVData; +struct NiPosData; +struct NiVisData; +struct Controller; +struct Named; +struct NiSkinData; +struct NiFloatData; struct NiMorphData; -class NiPixelData; -class NiColorData; +struct NiPixelData; +struct NiColorData; struct NiKeyframeData; -class NiTriShapeData; -class NiTriStripsData; -class NiSkinInstance; -class NiSourceTexture; -class NiRotatingParticlesData; -class NiAutoNormalParticlesData; -class NiPalette; +struct NiTriStripsData; +struct NiSkinInstance; +struct NiSourceTexture; +struct NiPalette; struct NiParticleModifier; -struct NiLinesData; struct NiBoolData; struct NiSkinPartition; struct NiFloatInterpolator; struct NiPoint3Interpolator; struct NiTransformInterpolator; +struct BSShaderTextureSet; +struct NiGeometryData; +struct BSShaderProperty; +struct NiAlphaProperty; +struct NiCollisionObject; +struct bhkWorldObject; +struct bhkShape; +struct bhkSerializable; +struct hkPackedNiTriStripsData; +struct NiAccumulator; using NodePtr = RecordPtrT; using ExtraPtr = RecordPtrT; @@ -162,13 +168,8 @@ using NiPixelDataPtr = RecordPtrT; using NiFloatDataPtr = RecordPtrT; using NiColorDataPtr = RecordPtrT; using NiKeyframeDataPtr = RecordPtrT; -using NiTriShapeDataPtr = RecordPtrT; -using NiTriStripsDataPtr = RecordPtrT; -using NiLinesDataPtr = RecordPtrT; using NiSkinInstancePtr = RecordPtrT; using NiSourceTexturePtr = RecordPtrT; -using NiRotatingParticlesDataPtr = RecordPtrT; -using NiAutoNormalParticlesDataPtr = RecordPtrT; using NiPalettePtr = RecordPtrT; using NiParticleModifierPtr = RecordPtrT; using NiBoolDataPtr = RecordPtrT; @@ -176,12 +177,24 @@ using NiSkinPartitionPtr = RecordPtrT; using NiFloatInterpolatorPtr = RecordPtrT; using NiPoint3InterpolatorPtr = RecordPtrT; using NiTransformInterpolatorPtr = RecordPtrT; +using BSShaderTextureSetPtr = RecordPtrT; +using NiGeometryDataPtr = RecordPtrT; +using BSShaderPropertyPtr = RecordPtrT; +using NiAlphaPropertyPtr = RecordPtrT; +using NiCollisionObjectPtr = RecordPtrT; +using bhkWorldObjectPtr = RecordPtrT; +using bhkShapePtr = RecordPtrT; +using hkPackedNiTriStripsDataPtr = RecordPtrT; +using NiAccumulatorPtr = RecordPtrT; using NodeList = RecordListT; using PropertyList = RecordListT; using ExtraList = RecordListT; using NiSourceTextureList = RecordListT; using NiFloatInterpolatorList = RecordListT; +using NiTriStripsDataList = RecordListT; +using bhkShapeList = RecordListT; +using bhkSerializableList = RecordListT; } // Namespace #endif diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 5b531121ee..bae5bd4565 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -1,10 +1,13 @@ #include "bulletnifloader.hpp" +#include #include +#include +#include +#include #include #include -#include #include @@ -14,15 +17,19 @@ #include #include #include +#include + +#include namespace { -osg::Matrixf getWorldTransform(const Nif::Node *node) +osg::Matrixf getWorldTransform(const Nif::Node& node, const Nif::Parent* nodeParent) { - if(node->parent != nullptr) - return node->trafo.toMatrix() * getWorldTransform(node->parent); - return node->trafo.toMatrix(); + osg::Matrixf result = node.trafo.toMatrix(); + for (const Nif::Parent* parent = nodeParent; parent != nullptr; parent = parent->mParent) + result *= parent->mNiNode.trafo.toMatrix(); + return result; } bool pathFileNameStartsWithX(const std::string& path) @@ -34,11 +41,10 @@ bool pathFileNameStartsWithX(const std::string& path) void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriShapeData& data, const osg::Matrixf &transform) { - mesh.preallocateVertices(static_cast(data.vertices.size())); - mesh.preallocateIndices(static_cast(data.triangles.size())); - const std::vector &vertices = data.vertices; const std::vector &triangles = data.triangles; + mesh.preallocateVertices(static_cast(vertices.size())); + mesh.preallocateIndices(static_cast(triangles.size())); for (std::size_t i = 0; i < triangles.size(); i += 3) { @@ -54,8 +60,6 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co { const std::vector &vertices = data.vertices; const std::vector> &strips = data.strips; - if (vertices.empty() || strips.empty()) - return; mesh.preallocateVertices(static_cast(vertices.size())); int numTriangles = 0; for (const std::vector& strip : strips) @@ -73,7 +77,9 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co if (strip.size() < 3) continue; - unsigned short a = strip[0], b = strip[0], c = strip[1]; + unsigned short a; + unsigned short b = strip[0]; + unsigned short c = strip[1]; for (size_t i = 2; i < strip.size(); i++) { a = b; @@ -102,12 +108,56 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::Node* nifNode, const osg::Matrixf &transform = osg::Matrixf()) +template +auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) + -> decltype(function(static_cast(geometry.data.get()))) { - if (nifNode->recType == Nif::RC_NiTriShape) - fillTriangleMesh(mesh, static_cast(nifNode)->data.get(), transform); - else if (nifNode->recType == Nif::RC_NiTriStrips) - fillTriangleMesh(mesh, static_cast(nifNode)->data.get(), transform); + if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) + { + if (geometry.data->recType != Nif::RC_NiTriShapeData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->triangles.empty()) + return {}; + + return function(static_cast(*data)); + } + + if (geometry.recType == Nif::RC_NiTriStrips) + { + if (geometry.data->recType != Nif::RC_NiTriStripsData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->strips.empty()) + return {}; + + return function(static_cast(*data)); + } + + return {}; +} + +std::monostate fillTriangleMesh(std::unique_ptr& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + if (mesh == nullptr) + mesh = std::make_unique(false); + fillTriangleMesh(*mesh, data, transform); + return std::monostate {}; + }); +} + +std::unique_ptr makeChildMesh(const Nif::NiGeometry& geometry) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + auto mesh = std::make_unique(); + fillTriangleMesh(*mesh, data, osg::Matrixf()); + return mesh; + }); } } @@ -123,100 +173,125 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) mStaticMesh.reset(); mAvoidStaticMesh.reset(); - Nif::Node* node = nullptr; + mShape->mFileHash = nif.getHash(); + const size_t numRoots = nif.numRoots(); + std::vector roots; for (size_t i = 0; i < numRoots; ++i) { - Nif::Record* r = nif.getRoot(i); - assert(r != nullptr); - if ((node = dynamic_cast(r))) - break; + const Nif::Record* r = nif.getRoot(i); + if (!r) + continue; + const Nif::Node* node = dynamic_cast(r); + if (node) + roots.emplace_back(node); } - if (!node) + const std::string filename = nif.getFilename(); + mShape->mFileName = filename; + if (roots.empty()) { - warn("Found no root nodes in NIF."); + warn("Found no root nodes in NIF file " + filename); return mShape; } - if (findBoundingBox(node)) - { - const btVector3 halfExtents = Misc::Convert::toBullet(mShape->mCollisionBoxHalfExtents); - const btVector3 origin = Misc::Convert::toBullet(mShape->mCollisionBoxTranslate); - std::unique_ptr compound (new btCompoundShape); - std::unique_ptr boxShape(new btBoxShape(halfExtents)); - btTransform transform = btTransform::getIdentity(); - transform.setOrigin(origin); - compound->addChildShape(transform, boxShape.get()); - boxShape.release(); - - mShape->mCollisionShape = compound.release(); - return mShape; - } - else + // Try to find a valid bounding box first. If one's found for any root node, use that. + for (const Nif::Node* node : roots) { - bool autogenerated = hasAutoGeneratedCollision(node); - - // files with the name convention xmodel.nif usually have keyframes stored in a separate file xmodel.kf (see Animation::addAnimSource). - // assume all nodes in the file will be animated - const auto filename = nif.getFilename(); - const bool isAnimated = pathFileNameStartsWithX(filename); - - handleNode(filename, node, 0, autogenerated, isAnimated, autogenerated); - - if (mCompoundShape) - { - if (mStaticMesh) - { - btTransform trans; - trans.setIdentity(); - std::unique_ptr child(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); - mCompoundShape->addChildShape(trans, child.get()); - child.release(); - mStaticMesh.release(); - } - mShape->mCollisionShape = mCompoundShape.release(); - } - else if (mStaticMesh) + if (findBoundingBox(*node, filename)) { - mShape->mCollisionShape = new Resource::TriangleMeshShape(mStaticMesh.get(), true); - mStaticMesh.release(); + const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); + const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); + auto compound = std::make_unique(); + auto boxShape = std::make_unique(extents); + btTransform transform = btTransform::getIdentity(); + transform.setOrigin(center); + compound->addChildShape(transform, boxShape.get()); + std::ignore = boxShape.release(); + + mShape->mCollisionShape.reset(compound.release()); + return mShape; } + } + // files with the name convention xmodel.nif usually have keyframes stored in a separate file xmodel.kf (see Animation::addAnimSource). + // assume all nodes in the file will be animated + const bool isAnimated = pathFileNameStartsWithX(filename); + + // If there's no bounding box, we'll have to generate a Bullet collision shape + // from the collision data present in every root node. + for (const Nif::Node* node : roots) + { + bool hasCollisionNode = hasRootCollisionNode(*node); + bool hasCollisionShape = hasCollisionNode && !collisionShapeIsEmpty(*node); + if (hasCollisionNode && !hasCollisionShape) + mShape->mCollisionType = Resource::BulletShape::CollisionType::Camera; + bool generateCollisionShape = !hasCollisionShape; + handleNode(filename, *node, nullptr, 0, generateCollisionShape, isAnimated, generateCollisionShape, false, mShape->mCollisionType); + } - if (mAvoidStaticMesh) + if (mCompoundShape) + { + if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { - mShape->mAvoidCollisionShape = new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false); - mAvoidStaticMesh.release(); + btTransform trans; + trans.setIdentity(); + std::unique_ptr child = std::make_unique(mStaticMesh.get(), true); + mCompoundShape->addChildShape(trans, child.get()); + std::ignore = child.release(); + std::ignore = mStaticMesh.release(); } + mShape->mCollisionShape = std::move(mCompoundShape); + } + else if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) + { + mShape->mCollisionShape.reset(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); + std::ignore = mStaticMesh.release(); + } - return mShape; + if (mAvoidStaticMesh != nullptr && mAvoidStaticMesh->getNumTriangles() > 0) + { + mShape->mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false)); + std::ignore = mAvoidStaticMesh.release(); } + + return mShape; } // Find a boundingBox in the node hierarchy. // Return: use bounding box for collision? -bool BulletNifLoader::findBoundingBox(const Nif::Node* node) +bool BulletNifLoader::findBoundingBox(const Nif::Node& node, const std::string& filename) { - if (node->hasBounds) + if (node.hasBounds) { - mShape->mCollisionBoxHalfExtents = node->boundXYZ; - mShape->mCollisionBoxTranslate = node->boundPos; + unsigned int type = node.bounds.type; + switch (type) + { + case Nif::NiBoundingVolume::Type::BOX_BV: + mShape->mCollisionBox.mExtents = node.bounds.box.extents; + mShape->mCollisionBox.mCenter = node.bounds.box.center; + break; + default: + { + std::stringstream warning; + warning << "Unsupported NiBoundingVolume type " << type << " in node " << node.recIndex; + warning << " in file " << filename; + warn(warning.str()); + } + } - if (node->flags & Nif::NiNode::Flag_BBoxCollision) + if (node.hasBBoxCollision()) { return true; } } - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) { - bool found = findBoundingBox (list[i].getPtr()); - if (found) + if (findBoundingBox(list[i].get(), filename)) return true; } } @@ -224,50 +299,72 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node) return false; } -bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) +bool BulletNifLoader::hasRootCollisionNode(const Nif::Node& rootNode) const { - const Nif::NiNode *ninode = dynamic_cast(rootNode); - if(ninode) + if (const Nif::NiNode* ninode = dynamic_cast(&rootNode)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { - if(!list[i].empty()) - { - if(list[i].getPtr()->recType == Nif::RC_RootCollisionNode) - return false; - } + if(list[i].empty()) + continue; + if (list[i].getPtr()->recType == Nif::RC_RootCollisionNode) + return true; + } + } + return false; +} + +bool BulletNifLoader::collisionShapeIsEmpty(const Nif::Node& rootNode) const +{ + if (const Nif::NiNode* ninode = dynamic_cast(&rootNode)) + { + const Nif::NodeList &list = ninode->children; + for(size_t i = 0;i < list.length();i++) + { + if(list[i].empty()) + continue; + const Nif::Node* childNode = list[i].getPtr(); + if (childNode->recType != Nif::RC_RootCollisionNode) + continue; + const Nif::NiNode* niChildnode = static_cast(childNode); // RootCollisionNode is always a NiNode + if (childNode->hasBounds || niChildnode->children.length() > 0) + return false; } } return true; } -void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *node, int flags, - bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid) +void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& node, const Nif::Parent* parent, + int flags, bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid, unsigned int& collisionType) { // TODO: allow on-the fly collision switching via toggling this flag - if (node->recType == Nif::RC_NiCollisionSwitch && !(node->flags & Nif::NiNode::Flag_ActiveCollision)) + if (node.recType == Nif::RC_NiCollisionSwitch && !node.collisionActive()) + return; + + // If RootCollisionNode is empty we treat it as NCC flag and autogenerate collision shape as there was no RootCollisionNode. + // So ignoring it here if `autogenerated` is true and collisionType was set to `Camera`. + if (node.recType == Nif::RC_RootCollisionNode && autogenerated && collisionType == Resource::BulletShape::CollisionType::Camera) return; // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. - flags |= node->flags; + flags |= node.flags; - if (!node->controller.empty() && node->controller->recType == Nif::RC_NiKeyframeController - && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) + if (!node.controller.empty() && node.controller->recType == Nif::RC_NiKeyframeController && node.controller->isActive()) isAnimated = true; - isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); + isCollisionNode = isCollisionNode || (node.recType == Nif::RC_RootCollisionNode); // Don't collide with AvoidNode shapes - avoid = avoid || (node->recType == Nif::RC_AvoidNode); + avoid = avoid || (node.recType == Nif::RC_AvoidNode); // We encountered a RootCollisionNode inside autogenerated mesh. It is not right. - if (node->recType == Nif::RC_RootCollisionNode && autogenerated) + if (node.recType == Nif::RC_RootCollisionNode && autogenerated) Log(Debug::Info) << "RootCollisionNode is not attached to the root node in " << fileName << ". Treating it as a common NiTriShape."; // Check for extra data - for (Nif::ExtraPtr e = node->extra; !e.empty(); e = e->next) + for (Nif::ExtraPtr e = node.extra; !e.empty(); e = e->next) { if (e->recType == Nif::RC_NiStringExtraData) { @@ -277,8 +374,13 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *n if (Misc::StringUtils::ciCompareLen(sd->string, "NC", 2) == 0) { - // No collision. Use an internal flag setting to mark this. - flags |= 0x800; + // NCC flag in vanilla is partly case sensitive: prefix NC is case insensitive but second C needs be uppercase + if (sd->string.length() > 2 && sd->string[2] == 'C') + // Collide only with camera. + collisionType = Resource::BulletShape::CollisionType::Camera; + else + // No collision. + collisionType = Resource::BulletShape::CollisionType::None; } else if (sd->string == "MRK" && autogenerated) { @@ -294,98 +396,69 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *n // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. // (occurs in tr_ex_imp_wall_arch_04.nif) - if(!node->hasBounds && (node->recType == Nif::RC_NiTriShape || node->recType == Nif::RC_NiTriStrips)) + if(!node.hasBounds && (node.recType == Nif::RC_NiTriShape + || node.recType == Nif::RC_NiTriStrips + || node.recType == Nif::RC_BSLODTriShape)) { - handleNiTriShape(node, flags, getWorldTransform(node), isAnimated, avoid); + handleNiTriShape(static_cast(node), parent, getWorldTransform(node, parent), isAnimated, avoid); } } // For NiNodes, loop through children - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; + const Nif::Parent currentParent {*ninode, parent}; for(size_t i = 0;i < list.length();i++) { - if(!list[i].empty()) - handleNode(fileName, list[i].getPtr(), flags, isCollisionNode, isAnimated, autogenerated, avoid); + if (list[i].empty()) + continue; + + assert(std::find(list[i]->parents.begin(), list[i]->parents.end(), ninode) != list[i]->parents.end()); + handleNode(fileName, list[i].get(), ¤tParent, flags, isCollisionNode, isAnimated, autogenerated, avoid, collisionType); } } } -void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf &transform, - bool isAnimated, bool avoid) +void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const Nif::Parent* nodeParent, + const osg::Matrixf &transform, bool isAnimated, bool avoid) { - assert(nifNode != nullptr); - - // If the object was marked "NCO" earlier, it shouldn't collide with - // anything. So don't do anything. - if ((flags & 0x800)) + if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) return; - if (nifNode->recType == Nif::RC_NiTriShape) - { - const Nif::NiTriShape* shape = static_cast(nifNode); - if (!shape->skin.empty()) - isAnimated = false; - if (shape->data.empty() || shape->data->triangles.empty()) - return; - } - else - { - const Nif::NiTriStrips* shape = static_cast(nifNode); - if (!shape->skin.empty()) - isAnimated = false; - if (shape->data.empty() || shape->data->strips.empty()) - return; - } - + if (!niGeometry.skin.empty()) + isAnimated = false; if (isAnimated) { + std::unique_ptr childMesh = makeChildMesh(niGeometry); + if (childMesh == nullptr || childMesh->getNumTriangles() == 0) + return; + if (!mCompoundShape) mCompoundShape.reset(new btCompoundShape); - std::unique_ptr childMesh(new btTriangleMesh); - - fillTriangleMesh(*childMesh, nifNode); - - std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); - childMesh.release(); + auto childShape = std::make_unique(childMesh.get(), true); + std::ignore = childMesh.release(); - float scale = nifNode->trafo.scale; - const Nif::Node* parent = nifNode; - while (parent->parent) - { - parent = parent->parent; - scale *= parent->trafo.scale; - } + float scale = niGeometry.trafo.scale; + for (const Nif::Parent* parent = nodeParent; parent != nullptr; parent = parent->mParent) + scale *= parent->mNiNode.trafo.scale; osg::Quat q = transform.getRotate(); osg::Vec3f v = transform.getTrans(); childShape->setLocalScaling(btVector3(scale, scale, scale)); btTransform trans(btQuaternion(q.x(), q.y(), q.z(), q.w()), btVector3(v.x(), v.y(), v.z())); - mShape->mAnimatedShapes.emplace(nifNode->recIndex, mCompoundShape->getNumChildShapes()); + mShape->mAnimatedShapes.emplace(niGeometry.recIndex, mCompoundShape->getNumChildShapes()); mCompoundShape->addChildShape(trans, childShape.get()); - childShape.release(); + std::ignore = childShape.release(); } else if (avoid) - { - if (!mAvoidStaticMesh) - mAvoidStaticMesh.reset(new btTriangleMesh(false)); - - fillTriangleMesh(*mAvoidStaticMesh, nifNode, transform); - } + fillTriangleMesh(mAvoidStaticMesh, niGeometry, transform); else - { - if (!mStaticMesh) - mStaticMesh.reset(new btTriangleMesh(false)); - - // Static shape, just transform all vertices into position - fillTriangleMesh(*mStaticMesh, nifNode, transform); - } + fillTriangleMesh(mStaticMesh, niGeometry, transform); } } // namespace NifBullet diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index e423e51496..c674bbbd13 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -23,10 +23,12 @@ class btCollisionShape; namespace Nif { - class Node; + struct Node; struct Transformation; struct NiTriShape; struct NiTriStrips; + struct NiGeometry; + struct Parent; } namespace NifBullet @@ -40,10 +42,10 @@ class BulletNifLoader public: void warn(const std::string &msg) { - Log(Debug::Warning) << "NIFLoader: Warn:" << msg; + Log(Debug::Warning) << "NIFLoader: Warn: " << msg; } - void fail(const std::string &msg) + [[noreturn]] void fail(const std::string &msg) { Log(Debug::Error) << "NIFLoader: Fail: "<< msg; abort(); @@ -52,16 +54,18 @@ public: osg::ref_ptr load(const Nif::File& file); private: - bool findBoundingBox(const Nif::Node* node); + bool findBoundingBox(const Nif::Node& node, const std::string& filename); - void handleNode(const std::string& fileName, Nif::Node const *node, int flags, bool isCollisionNode, - bool isAnimated=false, bool autogenerated=false, bool avoid=false); + void handleNode(const std::string& fileName, const Nif::Node& node,const Nif::Parent* parent, int flags, + bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid, unsigned int& cameraOnlyCollision); - bool hasAutoGeneratedCollision(const Nif::Node *rootNode); + bool hasRootCollisionNode(const Nif::Node& rootNode) const; + bool collisionShapeIsEmpty(const Nif::Node& rootNode) const; - void handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); + void handleNiTriShape(const Nif::NiGeometry& nifNode, const Nif::Parent* parent, const osg::Matrixf& transform, + bool isAnimated, bool avoid); - std::unique_ptr mCompoundShape; + std::unique_ptr mCompoundShape; std::unique_ptr mStaticMesh; diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 64e9f7de6c..b2beead8bd 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -20,7 +20,7 @@ ControllerFunction::ControllerFunction(const Nif::Controller *ctrl) , mPhase(ctrl->phase) , mStartTime(ctrl->timeStart) , mStopTime(ctrl->timeStop) - , mExtrapolationMode(static_cast((ctrl->flags&0x6) >> 1)) + , mExtrapolationMode(ctrl->extrapolationMode()) { } @@ -31,7 +31,7 @@ float ControllerFunction::calculate(float value) const return time; switch (mExtrapolationMode) { - case Cycle: + case Nif::Controller::ExtrapolationMode::Cycle: { float delta = mStopTime - mStartTime; if ( delta <= 0 ) @@ -40,7 +40,7 @@ float ControllerFunction::calculate(float value) const float remainder = ( cycles - std::floor( cycles ) ) * delta; return mStartTime + remainder; } - case Reverse: + case Nif::Controller::ExtrapolationMode::Reverse: { float delta = mStopTime - mStartTime; if ( delta <= 0 ) @@ -55,9 +55,9 @@ float ControllerFunction::calculate(float value) const return mStopTime - remainder; } - case Constant: + case Nif::Controller::ExtrapolationMode::Constant: default: - return std::min(mStopTime, std::max(mStartTime, time)); + return std::clamp(time, mStartTime, mStopTime); } } @@ -71,45 +71,52 @@ KeyframeController::KeyframeController() } KeyframeController::KeyframeController(const KeyframeController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) - , Controller(copy) + : osg::Object(copy, copyop) + , SceneUtil::KeyframeController(copy) + , SceneUtil::NodeCallback(copy, copyop) , mRotations(copy.mRotations) , mXRotations(copy.mXRotations) , mYRotations(copy.mYRotations) , mZRotations(copy.mZRotations) , mTranslations(copy.mTranslations) , mScales(copy.mScales) + , mAxisOrder(copy.mAxisOrder) { } -KeyframeController::KeyframeController(const Nif::NiKeyframeData *data) - : mRotations(data->mRotations) - , mXRotations(data->mXRotations, 0.f) - , mYRotations(data->mYRotations, 0.f) - , mZRotations(data->mZRotations, 0.f) - , mTranslations(data->mTranslations, osg::Vec3f()) - , mScales(data->mScales, 1.f) -{ -} - -KeyframeController::KeyframeController(const Nif::NiTransformInterpolator* interpolator) - : mRotations(interpolator->data->mRotations, interpolator->defaultRot) - , mXRotations(interpolator->data->mXRotations, 0.f) - , mYRotations(interpolator->data->mYRotations, 0.f) - , mZRotations(interpolator->data->mZRotations, 0.f) - , mTranslations(interpolator->data->mTranslations, interpolator->defaultPos) - , mScales(interpolator->data->mScales, interpolator->defaultScale) -{ -} - -KeyframeController::KeyframeController(const float scale, const osg::Vec3f& pos, const osg::Quat& rot) - : mRotations(Nif::QuaternionKeyMapPtr(), rot) - , mXRotations(Nif::FloatKeyMapPtr(), 0.f) - , mYRotations(Nif::FloatKeyMapPtr(), 0.f) - , mZRotations(Nif::FloatKeyMapPtr(), 0.f) - , mTranslations(Nif::Vector3KeyMapPtr(), pos) - , mScales(Nif::FloatKeyMapPtr(), scale) +KeyframeController::KeyframeController(const Nif::NiKeyframeController *keyctrl) { + if (!keyctrl->interpolator.empty()) + { + const Nif::NiTransformInterpolator* interp = keyctrl->interpolator.getPtr(); + if (!interp->data.empty()) + { + mRotations = QuaternionInterpolator(interp->data->mRotations, interp->defaultRot); + mXRotations = FloatInterpolator(interp->data->mXRotations); + mYRotations = FloatInterpolator(interp->data->mYRotations); + mZRotations = FloatInterpolator(interp->data->mZRotations); + mTranslations = Vec3Interpolator(interp->data->mTranslations, interp->defaultPos); + mScales = FloatInterpolator(interp->data->mScales, interp->defaultScale); + mAxisOrder = interp->data->mAxisOrder; + } + else + { + mRotations = QuaternionInterpolator(Nif::QuaternionKeyMapPtr(), interp->defaultRot); + mTranslations = Vec3Interpolator(Nif::Vector3KeyMapPtr(), interp->defaultPos); + mScales = FloatInterpolator(Nif::FloatKeyMapPtr(), interp->defaultScale); + } + } + else if (!keyctrl->data.empty()) + { + const Nif::NiKeyframeData* keydata = keyctrl->data.getPtr(); + mRotations = QuaternionInterpolator(keydata->mRotations); + mXRotations = FloatInterpolator(keydata->mXRotations); + mYRotations = FloatInterpolator(keydata->mYRotations); + mZRotations = FloatInterpolator(keydata->mZRotations); + mTranslations = Vec3Interpolator(keydata->mTranslations); + mScales = FloatInterpolator(keydata->mScales, 1.f); + mAxisOrder = keydata->mAxisOrder; + } } osg::Quat KeyframeController::getXYZRotation(float time) const @@ -121,10 +128,31 @@ osg::Quat KeyframeController::getXYZRotation(float time) const yrot = mYRotations.interpKey(time); if (!mZRotations.empty()) zrot = mZRotations.interpKey(time); - osg::Quat xr(xrot, osg::Vec3f(1,0,0)); - osg::Quat yr(yrot, osg::Vec3f(0,1,0)); - osg::Quat zr(zrot, osg::Vec3f(0,0,1)); - return (xr*yr*zr); + osg::Quat xr(xrot, osg::X_AXIS); + osg::Quat yr(yrot, osg::Y_AXIS); + osg::Quat zr(zrot, osg::Z_AXIS); + switch (mAxisOrder) + { + case Nif::NiKeyframeData::AxisOrder::Order_XYZ: + return xr * yr * zr; + case Nif::NiKeyframeData::AxisOrder::Order_XZY: + return xr * zr * yr; + case Nif::NiKeyframeData::AxisOrder::Order_YZX: + return yr * zr * xr; + case Nif::NiKeyframeData::AxisOrder::Order_YXZ: + return yr * xr * zr; + case Nif::NiKeyframeData::AxisOrder::Order_ZXY: + return zr * xr * yr; + case Nif::NiKeyframeData::AxisOrder::Order_ZYX: + return zr * yr * xr; + case Nif::NiKeyframeData::AxisOrder::Order_XYX: + return xr * yr * xr; + case Nif::NiKeyframeData::AxisOrder::Order_YZY: + return yr * zr * yr; + case Nif::NiKeyframeData::AxisOrder::Order_ZXZ: + return zr * xr * zr; + } + return xr * yr * zr; } osg::Vec3f KeyframeController::getTranslation(float time) const @@ -134,53 +162,24 @@ osg::Vec3f KeyframeController::getTranslation(float time) const return osg::Vec3f(); } -void KeyframeController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void KeyframeController::operator() (NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) { if (hasInput()) { - NifOsg::MatrixTransform* trans = static_cast(node); - osg::Matrix mat = trans->getMatrix(); - float time = getInputValue(nv); - Nif::Matrix3& rot = trans->mRotationScale; - - bool setRot = false; - if(!mRotations.empty()) - { - mat.setRotate(mRotations.interpKey(time)); - setRot = true; - } + if (!mRotations.empty()) + node->setRotation(mRotations.interpKey(time)); else if (!mXRotations.empty() || !mYRotations.empty() || !mZRotations.empty()) - { - mat.setRotate(getXYZRotation(time)); - setRot = true; - } + node->setRotation(getXYZRotation(time)); else - { - // no rotation specified, use the previous value - for (int i=0;i<3;++i) - for (int j=0;j<3;++j) - mat(j,i) = rot.mValues[i][j]; // NB column/row major difference - } - - if (setRot) // copy the new values back - for (int i=0;i<3;++i) - for (int j=0;j<3;++j) - rot.mValues[i][j] = mat(j,i); // NB column/row major difference - - float& scale = trans->mScale; - if(!mScales.empty()) - scale = mScales.interpKey(time); - - for (int i=0;i<3;++i) - for (int j=0;j<3;++j) - mat(i,j) *= scale; + node->setRotation(node->mRotationScale); - if(!mTranslations.empty()) - mat.setTrans(mTranslations.interpKey(time)); + if (!mScales.empty()) + node->setScale(mScales.interpKey(time)); - trans->setMatrix(mat); + if (!mTranslations.empty()) + node->setTranslation(mTranslations.interpKey(time)); } traverse(node, nv); @@ -191,8 +190,8 @@ GeomMorpherController::GeomMorpherController() } GeomMorpherController::GeomMorpherController(const GeomMorpherController ©, const osg::CopyOp ©op) - : osg::Drawable::UpdateCallback(copy, copyop) - , Controller(copy) + : Controller(copy) + , SceneUtil::NodeCallback(copy, copyop) , mKeyFrames(copy.mKeyFrames) { } @@ -218,26 +217,25 @@ GeomMorpherController::GeomMorpherController(const Nif::NiGeomMorpherController* } } -void GeomMorpherController::update(osg::NodeVisitor *nv, osg::Drawable *drawable) +void GeomMorpherController::operator()(SceneUtil::MorphGeometry* node, osg::NodeVisitor *nv) { - SceneUtil::MorphGeometry* morphGeom = static_cast(drawable); if (hasInput()) { if (mKeyFrames.size() <= 1) return; float input = getInputValue(nv); - int i = 0; + int i = 1; for (std::vector::iterator it = mKeyFrames.begin()+1; it != mKeyFrames.end(); ++it,++i) { float val = 0; if (!(*it).empty()) val = it->interpKey(input); - SceneUtil::MorphGeometry::MorphTarget& target = morphGeom->getMorphTarget(i); + SceneUtil::MorphGeometry::MorphTarget& target = node->getMorphTarget(i); if (target.getWeight() != val) { target.setWeight(val); - morphGeom->dirty(); + node->dirty(); } } } @@ -278,19 +276,18 @@ void UVController::apply(osg::StateSet* stateset, osg::NodeVisitor* nv) if (hasInput()) { float value = getInputValue(nv); - float uTrans = mUTrans.interpKey(value); - float vTrans = mVTrans.interpKey(value); - float uScale = mUScale.interpKey(value); - float vScale = mVScale.interpKey(value); - - osg::Matrix flipMat; - flipMat.preMultTranslate(osg::Vec3f(0,1,0)); - flipMat.preMultScale(osg::Vec3f(1,-1,1)); - osg::Matrixf mat = osg::Matrixf::scale(uScale, vScale, 1); - mat.setTrans(uTrans, vTrans, 0); + // First scale the UV relative to its center, then apply the offset. + // U offset is flipped regardless of the graphics library, + // while V offset is flipped to account for OpenGL Y axis convention. + osg::Vec3f uvOrigin(0.5f, 0.5f, 0.f); + osg::Vec3f uvScale(mUScale.interpKey(value), mVScale.interpKey(value), 1.f); + osg::Vec3f uvTrans(-mUTrans.interpKey(value), -mVTrans.interpKey(value), 0.f); - mat = flipMat * mat * flipMat; + osg::Matrixf mat = osg::Matrixf::translate(uvOrigin); + mat.preMultScale(uvScale); + mat.preMultTranslate(-uvOrigin); + mat.setTrans(mat.getTrans() + uvTrans); // setting once is enough because all other texture units share the same TexMat (see setDefaults). if (!mTextureUnits.empty()) @@ -313,7 +310,7 @@ VisController::VisController() } VisController::VisController(const VisController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mData(copy.mData) , mMask(copy.mMask) @@ -354,14 +351,14 @@ RollController::RollController(const Nif::NiFloatInterpolator* interpolator) } RollController::RollController(const RollController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mData(copy.mData) , mStartingTime(copy.mStartingTime) { } -void RollController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void RollController::operator() (osg::MatrixTransform* node, osg::NodeVisitor* nv) { traverse(node, nv); @@ -372,15 +369,16 @@ void RollController::operator() (osg::Node* node, osg::NodeVisitor* nv) mStartingTime = newTime; float value = mData.interpKey(getInputValue(nv)); - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); // Rotate around "roll" axis. // Note: in original game rotation speed is the framerate-dependent in a very tricky way. // Do not replicate this behaviour until we will really need it. // For now consider controller's current value as an angular speed in radians per 1/60 seconds. - matrix = osg::Matrix::rotate(value * duration * 60.f, 0, 0, 1) * matrix; - transform->setMatrix(matrix); + node->preMult(osg::Matrix::rotate(value * duration * 60.f, 0, 0, 1)); + + // Note: doing it like this means RollControllers are not compatible with KeyframeControllers. + // KeyframeController currently wins the conflict. + // However unlikely that is, NetImmerse might combine the transformations somehow. } } @@ -546,29 +544,28 @@ ParticleSystemController::ParticleSystemController() } ParticleSystemController::ParticleSystemController(const ParticleSystemController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mEmitStart(copy.mEmitStart) , mEmitStop(copy.mEmitStop) { } -void ParticleSystemController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void ParticleSystemController::operator() (osgParticle::ParticleProcessor* node, osg::NodeVisitor* nv) { - osgParticle::ParticleProcessor* emitter = static_cast(node); if (hasInput()) { float time = getInputValue(nv); - emitter->getParticleSystem()->setFrozen(false); - emitter->setEnabled(time >= mEmitStart && time < mEmitStop); + node->getParticleSystem()->setFrozen(false); + node->setEnabled(time >= mEmitStart && time < mEmitStop); } else - emitter->getParticleSystem()->setFrozen(true); + node->getParticleSystem()->setFrozen(true); traverse(node, nv); } PathController::PathController(const PathController ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) , Controller(copy) , mPath(copy.mPath) , mPercent(copy.mPercent) @@ -593,7 +590,7 @@ float PathController::getPercent(float time) const return percent; } -void PathController::operator() (osg::Node* node, osg::NodeVisitor* nv) +void PathController::operator() (NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) { if (mPath.empty() || mPercent.empty() || !hasInput()) { @@ -601,14 +598,9 @@ void PathController::operator() (osg::Node* node, osg::NodeVisitor* nv) return; } - osg::MatrixTransform* trans = static_cast(node); - osg::Matrix mat = trans->getMatrix(); - float time = getInputValue(nv); float percent = getPercent(time); - osg::Vec3f pos(mPath.interpKey(percent)); - mat.setTrans(pos); - trans->setMatrix(mat); + node->setTranslation(mPath.interpKey(percent)); traverse(node, nv); } diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index a29fabefd0..c79c6a70d1 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -6,7 +6,8 @@ #include #include -#include +#include +#include #include #include @@ -14,24 +15,27 @@ #include -#include -#include -#include - - namespace osg { class Material; + class MatrixTransform; } namespace osgParticle { - class Emitter; + class ParticleProcessor; +} + +namespace SceneUtil +{ + class MorphGeometry; } namespace NifOsg { + class MatrixTransform; + // interpolation of keyframes template class ValueInterpolator @@ -66,7 +70,9 @@ namespace NifOsg std::conjunction_v< std::disjunction< std::is_same, - std::is_same + std::is_same, + std::is_same, + std::is_same >, std::is_same >, @@ -83,7 +89,7 @@ namespace NifOsg mLastLowKey = mKeys->mKeys.end(); mLastHighKey = mKeys->mKeys.end(); } - }; + } ValueInterpolator(std::shared_ptr keys, ValueT defaultVal = ValueT()) : mKeys(keys) @@ -194,13 +200,7 @@ namespace NifOsg float mPhase; float mStartTime; float mStopTime; - enum ExtrapolationMode - { - Cycle = 0, - Reverse = 1, - Constant = 2 - }; - ExtrapolationMode mExtrapolationMode; + Nif::Controller::ExtrapolationMode mExtrapolationMode; public: ControllerFunction(const Nif::Controller *ctrl); @@ -210,8 +210,7 @@ namespace NifOsg float getMaximum() const override; }; - /// Must be set on a SceneUtil::MorphGeometry. - class GeomMorpherController : public osg::Drawable::UpdateCallback, public SceneUtil::Controller + class GeomMorpherController : public SceneUtil::Controller, public SceneUtil::NodeCallback { public: GeomMorpherController(const Nif::NiGeomMorpherController* ctrl); @@ -220,31 +219,33 @@ namespace NifOsg META_Object(NifOsg, GeomMorpherController) - void update(osg::NodeVisitor* nv, osg::Drawable* drawable) override; + void operator()(SceneUtil::MorphGeometry*, osg::NodeVisitor*); private: std::vector mKeyFrames; }; - class KeyframeController : public osg::NodeCallback, public SceneUtil::Controller +#ifdef _MSC_VER +#pragma warning( push ) + /* + * Warning C4250: 'NifOsg::KeyframeController': inherits 'osg::Callback::osg::Callback::asCallback' via dominance, + * there is no way to solved this if an object must inherit from both osg::Object and osg::Callback + */ +#pragma warning( disable : 4250 ) +#endif + class KeyframeController : public SceneUtil::KeyframeController, public SceneUtil::NodeCallback { public: - // This is used if there's no interpolator but there is data (Morrowind meshes). - KeyframeController(const Nif::NiKeyframeData *data); - // This is used if the interpolator has data. - KeyframeController(const Nif::NiTransformInterpolator* interpolator); - // This is used if there are default values available (e.g. from a data-less interpolator). - // If there's neither keyframe data nor an interpolator a KeyframeController must not be created. - KeyframeController(const float scale, const osg::Vec3f& pos, const osg::Quat& rot); - KeyframeController(); KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop); + KeyframeController(const Nif::NiKeyframeController *keyctrl); META_Object(NifOsg, KeyframeController) - virtual osg::Vec3f getTranslation(float time) const; + osg::Vec3f getTranslation(float time) const override; + osg::Callback* getAsCallback() override { return this; } - void operator() (osg::Node*, osg::NodeVisitor*) override; + void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); private: QuaternionInterpolator mRotations; @@ -256,8 +257,13 @@ namespace NifOsg Vec3Interpolator mTranslations; FloatInterpolator mScales; + Nif::NiKeyframeData::AxisOrder mAxisOrder{Nif::NiKeyframeData::AxisOrder::Order_XYZ}; + osg::Quat getXYZRotation(float time) const; }; +#ifdef _MSC_VER +#pragma warning( pop ) +#endif class UVController : public SceneUtil::StateSetUpdater, public SceneUtil::Controller { @@ -279,7 +285,7 @@ namespace NifOsg std::set mTextureUnits; }; - class VisController : public osg::NodeCallback, public SceneUtil::Controller + class VisController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: std::vector mData; @@ -294,10 +300,10 @@ namespace NifOsg META_Object(NifOsg, VisController) - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osg::Node* node, osg::NodeVisitor* nv); }; - class RollController : public osg::NodeCallback, public SceneUtil::Controller + class RollController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: FloatInterpolator mData; @@ -309,7 +315,7 @@ namespace NifOsg RollController() = default; RollController(const RollController& copy, const osg::CopyOp& copyop); - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osg::MatrixTransform* node, osg::NodeVisitor* nv); META_Object(NifOsg, RollController) }; @@ -380,7 +386,7 @@ namespace NifOsg void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; }; - class ParticleSystemController : public osg::NodeCallback, public SceneUtil::Controller + class ParticleSystemController : public SceneUtil::NodeCallback, public SceneUtil::Controller { public: ParticleSystemController(const Nif::NiParticleSystemController* ctrl); @@ -389,14 +395,14 @@ namespace NifOsg META_Object(NifOsg, ParticleSystemController) - void operator() (osg::Node* node, osg::NodeVisitor* nv) override; + void operator() (osgParticle::ParticleProcessor* node, osg::NodeVisitor* nv); private: float mEmitStart; float mEmitStop; }; - class PathController : public osg::NodeCallback, public SceneUtil::Controller + class PathController : public SceneUtil::NodeCallback, public SceneUtil::Controller { public: PathController(const Nif::NiPathController* ctrl); @@ -405,7 +411,7 @@ namespace NifOsg META_Object(NifOsg, PathController) - void operator() (osg::Node*, osg::NodeVisitor*) override; + void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); private: Vec3Interpolator mPath; diff --git a/components/nifosg/matrixtransform.cpp b/components/nifosg/matrixtransform.cpp index bc461b9c10..2170c84e5c 100644 --- a/components/nifosg/matrixtransform.cpp +++ b/components/nifosg/matrixtransform.cpp @@ -2,11 +2,6 @@ namespace NifOsg { - MatrixTransform::MatrixTransform() - : osg::MatrixTransform() - { - } - MatrixTransform::MatrixTransform(const Nif::Transformation &trafo) : osg::MatrixTransform(trafo.toMatrix()) , mScale(trafo.scale) @@ -20,4 +15,60 @@ namespace NifOsg , mRotationScale(copy.mRotationScale) { } + + void MatrixTransform::setScale(float scale) + { + // Update the decomposed scale. + mScale = scale; + + // Rescale the node using the known components. + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + _matrix(i,j) = mRotationScale.mValues[j][i] * mScale; // NB: column/row major difference + + _inverseDirty = true; + dirtyBound(); + } + + void MatrixTransform::setRotation(const osg::Quat &rotation) + { + // First override the rotation ignoring the scale. + _matrix.setRotate(rotation); + + for (int i = 0; i < 3; ++i) + { + for (int j = 0; j < 3; ++j) + { + // Update the current decomposed rotation and restore the known scale. + mRotationScale.mValues[j][i] = _matrix(i,j); // NB: column/row major difference + _matrix(i,j) *= mScale; + } + } + + _inverseDirty = true; + dirtyBound(); + } + + void MatrixTransform::setRotation(const Nif::Matrix3 &rotation) + { + // Update the decomposed rotation. + mRotationScale = rotation; + + // Reorient the node using the known components. + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + _matrix(i,j) = mRotationScale.mValues[j][i] * mScale; // NB: column/row major difference + + _inverseDirty = true; + dirtyBound(); + } + + void MatrixTransform::setTranslation(const osg::Vec3f &translation) + { + // The translation is independent from the rotation and scale so we can apply it directly. + _matrix.setTrans(translation); + + _inverseDirty = true; + dirtyBound(); + } } diff --git a/components/nifosg/matrixtransform.hpp b/components/nifosg/matrixtransform.hpp index 975f71c622..ffb3c1c37d 100644 --- a/components/nifosg/matrixtransform.hpp +++ b/components/nifosg/matrixtransform.hpp @@ -11,7 +11,7 @@ namespace NifOsg class MatrixTransform : public osg::MatrixTransform { public: - MatrixTransform(); + MatrixTransform() = default; MatrixTransform(const Nif::Transformation &trafo); MatrixTransform(const MatrixTransform ©, const osg::CopyOp ©op); @@ -24,6 +24,14 @@ namespace NifOsg // we store the scale and rotation components separately here. float mScale{0.f}; Nif::Matrix3 mRotationScale; + + // Utility methods to transform the node and keep these components up-to-date. + // The matrix's components should not be overridden manually or using preMult/postMult + // unless you're sure you know what you are doing. + void setScale(float scale); + void setRotation(const osg::Quat &rotation); + void setRotation(const Nif::Matrix3 &rotation); + void setTranslation(const osg::Vec3f &translation); }; } diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index a5a61b3176..fd3fb40d9f 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -1,12 +1,14 @@ #include "nifloader.hpp" #include +#include #include #include #include #include #include +#include #include #include @@ -16,7 +18,8 @@ #include #include #include -#include +#include +#include // particle #include @@ -47,11 +50,25 @@ namespace { + struct DisableOptimizer : osg::NodeVisitor + { + DisableOptimizer(osg::NodeVisitor::TraversalMode mode = TRAVERSE_ALL_CHILDREN) : osg::NodeVisitor(mode) {} + + void apply(osg::Node &node) override + { + node.setDataVariance(osg::Object::DYNAMIC); + traverse(node); + } + + void apply(osg::Drawable &node) override + { + traverse(node); + } + }; void getAllNiNodes(const Nif::Node* node, std::vector& outIndices) { - const Nif::NiNode* ninode = dynamic_cast(node); - if (ninode) + if (const Nif::NiNode* ninode = dynamic_cast(node)) { outIndices.push_back(ninode->recIndex); for (unsigned int i=0; ichildren.length(); ++i) @@ -67,16 +84,17 @@ namespace case Nif::RC_NiTriShape: case Nif::RC_NiTriStrips: case Nif::RC_NiLines: + case Nif::RC_BSLODTriShape: return true; } return false; } // Collect all properties affecting the given drawable that should be handled on drawable basis rather than on the node hierarchy above it. - void collectDrawableProperties(const Nif::Node* nifNode, std::vector& out) + void collectDrawableProperties(const Nif::Node* nifNode, const Nif::Parent* parent, std::vector& out) { - if (nifNode->parent) - collectDrawableProperties(nifNode->parent, out); + if (parent != nullptr) + collectDrawableProperties(&parent->mNiNode, parent->mParent, out); const Nif::PropertyList& props = nifNode->props; for (size_t i = 0; i (nifNode); + if (geometry) + { + if (!geometry->shaderprop.empty()) + out.emplace_back(geometry->shaderprop.getPtr()); + if (!geometry->alphaprop.empty()) + out.emplace_back(geometry->alphaprop.getPtr()); + } } // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale // set just like a regular MatrixTransform, but the rotation set will be overridden in order to face the camera. - // Must be set as a cull callback. - class BillboardCallback : public osg::NodeCallback + class BillboardCallback : public SceneUtil::NodeCallback { public: BillboardCallback() { } BillboardCallback(const BillboardCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) { } META_Object(NifOsg, BillboardCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::Matrix modelView = *cv->getModelViewMatrix(); // attempt to preserve scale @@ -133,47 +157,24 @@ namespace cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - traverse(node, nv); + traverse(node, cv); cv->popModelViewMatrix(); } }; - void extractTextKeys(const Nif::NiTextKeyExtraData *tk, NifOsg::TextKeyMap &textkeys) + void extractTextKeys(const Nif::NiTextKeyExtraData *tk, SceneUtil::TextKeyMap &textkeys) { for(size_t i = 0;i < tk->list.size();i++) { - const std::string &str = tk->list[i].text; - std::string::size_type pos = 0; - while(pos < str.length()) + std::vector results; + Misc::StringUtils::split(tk->list[i].text, results, "\r\n"); + for (std::string &result : results) { - if(::isspace(str[pos])) - { - pos++; - continue; - } - - std::string::size_type nextpos = std::min(str.find('\r', pos), str.find('\n', pos)); - if(nextpos != std::string::npos) - { - do { - nextpos--; - } while(nextpos > pos && ::isspace(str[nextpos])); - nextpos++; - } - else if(::isspace(*str.rbegin())) - { - std::string::const_iterator last = str.end(); - do { - --last; - } while(last != str.begin() && ::isspace(*last)); - nextpos = std::distance(str.begin(), ++last); - } - std::string result = str.substr(pos, nextpos-pos); + Misc::StringUtils::trim(result); Misc::StringUtils::lowerCaseInPlace(result); - textkeys.emplace(tk->list[i].time, std::move(result)); - - pos = nextpos; + if (!result.empty()) + textkeys.emplace(tk->list[i].time, std::move(result)); } } } @@ -204,7 +205,7 @@ namespace NifOsg return sHiddenNodeMask; } - unsigned int Loader::sIntersectionDisabledNodeMask = ~0; + unsigned int Loader::sIntersectionDisabledNodeMask = ~0u; void Loader::setIntersectionDisabledNodeMask(unsigned int mask) { @@ -228,21 +229,27 @@ namespace NifOsg std::string mFilename; unsigned int mVersion, mUserVersion, mBethVersion; - size_t mFirstRootTextureIndex = -1; + size_t mFirstRootTextureIndex{~0u}; bool mFoundFirstRootTexturingProperty = false; + bool mHasNightDayLabel = false; + bool mHasHerbalismLabel = false; + bool mHasStencilProperty = false; + + const Nif::NiSortAdjustNode* mPushedSorter = nullptr; + const Nif::NiSortAdjustNode* mLastAppliedNoInheritSorter = nullptr; + // This is used to queue emitters that weren't attached to their node yet. std::vector>> mEmitterQueue; - static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target) + static void loadKf(Nif::NIFFilePtr nif, SceneUtil::KeyframeHolder& target) { const Nif::NiSequenceStreamHelper *seq = nullptr; const size_t numRoots = nif->numRoots(); for (size_t i = 0; i < numRoots; ++i) { const Nif::Record *r = nif->getRoot(i); - assert(r != nullptr); - if (r->recType == Nif::RC_NiSequenceStreamHelper) + if (r && r->recType == Nif::RC_NiSequenceStreamHelper) { seq = static_cast(r); break; @@ -284,8 +291,8 @@ namespace NifOsg if (key->data.empty() && key->interpolator.empty()) continue; - osg::ref_ptr callback(handleKeyframeController(key)); - callback->setFunction(std::shared_ptr(new NifOsg::ControllerFunction(key))); + osg::ref_ptr callback = new NifOsg::KeyframeController(key); + setupController(key, callback, /*animflags*/0); if (!target.mKeyframeControllers.emplace(strdata->string, callback).second) Log(Debug::Verbose) << "Controller " << strdata->string << " present more than once in " << nif->getFilename() << ", ignoring later version"; @@ -294,20 +301,33 @@ namespace NifOsg osg::ref_ptr load(Nif::NIFFilePtr nif, Resource::ImageManager* imageManager) { - const Nif::Node* nifNode = nullptr; const size_t numRoots = nif->numRoots(); + std::vector roots; for (size_t i = 0; i < numRoots; ++i) { const Nif::Record* r = nif->getRoot(i); - if ((nifNode = dynamic_cast(r))) - break; + if (!r) + continue; + const Nif::Node* nifNode = dynamic_cast(r); + if (nifNode) + roots.emplace_back(nifNode); } - if (!nifNode) + if (roots.empty()) nif->fail("Found no root nodes"); - osg::ref_ptr textkeys (new TextKeyMapHolder); + osg::ref_ptr textkeys (new SceneUtil::TextKeyMapHolder); - osg::ref_ptr created = handleNode(nifNode, nullptr, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); + osg::ref_ptr created(new osg::Group); + created->setDataVariance(osg::Object::STATIC); + for (const Nif::Node* root : roots) + { + auto node = handleNode(root, nullptr, nullptr, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); + created->addChild(node); + } + if (mHasNightDayLabel) + created->getOrCreateUserDataContainer()->addDescription(Constants::NightDayLabel); + if (mHasHerbalismLabel) + created->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel); // Attach particle emitters to their nodes which should all be loaded by now. handleQueuedParticleEmitters(created, nif); @@ -315,37 +335,51 @@ namespace NifOsg if (nif->getUseSkinning()) { osg::ref_ptr skel = new SceneUtil::Skeleton; - - osg::Group* root = created->asGroup(); - if (root && root->getDataVariance() == osg::Object::STATIC && !root->asTransform()) - { - skel->setStateSet(root->getStateSet()); - skel->setName(root->getName()); - for (unsigned int i=0; igetNumChildren(); ++i) - skel->addChild(root->getChild(i)); - root->removeChildren(0, root->getNumChildren()); - } - else - skel->addChild(created); + skel->setStateSet(created->getStateSet()); + skel->setName(created->getName()); + for (unsigned int i=0; i < created->getNumChildren(); ++i) + skel->addChild(created->getChild(i)); + created->removeChildren(0, created->getNumChildren()); created = skel; } if (!textkeys->mTextKeys.empty()) created->getOrCreateUserDataContainer()->addUserObject(textkeys); + created->setUserValue(Misc::OsgUserValues::sFileHash, nif->getHash()); + return created; } void applyNodeProperties(const Nif::Node *nifNode, osg::Node *applyTo, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { const Nif::PropertyList& props = nifNode->props; + + bool hasStencilProperty = false; + + for (size_t i = 0; i recType == Nif::RC_NiStencilProperty) + { + const Nif::NiStencilProperty* stencilprop = static_cast(props[i].getPtr()); + if (stencilprop->data.enabled != 0) + { + hasStencilProperty = true; + break; + } + } + } + for (size_t i = 0; i parent == nullptr && !mFoundFirstRootTexturingProperty && props[i].getPtr()->recType == Nif::RC_NiTexturingProperty) + if (nifNode->parents.empty() && !mFoundFirstRootTexturingProperty && props[i].getPtr()->recType == Nif::RC_NiTexturingProperty) { mFirstRootTextureIndex = props[i].getPtr()->recIndex; mFoundFirstRootTexturingProperty = true; @@ -353,23 +387,28 @@ namespace NifOsg else if (props[i].getPtr()->recType == Nif::RC_NiTexturingProperty) { if (props[i].getPtr()->recIndex == mFirstRootTextureIndex) - applyTo->setUserValue("overrideFx", 1); + applyTo->setUserValue("overrideFx", 1); } - handleProperty(props[i].getPtr(), applyTo, composite, imageManager, boundTextures, animflags); - } + handleProperty(props[i].getPtr(), applyTo, composite, imageManager, boundTextures, animflags, hasStencilProperty); + } } + + auto geometry = dynamic_cast(nifNode); + // NiGeometry's NiAlphaProperty doesn't get handled here because it's a drawable property + if (geometry && !geometry->shaderprop.empty()) + handleProperty(geometry->shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags, hasStencilProperty); } - void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags) + static void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags) { bool autoPlay = animflags & Nif::NiNode::AnimFlag_AutoPlay; if (autoPlay) - toSetup->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); + toSetup->setSource(std::make_shared()); - toSetup->setFunction(std::shared_ptr(new ControllerFunction(ctrl))); + toSetup->setFunction(std::make_shared(ctrl)); } - osg::ref_ptr handleLodNode(const Nif::NiLODNode* niLodNode) + static osg::ref_ptr handleLodNode(const Nif::NiLODNode* niLodNode) { osg::ref_ptr lod (new osg::LOD); lod->setName(niLodNode->name); @@ -384,7 +423,7 @@ namespace NifOsg return lod; } - osg::ref_ptr handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode) + static osg::ref_ptr handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode) { osg::ref_ptr switchNode (new osg::Switch); switchNode->setName(niSwitchNode->name); @@ -393,6 +432,33 @@ namespace NifOsg return switchNode; } + static osg::ref_ptr prepareSequenceNode(const Nif::Node* nifNode) + { + const Nif::NiFltAnimationNode* niFltAnimationNode = static_cast(nifNode); + osg::ref_ptr sequenceNode (new osg::Sequence); + sequenceNode->setName(niFltAnimationNode->name); + if (niFltAnimationNode->children.length()!=0) + { + if (niFltAnimationNode->swing()) + sequenceNode->setDefaultTime(niFltAnimationNode->mDuration/(niFltAnimationNode->children.length()*2)); + else + sequenceNode->setDefaultTime(niFltAnimationNode->mDuration/niFltAnimationNode->children.length()); + } + return sequenceNode; + } + + static void activateSequenceNode(osg::Group* osgNode, const Nif::Node* nifNode) + { + const Nif::NiFltAnimationNode* niFltAnimationNode = static_cast(nifNode); + osg::Sequence* sequenceNode = static_cast(osgNode); + if (niFltAnimationNode->swing()) + sequenceNode->setInterval(osg::Sequence::SWING, 0,-1); + else + sequenceNode->setInterval(osg::Sequence::LOOP, 0,-1); + sequenceNode->setDuration(1.0f, -1); + sequenceNode->setMode(osg::Sequence::START); + } + osg::ref_ptr handleSourceTexture(const Nif::NiSourceTexture* st, Resource::ImageManager* imageManager) { if (!st) @@ -454,24 +520,15 @@ namespace NifOsg if (image) texture2d->setTextureSize(image->s(), image->t()); texture2d->setName("envMap"); - bool wrapT = textureEffect->clamp & 0x1; - bool wrapS = (textureEffect->clamp >> 1) & 0x1; - texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texture2d->setWrap(osg::Texture::WRAP_S, textureEffect->wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, textureEffect->wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); int texUnit = 3; // FIXME osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(texUnit, texGen, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); } @@ -491,7 +548,7 @@ namespace NifOsg // The Root node can be created as a Group if no transformation is required. // This takes advantage of the fact root nodes can't have additional controllers // loaded from an external .kf file (original engine just throws "can't find node" errors if you try). - if (!nifNode->parent && nifNode->controller.empty() && nifNode->trafo.isIdentity()) + if (nifNode->parents.empty() && nifNode->controller.empty() && nifNode->trafo.isIdentity()) node = new osg::Group; dataVariance = nifNode->isBone ? osg::Object::DYNAMIC : osg::Object::STATIC; @@ -501,20 +558,15 @@ namespace NifOsg if (!node) node = new NifOsg::MatrixTransform(nifNode->trafo); - if (nifNode->recType == Nif::RC_NiCollisionSwitch && !(nifNode->flags & Nif::NiNode::Flag_ActiveCollision)) - { - node->setNodeMask(Loader::getIntersectionDisabledNodeMask()); - // This node must not be combined with another node. - dataVariance = osg::Object::DYNAMIC; - } - node->setDataVariance(dataVariance); return node; } - osg::ref_ptr handleNode(const Nif::Node* nifNode, osg::Group* parentNode, Resource::ImageManager* imageManager, - std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool hasAnimatedParents, TextKeyMap* textKeys, osg::Node* rootNode=nullptr) + osg::ref_ptr handleNode(const Nif::Node* nifNode, const Nif::Parent* parent, osg::Group* parentNode, + Resource::ImageManager* imageManager, std::vector boundTextures, int animflags, + bool skipMeshes, bool hasMarkers, bool hasAnimatedParents, SceneUtil::TextKeyMap* textKeys, + osg::Node* rootNode=nullptr) { if (rootNode != nullptr && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box")) return nullptr; @@ -562,23 +614,46 @@ namespace NifOsg else if(e->recType == Nif::RC_NiStringExtraData) { const Nif::NiStringExtraData *sd = static_cast(e.getPtr()); + + constexpr std::string_view extraDataIdentifer = "omw:data"; + // String markers may contain important information // affecting the entire subtree of this obj - if(sd->string == "MRK" && !Loader::getShowMarkers()) + if (sd->string == "MRK" && !Loader::getShowMarkers()) { // Marker objects. These meshes are only visible in the editor. hasMarkers = true; } - else if(sd->string == "BONE") + else if (sd->string == "BONE") { node->getOrCreateUserDataContainer()->addDescription("CustomBone"); } + else if (sd->string.rfind(extraDataIdentifer, 0) == 0) + { + node->setUserValue(Misc::OsgUserValues::sExtraData, sd->string.substr(extraDataIdentifer.length())); + } } } if (nifNode->recType == Nif::RC_NiBSAnimationNode || nifNode->recType == Nif::RC_NiBSParticleNode) animflags = nifNode->flags; + if (nifNode->recType == Nif::RC_NiSortAdjustNode) + { + auto sortNode = static_cast(nifNode); + + if (sortNode->mSubSorter.empty()) + { + Log(Debug::Warning) << "Empty accumulator found in '" << nifNode->recName << "' node " << nifNode->recIndex; + } + else + { + if (mPushedSorter && !mPushedSorter->mSubSorter.empty() && mPushedSorter->mMode != Nif::NiSortAdjustNode::SortingMode_Inherit) + mLastAppliedNoInheritSorter = mPushedSorter; + mPushedSorter = sortNode; + } + } + // Hide collision shapes, but don't skip the subgraph // We still need to animate the hidden bones so the physics system can access them if (nifNode->recType == Nif::RC_RootCollisionNode) @@ -589,12 +664,13 @@ namespace NifOsg // We can skip creating meshes for hidden nodes if they don't have a VisController that // might make them visible later - if (nifNode->flags & Nif::NiNode::Flag_Hidden) + if (nifNode->isHidden()) { bool hasVisController = false; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if ((hasVisController |= (ctrl->recType == Nif::RC_NiVisController))) + hasVisController |= (ctrl->recType == Nif::RC_NiVisController); + if (hasVisController) break; } @@ -604,6 +680,9 @@ namespace NifOsg node->setNodeMask(Loader::getHiddenNodeMask()); } + if (nifNode->recType == Nif::RC_NiCollisionSwitch && !nifNode->collisionActive()) + node->setNodeMask(Loader::getIntersectionDisabledNodeMask()); + osg::ref_ptr composite = new SceneUtil::CompositeStateSetUpdater; applyNodeProperties(nifNode, node, composite, imageManager, boundTextures, animflags); @@ -622,17 +701,17 @@ namespace NifOsg Nif::NiSkinInstancePtr skin = static_cast(nifNode)->skin; if (skin.empty()) - handleGeometry(nifNode, node, composite, boundTextures, animflags); + handleGeometry(nifNode, parent, node, composite, boundTextures, animflags); else - handleSkinnedGeometry(nifNode, node, composite, boundTextures, animflags); + handleSkinnedGeometry(nifNode, parent, node, composite, boundTextures, animflags); if (!nifNode->controller.empty()) handleMeshControllers(nifNode, node, composite, boundTextures, animflags); } } - if(nifNode->recType == Nif::RC_NiAutoNormalParticles || nifNode->recType == Nif::RC_NiRotatingParticles) - handleParticleSystem(nifNode, node, composite, animflags, rootNode); + if (nifNode->recType == Nif::RC_NiParticles) + handleParticleSystem(nifNode, parent, node, composite, animflags); if (composite->getNumControllers() > 0) { @@ -662,10 +741,10 @@ namespace NifOsg const Nif::NiSwitchNode* niSwitchNode = static_cast(nifNode); osg::ref_ptr switchNode = handleSwitchNode(niSwitchNode); node->addChild(switchNode); - if (niSwitchNode->name == Constants::NightDayLabel && !SceneUtil::hasUserDescription(rootNode, Constants::NightDayLabel)) - rootNode->getOrCreateUserDataContainer()->addDescription(Constants::NightDayLabel); - else if (niSwitchNode->name == Constants::HerbalismLabel && !SceneUtil::hasUserDescription(rootNode, Constants::HerbalismLabel)) - rootNode->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel); + if (niSwitchNode->name == Constants::NightDayLabel) + mHasNightDayLabel = true; + else if (niSwitchNode->name == Constants::HerbalismLabel) + mHasHerbalismLabel = true; currentNode = switchNode; } @@ -676,6 +755,12 @@ namespace NifOsg node->addChild(lodNode); currentNode = lodNode; } + else if (nifNode->recType == Nif::RC_NiFltAnimationNode) + { + osg::ref_ptr sequenceNode = prepareSequenceNode(nifNode); + node->addChild(sequenceNode); + currentNode = sequenceNode; + } const Nif::NiNode *ninode = dynamic_cast(nifNode); if(ninode) @@ -688,13 +773,17 @@ namespace NifOsg } const Nif::NodeList &children = ninode->children; + const Nif::Parent currentParent {*ninode, parent}; for(size_t i = 0;i < children.length();++i) { if(!children[i].empty()) - handleNode(children[i].getPtr(), currentNode, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, hasAnimatedParents, textKeys, rootNode); + handleNode(children[i].getPtr(), ¤tParent, currentNode, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, hasAnimatedParents, textKeys, rootNode); } } + if (nifNode->recType == Nif::RC_NiFltAnimationNode) + activateSequenceNode(currentNode,nifNode); + return node; } @@ -702,7 +791,7 @@ namespace NifOsg { for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiUVController) { @@ -725,36 +814,18 @@ namespace NifOsg } } - static osg::ref_ptr handleKeyframeController(const Nif::NiKeyframeController* keyctrl) - { - osg::ref_ptr ctrl; - if (!keyctrl->interpolator.empty()) - { - const Nif::NiTransformInterpolator* interp = keyctrl->interpolator.getPtr(); - if (!interp->data.empty()) - ctrl = new NifOsg::KeyframeController(interp); - else - ctrl = new NifOsg::KeyframeController(interp->defaultScale, interp->defaultPos, interp->defaultRot); - } - else if (!keyctrl->data.empty()) - { - ctrl = new NifOsg::KeyframeController(keyctrl->data.getPtr()); - } - return ctrl; - } - void handleNodeControllers(const Nif::Node* nifNode, osg::Node* node, int animflags, bool& isAnimated) { for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiKeyframeController) { const Nif::NiKeyframeController *key = static_cast(ctrl.getPtr()); if (key->data.empty() && key->interpolator.empty()) continue; - osg::ref_ptr callback(handleKeyframeController(key)); + osg::ref_ptr callback = new KeyframeController(key); setupController(key, callback, animflags); node->addUpdateCallback(callback); isAnimated = true; @@ -808,7 +879,7 @@ namespace NifOsg { for (Nif::ControllerPtr ctrl = materialProperty->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiAlphaController) { @@ -828,8 +899,10 @@ namespace NifOsg const Nif::NiMaterialColorController* matctrl = static_cast(ctrl.getPtr()); if (matctrl->data.empty() && matctrl->interpolator.empty()) continue; - osg::ref_ptr osgctrl; auto targetColor = static_cast(matctrl->targetColor); + if (mVersion <= Nif::NIFFile::NIFVersion::VER_MW && targetColor == MaterialColorController::TargetColor::Specular) + continue; + osg::ref_ptr osgctrl; if (!matctrl->interpolator.empty()) osgctrl = new MaterialColorController(matctrl->interpolator.getPtr(), targetColor, baseMaterial); else // if (!matctrl->data.empty()) @@ -846,7 +919,7 @@ namespace NifOsg { for (Nif::ControllerPtr ctrl = texProperty->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiFlipController) { @@ -937,15 +1010,17 @@ namespace NifOsg } // Load the initial state of the particle system, i.e. the initial particles and their positions, velocity and colors. - void handleParticleInitialState(const Nif::Node* nifNode, osgParticle::ParticleSystem* partsys, const Nif::NiParticleSystemController* partctrl) + void handleParticleInitialState(const Nif::Node* nifNode, ParticleSystem* partsys, const Nif::NiParticleSystemController* partctrl) { - const Nif::NiAutoNormalParticlesData *particledata = nullptr; - if(nifNode->recType == Nif::RC_NiAutoNormalParticles) - particledata = static_cast(nifNode)->data.getPtr(); - else if(nifNode->recType == Nif::RC_NiRotatingParticles) - particledata = static_cast(nifNode)->data.getPtr(); - else + auto particleNode = static_cast(nifNode); + if (particleNode->data.empty() || particleNode->data->recType != Nif::RC_NiParticlesData) + { + partsys->setQuota(partctrl->numParticles); return; + } + + auto particledata = static_cast(particleNode->data.getPtr()); + partsys->setQuota(particledata->numParticles); osg::BoundingBox box; @@ -958,6 +1033,9 @@ namespace NifOsg if (particle.lifespan <= 0) continue; + if (particle.vertex >= particledata->vertices.size()) + continue; + ParticleAgeSetter particletemplate(std::max(0.f, particle.lifetime)); osgParticle::Particle* created = partsys->createParticle(&particletemplate); @@ -966,16 +1044,16 @@ namespace NifOsg // Note this position and velocity is not correct for a particle system with absolute reference frame, // which can not be done in this loader since we are not attached to the scene yet. Will be fixed up post-load in the SceneManager. created->setVelocity(particle.velocity); - const osg::Vec3f& position = particledata->vertices.at(particle.vertex); + const osg::Vec3f& position = particledata->vertices[particle.vertex]; created->setPosition(position); osg::Vec4f partcolor (1.f,1.f,1.f,1.f); - if (particle.vertex < int(particledata->colors.size())) - partcolor = particledata->colors.at(particle.vertex); + if (particle.vertex < particledata->colors.size()) + partcolor = particledata->colors[particle.vertex]; float size = partctrl->size; - if (particle.vertex < int(particledata->sizes.size())) - size *= particledata->sizes.at(particle.vertex); + if (particle.vertex < particledata->sizes.size()) + size *= particledata->sizes[particle.vertex]; created->setSizeRange(osgParticle::rangef(size, size)); box.expandBy(osg::BoundingSphere(position, size)); @@ -990,7 +1068,7 @@ namespace NifOsg osg::ref_ptr handleParticleEmitter(const Nif::NiParticleSystemController* partctrl) { std::vector targets; - if (partctrl->recType == Nif::RC_NiBSPArrayController) + if (partctrl->recType == Nif::RC_NiBSPArrayController && !partctrl->emitAtVertex()) { getAllNiNodes(partctrl->emitter.getPtr(), targets); } @@ -998,7 +1076,7 @@ namespace NifOsg osg::ref_ptr emitter = new Emitter(targets); osgParticle::ConstantRateCounter* counter = new osgParticle::ConstantRateCounter; - if (partctrl->emitFlags & Nif::NiParticleSystemController::NoAutoAdjust) + if (partctrl->noAutoAdjust()) counter->setNumberOfParticlesPerSecondToCreate(partctrl->emitRate); else if (partctrl->lifetime == 0 && partctrl->lifetimeRandom == 0) counter->setNumberOfParticlesPerSecondToCreate(0); @@ -1013,17 +1091,25 @@ namespace NifOsg partctrl->verticalDir, partctrl->verticalAngle, partctrl->lifetime, partctrl->lifetimeRandom); emitter->setShooter(shooter); + emitter->setFlags(partctrl->flags); - osgParticle::BoxPlacer* placer = new osgParticle::BoxPlacer; - placer->setXRange(-partctrl->offsetRandom.x() / 2.f, partctrl->offsetRandom.x() / 2.f); - placer->setYRange(-partctrl->offsetRandom.y() / 2.f, partctrl->offsetRandom.y() / 2.f); - placer->setZRange(-partctrl->offsetRandom.z() / 2.f, partctrl->offsetRandom.z() / 2.f); + if (partctrl->recType == Nif::RC_NiBSPArrayController && partctrl->emitAtVertex()) + { + emitter->setGeometryEmitterTarget(partctrl->emitter->recIndex); + } + else + { + osgParticle::BoxPlacer* placer = new osgParticle::BoxPlacer; + placer->setXRange(-partctrl->offsetRandom.x() / 2.f, partctrl->offsetRandom.x() / 2.f); + placer->setYRange(-partctrl->offsetRandom.y() / 2.f, partctrl->offsetRandom.y() / 2.f); + placer->setZRange(-partctrl->offsetRandom.z() / 2.f, partctrl->offsetRandom.z() / 2.f); + emitter->setPlacer(placer); + } - emitter->setPlacer(placer); return emitter; } - void handleQueuedParticleEmitters(osg::Node* rootNode, Nif::NIFFilePtr nif) + void handleQueuedParticleEmitters(osg::Group* rootNode, Nif::NIFFilePtr nif) { for (const auto& emitterPair : mEmitterQueue) { @@ -1040,11 +1126,15 @@ namespace NifOsg // Emitter attached to the emitter node. Note one side effect of the emitter using the CullVisitor is that hiding its node // actually causes the emitter to stop firing. Convenient, because MW behaves this way too! emitterNode->addChild(emitterPair.second); + + DisableOptimizer disableOptimizer; + emitterNode->accept(disableOptimizer); } mEmitterQueue.clear(); } - void handleParticleSystem(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags, osg::Node* rootNode) + void handleParticleSystem(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Group *parentNode, + SceneUtil::CompositeStateSetUpdater* composite, int animflags) { osg::ref_ptr partsys (new ParticleSystem); partsys->setSortMode(osgParticle::ParticleSystem::SORT_BACK_TO_FRONT); @@ -1052,7 +1142,7 @@ namespace NifOsg const Nif::NiParticleSystemController* partctrl = nullptr; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if(ctrl->recType == Nif::RC_NiParticleSystemController || ctrl->recType == Nif::RC_NiBSPArrayController) partctrl = static_cast(ctrl.getPtr()); @@ -1077,14 +1167,10 @@ namespace NifOsg handleParticleInitialState(nifNode, partsys, partctrl); - partsys->setQuota(partctrl->numParticles); - partsys->getDefaultParticleTemplate().setSizeRange(osgParticle::rangef(partctrl->size, partctrl->size)); partsys->getDefaultParticleTemplate().setColorRange(osgParticle::rangev4(osg::Vec4f(1.f,1.f,1.f,1.f), osg::Vec4f(1.f,1.f,1.f,1.f))); partsys->getDefaultParticleTemplate().setAlphaRange(osgParticle::rangef(1.f, 1.f)); - partsys->setFreezeOnCull(true); - if (!partctrl->emitter.empty()) { osg::ref_ptr emitter = handleParticleEmitter(partctrl); @@ -1117,7 +1203,7 @@ namespace NifOsg handleParticlePrograms(partctrl->affectors, partctrl->colliders, parentNode, partsys.get(), rf); std::vector drawableProps; - collectDrawableProperties(nifNode, drawableProps); + collectDrawableProperties(nifNode, parent, drawableProps); applyDrawableProperties(parentNode, drawableProps, composite, true, animflags); // particle system updater (after the emitters and affectors in the scene graph) @@ -1137,8 +1223,6 @@ namespace NifOsg trans->addChild(toAttach); parentNode->addChild(trans); } - // create partsys stateset in order to pass in ShaderVisitor like all other Drawables - partsys->getOrCreateStateSet(); } void handleNiGeometryData(osg::Geometry *geometry, const Nif::NiGeometryData* data, const std::vector& boundTextures, const std::string& name) @@ -1155,87 +1239,93 @@ namespace NifOsg const auto& uvlist = data->uvlist; int textureStage = 0; - for (const unsigned int uvSet : boundTextures) + for (std::vector::const_iterator it = boundTextures.begin(); it != boundTextures.end(); ++it, ++textureStage) { + unsigned int uvSet = *it; if (uvSet >= uvlist.size()) { Log(Debug::Verbose) << "Out of bounds UV set " << uvSet << " on shape \"" << name << "\" in " << mFilename; - if (!uvlist.empty()) - geometry->setTexCoordArray(textureStage, new osg::Vec2Array(uvlist[0].size(), uvlist[0].data()), osg::Array::BIND_PER_VERTEX); - continue; + if (uvlist.empty()) + continue; + uvSet = 0; } geometry->setTexCoordArray(textureStage, new osg::Vec2Array(uvlist[uvSet].size(), uvlist[uvSet].data()), osg::Array::BIND_PER_VERTEX); - textureStage++; } } - void handleNiGeometry(const Nif::Node *nifNode, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) + void handleNiGeometry(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Geometry *geometry, + osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, + const std::vector& boundTextures, int animflags) { - const Nif::NiGeometryData* niGeometryData = nullptr; - if (nifNode->recType == Nif::RC_NiTriShape) + const Nif::NiGeometry* niGeometry = static_cast(nifNode); + if (niGeometry->data.empty()) + return; + const Nif::NiGeometryData* niGeometryData = niGeometry->data.getPtr(); + + if (niGeometry->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_BSLODTriShape) { - const Nif::NiTriShape* triShape = static_cast(nifNode); - if (!triShape->data.empty()) - { - const Nif::NiTriShapeData* data = triShape->data.getPtr(); - niGeometryData = static_cast(data); - if (!data->triangles.empty()) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, data->triangles.size(), - (unsigned short*)data->triangles.data())); - } + if (niGeometryData->recType != Nif::RC_NiTriShapeData) + return; + auto triangles = static_cast(niGeometryData)->triangles; + if (triangles.empty()) + return; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, triangles.size(), + (unsigned short*)triangles.data())); } - else if (nifNode->recType == Nif::RC_NiTriStrips) + else if (niGeometry->recType == Nif::RC_NiTriStrips) { - const Nif::NiTriStrips* triStrips = static_cast(nifNode); - if (!triStrips->data.empty()) + if (niGeometryData->recType != Nif::RC_NiTriStripsData) + return; + auto data = static_cast(niGeometryData); + bool hasGeometry = false; + for (const auto& strip : data->strips) { - const Nif::NiTriStripsData* data = triStrips->data.getPtr(); - niGeometryData = static_cast(data); - if (!data->strips.empty()) - { - for (const auto& strip : data->strips) - { - if (strip.size() >= 3) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), - (unsigned short*)strip.data())); - } - } + if (strip.size() < 3) + continue; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), + reinterpret_cast(strip.data()))); + hasGeometry = true; } + if (!hasGeometry) + return; } - else if (nifNode->recType == Nif::RC_NiLines) + else if (niGeometry->recType == Nif::RC_NiLines) { - const Nif::NiLines* lines = static_cast(nifNode); - if (!lines->data.empty()) - { - const Nif::NiLinesData* data = lines->data.getPtr(); - niGeometryData = static_cast(data); - const auto& line = data->lines; - if (!line.empty()) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), (unsigned short*)line.data())); - } + if (niGeometryData->recType != Nif::RC_NiLinesData) + return; + auto data = static_cast(niGeometryData); + const auto& line = data->lines; + if (line.empty()) + return; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), + reinterpret_cast(line.data()))); } - if (niGeometryData) - handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->name); + handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->name); // osg::Material properties are handled here for two reasons: // - if there are no vertex colors, we need to disable colorMode. // - there are 3 "overlapping" nif properties that all affect the osg::Material, handling them // above the actual renderable would be tedious. std::vector drawableProps; - collectDrawableProperties(nifNode, drawableProps); - applyDrawableProperties(parentNode, drawableProps, composite, niGeometryData && !niGeometryData->colors.empty(), animflags); + collectDrawableProperties(nifNode, parent, drawableProps); + applyDrawableProperties(parentNode, drawableProps, composite, !niGeometryData->colors.empty(), animflags); } - void handleGeometry(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) + void handleGeometry(const Nif::Node* nifNode, const Nif::Parent* parent, osg::Group* parentNode, + SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, + int animflags) { assert(isTypeGeometry(nifNode->recType)); - osg::ref_ptr drawable; osg::ref_ptr geom (new osg::Geometry); - handleNiGeometry(nifNode, geom, parentNode, composite, boundTextures, animflags); + handleNiGeometry(nifNode, parent, geom, parentNode, composite, boundTextures, animflags); + // If the record had no valid geometry data in it, early-out + if (geom->empty()) + return; + osg::ref_ptr drawable; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + if (!ctrl->isActive()) continue; if(ctrl->recType == Nif::RC_NiGeomMorpherController) { @@ -1264,19 +1354,22 @@ namespace NifOsg const std::vector& morphs = morpher->data.getPtr()->mMorphs; if (morphs.empty()) return morphGeom; - // Note we are not interested in morph 0, which just contains the original vertices - for (unsigned int i = 1; i < morphs.size(); ++i) + if (morphs[0].mVertices.size() != static_cast(sourceGeometry->getVertexArray())->size()) + return morphGeom; + for (unsigned int i = 0; i < morphs.size(); ++i) morphGeom->addMorphTarget(new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f); return morphGeom; } - void handleSkinnedGeometry(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, - const std::vector& boundTextures, int animflags) + void handleSkinnedGeometry(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Group *parentNode, + SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { assert(isTypeGeometry(nifNode->recType)); osg::ref_ptr geometry (new osg::Geometry); - handleNiGeometry(nifNode, geometry, parentNode, composite, boundTextures, animflags); + handleNiGeometry(nifNode, parent, geometry, parentNode, composite, boundTextures, animflags); + if (geometry->empty()) + return; osg::ref_ptr rig(new SceneUtil::RigGeometry); rig->setSourceGeometry(geometry); rig->setName(nifNode->name); @@ -1357,7 +1450,7 @@ namespace NifOsg case 4: return osg::Stencil::GREATER; case 5: return osg::Stencil::NOTEQUAL; case 6: return osg::Stencil::GEQUAL; - case 7: return osg::Stencil::NEVER; // NifSkope says this is GL_ALWAYS, but in MW it's GL_NEVER + case 7: return osg::Stencil::ALWAYS; default: Log(Debug::Info) << "Unexpected stencil function: " << func << " in " << mFilename; return osg::Stencil::NEVER; @@ -1476,6 +1569,20 @@ namespace NifOsg return image; } + osg::ref_ptr createEmissiveTexEnv() + { + osg::ref_ptr texEnv(new osg::TexEnvCombine); + // Sum the previous colour and the emissive colour. + texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); + return texEnv; + } + void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { if (!boundTextures.empty()) @@ -1500,14 +1607,8 @@ namespace NifOsg case Nif::NiTexturingProperty::BumpTexture: case Nif::NiTexturingProperty::DetailTexture: case Nif::NiTexturingProperty::DecalTexture: - break; case Nif::NiTexturingProperty::GlossTexture: - { - // Not used by the vanilla engine. MCP (Morrowind Code Patch) adds an option to use Gloss maps: - // "- Gloss map fix. Morrowind removed gloss map entries from model files after loading them. This stops Morrowind from removing them." - // Log(Debug::Info) << "NiTexturingProperty::GlossTexture in " << mFilename << " not currently used."; - continue; - } + break; default: { Log(Debug::Info) << "Unhandled texture stage " << i << " on shape \"" << nodeName << "\" in " << mFilename; @@ -1539,11 +1640,8 @@ namespace NifOsg else texture2d = new osg::Texture2D; - bool wrapT = tex.clamp & 0x1; - bool wrapS = (tex.clamp >> 1) & 0x1; - - texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_S, tex.wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, tex.wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); uvSet = tex.uvSet; } @@ -1562,39 +1660,36 @@ namespace NifOsg if (i == Nif::NiTexturingProperty::GlowTexture) { - osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DarkTexture) { osg::TexEnv* texEnv = new osg::TexEnv; + // Modulate both the colour and the alpha with the dark map. texEnv->setMode(osg::TexEnv::MODULATE); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DetailTexture) { osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setScale_RGB(2.f); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setOperand1_Alpha(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::TEXTURE); + // Modulate previous colour... texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); + // with the detail map's colour, texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); + // and a twist: + texEnv->setScale_RGB(2.f); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::BumpTexture) { + // Bump maps offset the environment map. // Set this texture to Off by default since we can't render it with the fixed-function pipeline stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); osg::Matrix2 bumpMapMatrix(texprop->bumpMapMatrix.x(), texprop->bumpMapMatrix.y(), @@ -1602,20 +1697,33 @@ namespace NifOsg stateset->addUniform(new osg::Uniform("bumpMapMatrix", bumpMapMatrix)); stateset->addUniform(new osg::Uniform("envMapLumaBias", texprop->envMapLumaBias)); } + else if (i == Nif::NiTexturingProperty::GlossTexture) + { + // A gloss map is an environment map mask. + // Gloss maps are only implemented in the object shaders as well. + stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); + } else if (i == Nif::NiTexturingProperty::DecalTexture) { - osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + // This is only an inaccurate imitation of the original implementation, + // see https://github.com/niftools/nifskope/issues/184 + + osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; + // Interpolate to the decal texture's colour... + texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); + // ...from the previous colour... + texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); + // using the decal texture's alpha as the factor. + texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); + stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } switch (i) @@ -1638,6 +1746,9 @@ namespace NifOsg case Nif::NiTexturingProperty::DecalTexture: texture2d->setName("decalMap"); break; + case Nif::NiTexturingProperty::GlossTexture: + texture2d->setName("glossMap"); + break; default: break; } @@ -1648,8 +1759,114 @@ namespace NifOsg handleTextureControllers(texprop, composite, imageManager, stateset, animflags); } + void handleTextureSet(const Nif::BSShaderTextureSet* textureSet, unsigned int clamp, const std::string& nodeName, osg::StateSet* stateset, Resource::ImageManager* imageManager, std::vector& boundTextures) + { + if (!boundTextures.empty()) + { + for (unsigned int i = 0; i < boundTextures.size(); ++i) + stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); + boundTextures.clear(); + } + + const unsigned int uvSet = 0; + + for (size_t i = 0; i < textureSet->textures.size(); ++i) + { + if (textureSet->textures[i].empty()) + continue; + switch(i) + { + case Nif::BSShaderTextureSet::TextureType_Base: + case Nif::BSShaderTextureSet::TextureType_Normal: + case Nif::BSShaderTextureSet::TextureType_Glow: + break; + default: + { + Log(Debug::Info) << "Unhandled texture stage " << i << " on shape \"" << nodeName << "\" in " << mFilename; + continue; + } + } + std::string filename = Misc::ResourceHelpers::correctTexturePath(textureSet->textures[i], imageManager->getVFS()); + osg::ref_ptr image = imageManager->getImage(filename); + osg::ref_ptr texture2d = new osg::Texture2D(image); + if (image) + texture2d->setTextureSize(image->s(), image->t()); + bool wrapT = clamp & 0x1; + bool wrapS = (clamp >> 1) & 0x1; + texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + unsigned int texUnit = boundTextures.size(); + stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); + // BSShaderTextureSet presence means there's no need for FFP support for the affected node + switch (i) + { + case Nif::BSShaderTextureSet::TextureType_Base: + texture2d->setName("diffuseMap"); + break; + case Nif::BSShaderTextureSet::TextureType_Normal: + texture2d->setName("normalMap"); + break; + case Nif::BSShaderTextureSet::TextureType_Glow: + texture2d->setName("emissiveMap"); + break; + } + boundTextures.emplace_back(uvSet); + } + } + + std::string_view getBSShaderPrefix(unsigned int type) const + { + switch (static_cast(type)) + { + case Nif::BSShaderType::ShaderType_Default: return "nv_default"; + case Nif::BSShaderType::ShaderType_NoLighting: return "nv_nolighting"; + case Nif::BSShaderType::ShaderType_TallGrass: + case Nif::BSShaderType::ShaderType_Sky: + case Nif::BSShaderType::ShaderType_Skin: + case Nif::BSShaderType::ShaderType_Water: + case Nif::BSShaderType::ShaderType_Lighting30: + case Nif::BSShaderType::ShaderType_Tile: + Log(Debug::Warning) << "Unhandled BSShaderType " << type << " in " << mFilename; + return std::string_view(); + } + Log(Debug::Warning) << "Unknown BSShaderType " << type << " in " << mFilename; + return std::string_view(); + } + + std::string_view getBSLightingShaderPrefix(unsigned int type) const + { + switch (static_cast(type)) + { + case Nif::BSLightingShaderType::ShaderType_Default: return "nv_default"; + case Nif::BSLightingShaderType::ShaderType_EnvMap: + case Nif::BSLightingShaderType::ShaderType_Glow: + case Nif::BSLightingShaderType::ShaderType_Parallax: + case Nif::BSLightingShaderType::ShaderType_FaceTint: + case Nif::BSLightingShaderType::ShaderType_SkinTint: + case Nif::BSLightingShaderType::ShaderType_HairTint: + case Nif::BSLightingShaderType::ShaderType_ParallaxOcc: + case Nif::BSLightingShaderType::ShaderType_MultitexLand: + case Nif::BSLightingShaderType::ShaderType_LODLand: + case Nif::BSLightingShaderType::ShaderType_Snow: + case Nif::BSLightingShaderType::ShaderType_MultiLayerParallax: + case Nif::BSLightingShaderType::ShaderType_TreeAnim: + case Nif::BSLightingShaderType::ShaderType_LODObjects: + case Nif::BSLightingShaderType::ShaderType_SparkleSnow: + case Nif::BSLightingShaderType::ShaderType_LODObjectsHD: + case Nif::BSLightingShaderType::ShaderType_EyeEnvmap: + case Nif::BSLightingShaderType::ShaderType_Cloud: + case Nif::BSLightingShaderType::ShaderType_LODNoise: + case Nif::BSLightingShaderType::ShaderType_MultitexLandLODBlend: + case Nif::BSLightingShaderType::ShaderType_Dismemberment: + Log(Debug::Warning) << "Unhandled BSLightingShaderType " << type << " in " << mFilename; + return std::string_view(); + } + Log(Debug::Warning) << "Unknown BSLightingShaderType " << type << " in " << mFilename; + return std::string_view(); + } + void handleProperty(const Nif::Property *property, - osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) + osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags, bool hasStencilProperty) { switch (property->recType) { @@ -1677,6 +1894,7 @@ namespace NifOsg if (stencilprop->data.enabled != 0) { + mHasStencilProperty = true; osg::ref_ptr stencil = new osg::Stencil; stencil->setFunction(getStencilFunction(stencilprop->data.compareFunc), stencilprop->data.stencilRef, stencilprop->data.stencilMask); stencil->setStencilFailOperation(getStencilOperation(stencilprop->data.failAction)); @@ -1692,8 +1910,7 @@ namespace NifOsg { const Nif::NiWireframeProperty* wireprop = static_cast(property); osg::ref_ptr mode = new osg::PolygonMode; - mode->setMode(osg::PolygonMode::FRONT_AND_BACK, wireprop->flags == 0 ? osg::PolygonMode::FILL - : osg::PolygonMode::LINE); + mode->setMode(osg::PolygonMode::FRONT_AND_BACK, wireprop->isEnabled() ? osg::PolygonMode::LINE : osg::PolygonMode::FILL); mode = shareAttribute(mode); node->getOrCreateStateSet()->setAttributeAndModes(mode, osg::StateAttribute::ON); break; @@ -1701,11 +1918,15 @@ namespace NifOsg case Nif::RC_NiZBufferProperty: { const Nif::NiZBufferProperty* zprop = static_cast(property); - // VER_MW doesn't support a DepthFunction according to NifSkope + osg::StateSet* stateset = node->getOrCreateStateSet(); + stateset->setMode(GL_DEPTH_TEST, zprop->depthTest() ? osg::StateAttribute::ON : osg::StateAttribute::OFF); osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask((zprop->flags>>1)&1); + depth->setWriteMask(zprop->depthWrite()); + // Morrowind ignores depth test function, unless a NiStencilProperty is present, in which case it uses a fixed depth function of GL_ALWAYS. + if (hasStencilProperty) + depth->setFunction(osg::Depth::ALWAYS); depth = shareAttribute(depth); - node->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); break; } // OSG groups the material properties that NIFs have separate, so we have to parse them all again when one changed @@ -1728,6 +1949,73 @@ namespace NifOsg handleTextureProperty(texprop, node->getName(), stateset, composite, imageManager, boundTextures, animflags); break; } + case Nif::RC_BSShaderPPLightingProperty: + { + auto texprop = static_cast(property); + bool shaderRequired = true; + node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->type))); + node->setUserValue("shaderRequired", shaderRequired); + osg::StateSet* stateset = node->getOrCreateStateSet(); + if (!texprop->textureSet.empty()) + { + auto textureSet = texprop->textureSet.getPtr(); + handleTextureSet(textureSet, texprop->clamp, node->getName(), stateset, imageManager, boundTextures); + } + handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + break; + } + case Nif::RC_BSShaderNoLightingProperty: + { + auto texprop = static_cast(property); + bool shaderRequired = true; + node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->type))); + node->setUserValue("shaderRequired", shaderRequired); + osg::StateSet* stateset = node->getOrCreateStateSet(); + if (!texprop->filename.empty()) + { + if (!boundTextures.empty()) + { + for (unsigned int i = 0; i < boundTextures.size(); ++i) + stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); + boundTextures.clear(); + } + std::string filename = Misc::ResourceHelpers::correctTexturePath(texprop->filename, imageManager->getVFS()); + osg::ref_ptr image = imageManager->getImage(filename); + osg::ref_ptr texture2d = new osg::Texture2D(image); + texture2d->setName("diffuseMap"); + if (image) + texture2d->setTextureSize(image->s(), image->t()); + texture2d->setWrap(osg::Texture::WRAP_S, texprop->wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, texprop->wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + const unsigned int texUnit = 0; + const unsigned int uvSet = 0; + stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); + boundTextures.push_back(uvSet); + } + if (mBethVersion >= 27) + { + stateset->addUniform(new osg::Uniform("useFalloff", true)); + stateset->addUniform(new osg::Uniform("falloffParams", texprop->falloffParams)); + } + else + { + stateset->addUniform(new osg::Uniform("useFalloff", false)); + } + handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + break; + } + case Nif::RC_BSLightingShaderProperty: + { + auto texprop = static_cast(property); + bool shaderRequired = true; + node->setUserValue("shaderPrefix", std::string(getBSLightingShaderPrefix(texprop->type))); + node->setUserValue("shaderRequired", shaderRequired); + osg::StateSet* stateset = node->getOrCreateStateSet(); + if (!texprop->mTextureSet.empty()) + handleTextureSet(texprop->mTextureSet.getPtr(), texprop->mClamp, node->getName(), stateset, imageManager, boundTextures); + handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + break; + } // unused by mw case Nif::RC_NiShadeProperty: case Nif::RC_NiDitherProperty: @@ -1766,8 +2054,6 @@ namespace NifOsg void applyDrawableProperties(osg::Node* node, const std::vector& properties, SceneUtil::CompositeStateSetUpdater* composite, bool hasVertexColors, int animflags) { - osg::StateSet* stateset = node->getOrCreateStateSet(); - // Specular lighting is enabled by default, but there's a quirk... bool specEnabled = true; osg::ref_ptr mat (new osg::Material); @@ -1778,8 +2064,17 @@ namespace NifOsg mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); bool hasMatCtrl = false; + bool hasSortAlpha = false; + osg::StateSet* blendFuncStateSet = nullptr; + + auto setBin_Transparent = [] (osg::StateSet* ss) { ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); }; + auto setBin_BackToFront = [] (osg::StateSet* ss) { ss->setRenderBinDetails(0, "SORT_BACK_TO_FRONT"); }; + auto setBin_Traversal = [] (osg::StateSet* ss) { ss->setRenderBinDetails(2, "TraversalOrderBin"); }; + auto setBin_Inherit = [] (osg::StateSet* ss) { ss->setRenderBinToInherit(); }; int lightmode = 1; + float emissiveMult = 1.f; + float specStrength = 1.f; for (const Nif::Property* property : properties) { @@ -1788,8 +2083,9 @@ namespace NifOsg case Nif::RC_NiSpecularProperty: { // Specular property can turn specular lighting off. + // FIXME: NiMaterialColorController doesn't care about this. auto specprop = static_cast(property); - specEnabled = specprop->flags & 1; + specEnabled = specprop->isEnabled(); break; } case Nif::RC_NiMaterialProperty: @@ -1799,6 +2095,7 @@ namespace NifOsg mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.diffuse, matprop->data.alpha)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.ambient, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.emissive, 1.f)); + emissiveMult = matprop->data.emissiveMult; mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.specular, 1.f)); mat->setShininess(osg::Material::FRONT_AND_BACK, matprop->data.glossiness); @@ -1836,39 +2133,65 @@ namespace NifOsg case Nif::RC_NiAlphaProperty: { const Nif::NiAlphaProperty* alphaprop = static_cast(property); - if (alphaprop->flags&1) + if (alphaprop->useAlphaBlending()) { - osg::ref_ptr blendFunc (new osg::BlendFunc(getBlendMode((alphaprop->flags>>1)&0xf), - getBlendMode((alphaprop->flags>>5)&0xf))); + osg::ref_ptr blendFunc (new osg::BlendFunc(getBlendMode(alphaprop->sourceBlendMode()), + getBlendMode(alphaprop->destinationBlendMode()))); + // on AMD hardware, alpha still seems to be stored with an RGBA framebuffer with OpenGL. + // This might be mandated by the OpenGL 2.1 specification section 2.14.9, or might be a bug. + // Either way, D3D8.1 doesn't do that, so adapt the destination factor. + if (blendFunc->getDestination() == GL_DST_ALPHA) + blendFunc->setDestination(GL_ONE); blendFunc = shareAttribute(blendFunc); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - bool noSort = (alphaprop->flags>>13)&1; - if (!noSort) - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + if (!alphaprop->noSorter()) + { + hasSortAlpha = true; + if (!mPushedSorter) + setBin_Transparent(node->getStateSet()); + } else - stateset->setRenderBinToInherit(); + { + if (!mPushedSorter) + setBin_Inherit(node->getStateSet()); + } } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); stateset->removeMode(GL_BLEND); - stateset->setRenderBinToInherit(); + blendFuncStateSet = stateset; + if (!mPushedSorter) + blendFuncStateSet->setRenderBinToInherit(); } - if((alphaprop->flags>>9)&1) + if (alphaprop->useAlphaTesting()) { - osg::ref_ptr alphaFunc (new osg::AlphaFunc(getTestMode((alphaprop->flags>>10)&0x7), alphaprop->data.threshold/255.f)); + osg::ref_ptr alphaFunc (new osg::AlphaFunc(getTestMode(alphaprop->alphaTestMode()), alphaprop->data.threshold/255.f)); alphaFunc = shareAttribute(alphaFunc); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); stateset->removeMode(GL_ALPHA_TEST); } break; } + case Nif::RC_BSLightingShaderProperty: + { + auto shaderprop = static_cast(property); + mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderprop->mAlpha); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mEmissive, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mSpecular, 1.f)); + mat->setShininess(osg::Material::FRONT_AND_BACK, shaderprop->mGlossiness); + emissiveMult = shaderprop->mEmissiveMult; + specStrength = shaderprop->mSpecStrength; + break; + } + default: + break; } } @@ -1905,7 +2228,10 @@ namespace NifOsg mat->setColorMode(osg::Material::OFF); } - if (!hasMatCtrl && mat->getColorMode() == osg::Material::OFF + if (!mPushedSorter && !hasSortAlpha && mHasStencilProperty) + setBin_Traversal(node->getOrCreateStateSet()); + + if (!mPushedSorter && !hasMatCtrl && mat->getColorMode() == osg::Material::OFF && mat->getEmission(osg::Material::FRONT_AND_BACK) == osg::Vec4f(0,0,0,1) && mat->getDiffuse(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1) && mat->getAmbient(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1) @@ -1918,7 +2244,57 @@ namespace NifOsg mat = shareAttribute(mat); + osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + if (emissiveMult != 1.f) + stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); + if (specStrength != 1.f) + stateset->addUniform(new osg::Uniform("specStrength", specStrength)); + + if (!mPushedSorter) + return; + + auto assignBin = [&] (int mode, int type) { + if (mode == Nif::NiSortAdjustNode::SortingMode_Off) + { + setBin_Traversal(stateset); + return; + } + + if (type == Nif::RC_NiAlphaAccumulator) + { + if (hasSortAlpha) + setBin_BackToFront(stateset); + else + setBin_Traversal(stateset); + } + else if (type == Nif::RC_NiClusterAccumulator) + setBin_BackToFront(stateset); + else + Log(Debug::Error) << "Unrecognized NiAccumulator in " << mFilename; + }; + + switch (mPushedSorter->mMode) + { + case Nif::NiSortAdjustNode::SortingMode_Inherit: + { + if (mLastAppliedNoInheritSorter) + assignBin(mLastAppliedNoInheritSorter->mMode, mLastAppliedNoInheritSorter->mSubSorter->recType); + else + assignBin(mPushedSorter->mMode, Nif::RC_NiAlphaAccumulator); + break; + } + case Nif::NiSortAdjustNode::SortingMode_Off: + { + setBin_Traversal(stateset); + break; + } + case Nif::NiSortAdjustNode::SortingMode_Subsort: + { + assignBin(mPushedSorter->mMode, mPushedSorter->mSubSorter->recType); + break; + } + } } }; @@ -1929,7 +2305,7 @@ namespace NifOsg return impl.load(file, imageManager); } - void Loader::loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target) + void Loader::loadKf(Nif::NIFFilePtr kf, SceneUtil::KeyframeHolder& target) { LoaderImpl impl(kf->getFilename(), kf->getVersion(), kf->getUserVersion(), kf->getBethVersion()); impl.loadKf(kf, target); diff --git a/components/nifosg/nifloader.hpp b/components/nifosg/nifloader.hpp index 49a78ad5f6..8ee6b41674 100644 --- a/components/nifosg/nifloader.hpp +++ b/components/nifosg/nifloader.hpp @@ -2,12 +2,13 @@ #define OPENMW_COMPONENTS_NIFOSG_LOADER #include +#include +#include #include #include #include "controller.hpp" -#include "textkeymap.hpp" namespace osg { @@ -21,39 +22,6 @@ namespace Resource namespace NifOsg { - struct TextKeyMapHolder : public osg::Object - { - public: - TextKeyMapHolder() {} - TextKeyMapHolder(const TextKeyMapHolder& copy, const osg::CopyOp& copyop) - : osg::Object(copy, copyop) - , mTextKeys(copy.mTextKeys) - {} - - TextKeyMap mTextKeys; - - META_Object(NifOsg, TextKeyMapHolder) - - }; - - class KeyframeHolder : public osg::Object - { - public: - KeyframeHolder() {} - KeyframeHolder(const KeyframeHolder& copy, const osg::CopyOp& copyop) - : mTextKeys(copy.mTextKeys) - , mKeyframeControllers(copy.mKeyframeControllers) - { - } - - TextKeyMap mTextKeys; - - META_Object(OpenMW, KeyframeHolder) - - typedef std::map > KeyframeControllerMap; - KeyframeControllerMap mKeyframeControllers; - }; - /// The main class responsible for loading NIF files into an OSG-Scenegraph. /// @par This scene graph is self-contained and can be cloned using osg::clone if desired. Particle emitters /// and programs hold a pointer to their ParticleSystem, which would need to be manually updated when cloning. @@ -64,7 +32,7 @@ namespace NifOsg static osg::ref_ptr load(Nif::NIFFilePtr file, Resource::ImageManager* imageManager); /// Load keyframe controllers from the given kf file. - static void loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target); + static void loadKf(Nif::NIFFilePtr kf, SceneUtil::KeyframeHolder& target); /// Set whether or not nodes marked as "MRK" should be shown. /// These should be hidden ingame, but visible in the editor. diff --git a/components/nifosg/particle.cpp b/components/nifosg/particle.cpp index 2cb0ffc629..9706a1f91b 100644 --- a/components/nifosg/particle.cpp +++ b/components/nifosg/particle.cpp @@ -1,6 +1,7 @@ #include "particle.hpp" #include +#include #include #include @@ -11,6 +12,91 @@ #include #include #include +#include +#include +#include + +namespace +{ + class FindFirstGeometry : public osg::NodeVisitor + { + public: + FindFirstGeometry() + : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) + , mGeometry(nullptr) + { + } + + void apply(osg::Node& node) override + { + if (mGeometry) + return; + + traverse(node); + } + + void apply(osg::Drawable& drawable) override + { + if (auto morph = dynamic_cast(&drawable)) + { + mGeometry = morph->getSourceGeometry(); + return; + } + else if (auto rig = dynamic_cast(&drawable)) + { + mGeometry = rig->getSourceGeometry(); + return; + } + + traverse(drawable); + } + + void apply(osg::Geometry& geometry) override + { + mGeometry = &geometry; + } + + osg::Geometry* mGeometry; + }; + + class LocalToWorldAccumulator : public osg::NodeVisitor + { + public: + LocalToWorldAccumulator(osg::Matrix& matrix) : osg::NodeVisitor(), mMatrix(matrix) {} + + virtual void apply(osg::Transform& transform) + { + if (&transform != mLastAppliedTransform) + { + mLastAppliedTransform = &transform; + mLastMatrix = mMatrix; + } + transform.computeLocalToWorldMatrix(mMatrix, this); + } + + void accumulate(const osg::NodePath& path) + { + if (path.empty()) + return; + + size_t i = path.size(); + + for (auto rit = path.rbegin(); rit != path.rend(); rit++, --i) + { + const osg::Camera* camera = (*rit)->asCamera(); + if (camera && (camera->getReferenceFrame() != osg::Transform::RELATIVE_RF || camera->getParents().empty())) + break; + } + + for(; i < path.size(); ++i) + path[i]->accept(*this); + } + + osg::Matrix& mMatrix; + std::optional mLastMatrix; + osg::Transform* mLastAppliedTransform = nullptr; + }; +} namespace NifOsg { @@ -68,20 +154,16 @@ void ParticleSystem::drawImplementation(osg::RenderInfo& renderInfo) const osgParticle::ParticleSystem::drawImplementation(renderInfo); } -void InverseWorldMatrix::operator()(osg::Node *node, osg::NodeVisitor *nv) +void InverseWorldMatrix::operator()(osg::MatrixTransform *node, osg::NodeVisitor *nv) { - if (nv && nv->getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) - { - osg::NodePath path = nv->getNodePath(); - path.pop_back(); + osg::NodePath path = nv->getNodePath(); + path.pop_back(); - osg::MatrixTransform* trans = static_cast(node); + osg::Matrix mat = osg::computeLocalToWorld( path ); + mat.orthoNormalize(mat); // don't undo the scale + mat.invert(mat); + node->setMatrix(mat); - osg::Matrix mat = osg::computeLocalToWorld( path ); - mat.orthoNormalize(mat); // don't undo the scale - mat.invert(mat); - trans->setMatrix(mat); - } traverse(node,nv); } @@ -268,6 +350,8 @@ void GravityAffector::operate(osgParticle::Particle *particle, double dt) Emitter::Emitter() : osgParticle::Emitter() + , mFlags(0) + , mGeometryEmitterTarget(std::nullopt) { } @@ -278,29 +362,19 @@ Emitter::Emitter(const Emitter ©, const osg::CopyOp ©op) , mShooter(copy.mShooter) // need a deep copy because the remainder is stored in the object , mCounter(static_cast(copy.mCounter->clone(osg::CopyOp::DEEP_COPY_ALL))) + , mFlags(copy.mFlags) + , mGeometryEmitterTarget(copy.mGeometryEmitterTarget) + , mCachedGeometryEmitter(copy.mCachedGeometryEmitter) { } Emitter::Emitter(const std::vector &targets) : mTargets(targets) + , mFlags(0) + , mGeometryEmitterTarget(std::nullopt) { } -void Emitter::setShooter(osgParticle::Shooter *shooter) -{ - mShooter = shooter; -} - -void Emitter::setPlacer(osgParticle::Placer *placer) -{ - mPlacer = placer; -} - -void Emitter::setCounter(osgParticle::Counter *counter) -{ - mCounter = counter; -} - void Emitter::emitParticles(double dt) { int n = mCounter->numParticlesToCreate(dt); @@ -320,34 +394,92 @@ void Emitter::emitParticles(double dt) const osg::Matrix& ltw = getLocalToWorldMatrix(); osg::Matrix emitterToPs = ltw * worldToPs; - if (!mTargets.empty()) + osg::ref_ptr geometryVertices = nullptr; + + const bool useGeometryEmitter = mFlags & Nif::NiParticleSystemController::BSPArrayController_AtVertex; + + if (useGeometryEmitter || !mTargets.empty()) { - int randomIndex = Misc::Rng::rollClosedProbability() * (mTargets.size() - 1); - int randomRecIndex = mTargets[randomIndex]; + int recIndex; + + if (useGeometryEmitter) + { + if (!mGeometryEmitterTarget.has_value()) + return; + + recIndex = mGeometryEmitterTarget.value(); + } + else + { + int randomIndex = Misc::Rng::rollClosedProbability() * (mTargets.size() - 1); + recIndex = mTargets[randomIndex]; + } // we could use a map here for faster lookup - FindGroupByRecIndex visitor(randomRecIndex); + FindGroupByRecIndex visitor(recIndex); getParent(0)->accept(visitor); if (!visitor.mFound) { - Log(Debug::Info) << "Can't find emitter node" << randomRecIndex; + Log(Debug::Info) << "Can't find emitter node" << recIndex; return; } + if (useGeometryEmitter) + { + if (!mCachedGeometryEmitter.lock(geometryVertices)) + { + FindFirstGeometry geometryVisitor; + visitor.mFound->accept(geometryVisitor); + + if (geometryVisitor.mGeometry) + { + if (auto* vertices = dynamic_cast(geometryVisitor.mGeometry->getVertexArray())) + { + mCachedGeometryEmitter = osg::observer_ptr(vertices); + geometryVertices = vertices; + } + } + } + } + osg::NodePath path = visitor.mFoundPath; path.erase(path.begin()); - emitterToPs = osg::computeLocalToWorld(path) * emitterToPs; + if (!useGeometryEmitter && (mFlags & Nif::NiParticleSystemController::BSPArrayController_AtNode) && path.size()) + { + osg::Matrix current; + + LocalToWorldAccumulator accum(current); + accum.accumulate(path); + + osg::Matrix parent = accum.mLastMatrix.value_or(current); + + auto p1 = parent.getTrans(); + auto p2 = current.getTrans(); + current.setTrans((p2 - p1) * Misc::Rng::rollClosedProbability() + p1); + + emitterToPs = current * emitterToPs; + } + else + { + emitterToPs = osg::computeLocalToWorld(path) * emitterToPs; + } } emitterToPs.orthoNormalize(emitterToPs); + if (useGeometryEmitter && (!geometryVertices.valid() || geometryVertices->empty())) + return; + for (int i=0; icreateParticle(0); + osgParticle::Particle* P = getParticleSystem()->createParticle(nullptr); if (P) { - mPlacer->place(P); + if (useGeometryEmitter) + P->setPosition((*geometryVertices)[Misc::Rng::rollDice(geometryVertices->getNumElements())]); + else if (mPlacer) + mPlacer->place(P); mShooter->shoot(P); @@ -396,18 +528,24 @@ void FindGroupByRecIndex::applyNode(osg::Node &searchNode) PlanarCollider::PlanarCollider(const Nif::NiPlanarCollider *collider) : mBounceFactor(collider->mBounceFactor) + , mExtents(collider->mExtents) + , mPosition(collider->mPosition) + , mXVector(collider->mXVector) + , mYVector(collider->mYVector) , mPlane(-collider->mPlaneNormal, collider->mPlaneDistance) { } -PlanarCollider::PlanarCollider() - : mBounceFactor(0.f) -{ -} - PlanarCollider::PlanarCollider(const PlanarCollider ©, const osg::CopyOp ©op) : osgParticle::Operator(copy, copyop) , mBounceFactor(copy.mBounceFactor) + , mExtents(copy.mExtents) + , mPosition(copy.mPosition) + , mPositionInParticleSpace(copy.mPositionInParticleSpace) + , mXVector(copy.mXVector) + , mXVectorInParticleSpace(copy.mXVectorInParticleSpace) + , mYVector(copy.mYVector) + , mYVectorInParticleSpace(copy.mYVectorInParticleSpace) , mPlane(copy.mPlane) , mPlaneInParticleSpace(copy.mPlaneInParticleSpace) { @@ -415,25 +553,44 @@ PlanarCollider::PlanarCollider(const PlanarCollider ©, const osg::CopyOp &co void PlanarCollider::beginOperate(osgParticle::Program *program) { + mPositionInParticleSpace = mPosition; mPlaneInParticleSpace = mPlane; + mXVectorInParticleSpace = mXVector; + mYVectorInParticleSpace = mYVector; if (program->getReferenceFrame() == osgParticle::ParticleProcessor::ABSOLUTE_RF) + { + mPositionInParticleSpace = program->transformLocalToWorld(mPosition); mPlaneInParticleSpace.transform(program->getLocalToWorldMatrix()); + mXVectorInParticleSpace = program->rotateLocalToWorld(mXVector); + mYVectorInParticleSpace = program->rotateLocalToWorld(mYVector); + } } void PlanarCollider::operate(osgParticle::Particle *particle, double dt) { - float dotproduct = particle->getVelocity() * mPlaneInParticleSpace.getNormal(); + // Does the particle in question move towards the collider? + float velDotProduct = particle->getVelocity() * mPlaneInParticleSpace.getNormal(); + if (velDotProduct <= 0) + return; - if (dotproduct > 0) - { - osg::BoundingSphere bs(particle->getPosition(), 0.f); - if (mPlaneInParticleSpace.intersect(bs) == 1) - { - osg::Vec3 reflectedVelocity = particle->getVelocity() - mPlaneInParticleSpace.getNormal() * (2 * dotproduct); - reflectedVelocity *= mBounceFactor; - particle->setVelocity(reflectedVelocity); - } - } + // Does it intersect the collider's plane? + osg::BoundingSphere bs(particle->getPosition(), 0.f); + if (mPlaneInParticleSpace.intersect(bs) != 1) + return; + + // Is it inside the collider's bounds? + osg::Vec3f relativePos = particle->getPosition() - mPositionInParticleSpace; + float xDotProduct = relativePos * mXVectorInParticleSpace; + float yDotProduct = relativePos * mYVectorInParticleSpace; + if (-mExtents.x() * 0.5f > xDotProduct || mExtents.x() * 0.5f < xDotProduct) + return; + if (-mExtents.y() * 0.5f > yDotProduct || mExtents.y() * 0.5f < yDotProduct) + return; + + // Deflect the particle + osg::Vec3 reflectedVelocity = particle->getVelocity() - mPlaneInParticleSpace.getNormal() * (2 * velDotProduct); + reflectedVelocity *= mBounceFactor; + particle->setVelocity(reflectedVelocity); } SphericalCollider::SphericalCollider(const Nif::NiSphericalCollider* collider) diff --git a/components/nifosg/particle.hpp b/components/nifosg/particle.hpp index dd89f4501d..34e6a4308d 100644 --- a/components/nifosg/particle.hpp +++ b/components/nifosg/particle.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_COMPONENTS_NIFOSG_PARTICLE_H #define OPENMW_COMPONENTS_NIFOSG_PARTICLE_H +#include + #include #include #include @@ -8,16 +10,16 @@ #include #include -#include +#include #include "controller.hpp" // ValueInterpolator namespace Nif { - class NiGravity; - class NiPlanarCollider; - class NiSphericalCollider; - class NiColorData; + struct NiGravity; + struct NiPlanarCollider; + struct NiSphericalCollider; + struct NiColorData; } namespace NifOsg @@ -57,20 +59,20 @@ namespace NifOsg // Node callback used to set the inverse of the parent's world matrix on the MatrixTransform // that the callback is attached to. Used for certain particle systems, // so that the particles do not move with the node they are attached to. - class InverseWorldMatrix : public osg::NodeCallback + class InverseWorldMatrix : public SceneUtil::NodeCallback { public: InverseWorldMatrix() { } - InverseWorldMatrix(const InverseWorldMatrix& copy, const osg::CopyOp& op) - : osg::Object(), osg::NodeCallback() + InverseWorldMatrix(const InverseWorldMatrix& copy, const osg::CopyOp& copyop) + : osg::Object(copy, copyop), SceneUtil::NodeCallback(copy, copyop) { } META_Object(NifOsg, InverseWorldMatrix) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); }; class ParticleShooter : public osgParticle::Shooter @@ -102,7 +104,7 @@ namespace NifOsg { public: PlanarCollider(const Nif::NiPlanarCollider* collider); - PlanarCollider(); + PlanarCollider() = default; PlanarCollider(const PlanarCollider& copy, const osg::CopyOp& copyop); META_Object(NifOsg, PlanarCollider) @@ -111,9 +113,12 @@ namespace NifOsg void operate(osgParticle::Particle* particle, double dt) override; private: - float mBounceFactor; - osg::Plane mPlane; - osg::Plane mPlaneInParticleSpace; + float mBounceFactor{0.f}; + osg::Vec2f mExtents; + osg::Vec3f mPosition, mPositionInParticleSpace; + osg::Vec3f mXVector, mXVectorInParticleSpace; + osg::Vec3f mYVector, mYVectorInParticleSpace; + osg::Plane mPlane, mPlaneInParticleSpace; }; class SphericalCollider : public osgParticle::Operator @@ -233,9 +238,11 @@ namespace NifOsg void emitParticles(double dt) override; - void setShooter(osgParticle::Shooter* shooter); - void setPlacer(osgParticle::Placer* placer); - void setCounter(osgParticle::Counter* counter); + void setShooter(osgParticle::Shooter* shooter) { mShooter = shooter; } + void setPlacer(osgParticle::Placer* placer) { mPlacer = placer; } + void setCounter(osgParticle::Counter* counter) { mCounter = counter;} + void setGeometryEmitterTarget(std::optional recIndex) { mGeometryEmitterTarget = recIndex; } + void setFlags(int flags) { mFlags = flags; } private: // NIF Record indices @@ -244,6 +251,11 @@ namespace NifOsg osg::ref_ptr mPlacer; osg::ref_ptr mShooter; osg::ref_ptr mCounter; + + int mFlags; + + std::optional mGeometryEmitterTarget; + osg::observer_ptr mCachedGeometryEmitter; }; } diff --git a/components/platform/file.hpp b/components/platform/file.hpp new file mode 100644 index 0000000000..8faf9c67cb --- /dev/null +++ b/components/platform/file.hpp @@ -0,0 +1,64 @@ +#ifndef OPENMW_COMPONENTS_PLATFORM_FILE_HPP +#define OPENMW_COMPONENTS_PLATFORM_FILE_HPP + +#include +#include + +namespace Platform::File { + + enum class Handle : intptr_t + { + Invalid = -1 + }; + + enum class SeekType + { + Begin, + Current, + End + }; + + Handle open(const char* filename); + + void close(Handle handle); + + size_t size(Handle handle); + + void seek(Handle handle, size_t Position, SeekType type = SeekType::Begin); + size_t tell(Handle handle); + + size_t read(Handle handle, void* data, size_t size); + + class ScopedHandle + { + Handle mHandle{ Handle::Invalid }; + + public: + ScopedHandle() noexcept = default; + ScopedHandle(ScopedHandle& other) = delete; + ScopedHandle(Handle handle) noexcept : mHandle(handle) {} + ScopedHandle(ScopedHandle&& other) noexcept + : mHandle(other.mHandle) + { + other.mHandle = Handle::Invalid; + } + ScopedHandle& operator=(const ScopedHandle& other) = delete; + ScopedHandle& operator=(ScopedHandle&& other) noexcept + { + if (mHandle != Handle::Invalid) + close(mHandle); + mHandle = other.mHandle; + other.mHandle = Handle::Invalid; + return *this; + } + ~ScopedHandle() + { + if(mHandle != Handle::Invalid) + close(mHandle); + } + + operator Handle() const { return mHandle; } + }; +} + +#endif // OPENMW_COMPONENTS_PLATFORM_FILE_HPP diff --git a/components/platform/file.posix.cpp b/components/platform/file.posix.cpp new file mode 100644 index 0000000000..f79562dcfd --- /dev/null +++ b/components/platform/file.posix.cpp @@ -0,0 +1,102 @@ +#include "file.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Platform::File { + + static auto getNativeHandle(Handle handle) + { + assert(handle != Handle::Invalid); + + return static_cast(handle); + } + + static int getNativeSeekType(SeekType seek) + { + if (seek == SeekType::Begin) + return SEEK_SET; + if (seek == SeekType::Current) + return SEEK_CUR; + if (seek == SeekType::End) + return SEEK_END; + return -1; + } + + Handle open(const char* filename) + { +#ifdef O_BINARY + static const int openFlags = O_RDONLY | O_BINARY; +#else + static const int openFlags = O_RDONLY; +#endif + + auto handle = ::open(filename, openFlags, 0); + if (handle == -1) + { + throw std::runtime_error(std::string("Failed to open '") + filename + "' for reading: " + strerror(errno)); + } + return static_cast(handle); + } + + void close(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + ::close(nativeHandle); + } + + void seek(Handle handle, size_t position, SeekType type /*= SeekType::Begin*/) + { + const auto nativeHandle = getNativeHandle(handle); + const auto nativeSeekType = getNativeSeekType(type); + + if (::lseek(nativeHandle, position, nativeSeekType) == -1) + { + throw std::runtime_error("An lseek() call failed: " + std::string(strerror(errno))); + } + } + + size_t size(Handle handle) + { + const auto oldPos = tell(handle); + + seek(handle, 0, SeekType::End); + const auto fileSize = tell(handle); + seek(handle, oldPos, SeekType::Begin); + + + return static_cast(fileSize); + } + + size_t tell(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + size_t position = ::lseek(nativeHandle, 0, SEEK_CUR); + if (position == size_t(-1)) + { + throw std::runtime_error("An lseek() call failed: " + std::string(strerror(errno))); + } + return position; + } + + size_t read(Handle handle, void* data, size_t size) + { + auto nativeHandle = getNativeHandle(handle); + + int amount = ::read(nativeHandle, data, size); + if (amount == -1) + { + throw std::runtime_error("An attempt to read " + std::to_string(size) + " bytes failed: " + strerror(errno)); + } + return amount; + } + +} diff --git a/components/platform/file.stdio.cpp b/components/platform/file.stdio.cpp new file mode 100644 index 0000000000..558fea1154 --- /dev/null +++ b/components/platform/file.stdio.cpp @@ -0,0 +1,91 @@ +#include "file.hpp" + +#include +#include +#include +#include +#include + +namespace Platform::File { + + static auto getNativeHandle(Handle handle) + { + assert(handle != Handle::Invalid); + + return reinterpret_cast(static_cast(handle)); + } + + static int getNativeSeekType(SeekType seek) + { + if (seek == SeekType::Begin) + return SEEK_SET; + if (seek == SeekType::Current) + return SEEK_CUR; + if (seek == SeekType::End) + return SEEK_END; + return -1; + } + + Handle open(const char* filename) + { + FILE* handle = fopen(filename, "rb"); + if (handle == nullptr) + { + throw std::runtime_error(std::string("Failed to open '") + filename + "' for reading: " + strerror(errno)); + } + return static_cast(reinterpret_cast(handle)); + } + + void close(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + fclose(nativeHandle); + } + + void seek(Handle handle, size_t position, SeekType type /*= SeekType::Begin*/) + { + const auto nativeHandle = getNativeHandle(handle); + const auto nativeSeekType = getNativeSeekType(type); + if (fseek(nativeHandle, position, nativeSeekType) != 0) + { + throw std::runtime_error(std::string("An fseek() call failed: ") + strerror(errno)); + } + } + + size_t size(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + const auto oldPos = tell(handle); + seek(handle, 0, SeekType::End); + const auto fileSize = tell(handle); + seek(handle, oldPos, SeekType::Begin); + + return static_cast(fileSize); + } + + size_t tell(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + long position = ftell(nativeHandle); + if (position == -1) + { + throw std::runtime_error(std::string("An ftell() call failed: ") + strerror(errno)); + } + return static_cast(position); + } + + size_t read(Handle handle, void* data, size_t size) + { + auto nativeHandle = getNativeHandle(handle); + + int amount = fread(data, 1, size, nativeHandle); + if (amount == 0 && ferror(nativeHandle)) + { + throw std::runtime_error(std::string("An attempt to read ") + std::to_string(size) + " bytes failed: " + strerror(errno)); + } + return static_cast(amount); + } + +} diff --git a/components/platform/file.win32.cpp b/components/platform/file.win32.cpp new file mode 100644 index 0000000000..a2ba86a4ef --- /dev/null +++ b/components/platform/file.win32.cpp @@ -0,0 +1,97 @@ +#include "file.hpp" + +#include +#include +#include +#include +#include + +namespace Platform::File { + + static auto getNativeHandle(Handle handle) + { + assert(handle != Handle::Invalid); + + return reinterpret_cast(static_cast(handle)); + } + + static int getNativeSeekType(SeekType seek) + { + if (seek == SeekType::Begin) + return FILE_BEGIN; + if (seek == SeekType::Current) + return FILE_CURRENT; + if (seek == SeekType::End) + return FILE_END; + return -1; + } + + Handle open(const char* filename) + { + std::wstring wname = boost::locale::conv::utf_to_utf(filename); + HANDLE handle = CreateFileW(wname.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (handle == INVALID_HANDLE_VALUE) + { + throw std::runtime_error(std::string("Failed to open '") + filename + "' for reading: " + std::to_string(GetLastError())); + } + return static_cast(reinterpret_cast(handle)); + } + + void close(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + CloseHandle(nativeHandle); + } + + void seek(Handle handle, size_t position, SeekType type /*= SeekType::Begin*/) + { + const auto nativeHandle = getNativeHandle(handle); + const auto nativeSeekType = getNativeSeekType(type); + + if (SetFilePointer(nativeHandle, static_cast(position), nullptr, nativeSeekType) == INVALID_SET_FILE_POINTER) + { + if (auto errCode = GetLastError(); errCode != ERROR_SUCCESS) + { + throw std::runtime_error(std::string("An fseek() call failed: ") + std::to_string(errCode)); + } + } + } + + size_t size(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + BY_HANDLE_FILE_INFORMATION info; + + if (!GetFileInformationByHandle(nativeHandle, &info)) + throw std::runtime_error("A query operation on a file failed."); + + if (info.nFileSizeHigh != 0) + throw std::runtime_error("Files greater that 4GB are not supported."); + + return info.nFileSizeLow; + } + + size_t tell(Handle handle) + { + auto nativeHandle = getNativeHandle(handle); + + DWORD value = SetFilePointer(nativeHandle, 0, nullptr, SEEK_CUR); + if (value == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) + throw std::runtime_error("A query operation on a file failed."); + + return value; + } + + size_t read(Handle handle, void* data, size_t size) + { + auto nativeHandle = getNativeHandle(handle); + + DWORD bytesRead{}; + + if (!ReadFile(nativeHandle, data, static_cast(size), &bytesRead, nullptr)) + throw std::runtime_error(std::string("A read operation on a file failed: ") + std::to_string(GetLastError())); + + return bytesRead; + } +} diff --git a/components/platform/platform.cpp b/components/platform/platform.cpp new file mode 100644 index 0000000000..9044ba3792 --- /dev/null +++ b/components/platform/platform.cpp @@ -0,0 +1,23 @@ +#include "platform.hpp" + +#include + +namespace Platform { + + static void increaseFileHandleLimit() + { +#ifdef WIN32 + // Increase limit for open files at the stream I/O level, see + // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-170#remarks + _setmaxstdio(8192); +#else + // No-op on any other platform. +#endif + } + + void init() + { + increaseFileHandleLimit(); + } + +} diff --git a/components/platform/platform.hpp b/components/platform/platform.hpp new file mode 100644 index 0000000000..d8f2058217 --- /dev/null +++ b/components/platform/platform.hpp @@ -0,0 +1,10 @@ +#ifndef OPENMW_COMPONENTS_PLATFORM_PLATFORM_HPP +#define OPENMW_COMPONENTS_PLATFORM_PLATFORM_HPP + +namespace Platform { + + void init(); + +} + +#endif // OPENMW_COMPONENTS_PLATFORM_PLATFORM_HPP diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 78cf70038b..54462acb9c 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -4,10 +4,13 @@ #include #include #include -#include + +#if defined(Q_OS_MAC) #include +#endif -Process::ProcessInvoker::ProcessInvoker() +Process::ProcessInvoker::ProcessInvoker(QObject* parent) + : QObject(parent) { mProcess = new QProcess(this); @@ -56,6 +59,7 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis // mProcess = new QProcess(this); mName = name; mArguments = arguments; + mIgnoreErrors = false; QString path(name); #ifdef Q_OS_WIN @@ -151,6 +155,8 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis void Process::ProcessInvoker::processError(QProcess::ProcessError error) { + if (mIgnoreErrors) + return; QMessageBox msgBox; msgBox.setWindowTitle(tr("Error running executable")); msgBox.setIcon(QMessageBox::Critical); @@ -166,6 +172,8 @@ void Process::ProcessInvoker::processError(QProcess::ProcessError error) void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0 || exitStatus == QProcess::CrashExit) { + if (mIgnoreErrors) + return; QString error(mProcess->readAllStandardError()); error.append(tr("\nArguments:\n")); error.append(mArguments.join(" ")); @@ -181,3 +189,9 @@ void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus msgBox.exec(); } } + +void Process::ProcessInvoker::killProcess() +{ + mIgnoreErrors = true; + mProcess->kill(); +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp index 8fff6658ca..f4b402cb12 100644 --- a/components/process/processinvoker.hpp +++ b/components/process/processinvoker.hpp @@ -13,7 +13,7 @@ namespace Process public: - ProcessInvoker(); + ProcessInvoker(QObject* parent = nullptr); ~ProcessInvoker(); // void setProcessName(const QString &name); @@ -27,12 +27,16 @@ namespace Process inline bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } bool startProcess(const QString &name, const QStringList &arguments, bool detached = false); + void killProcess(); + private: QProcess *mProcess; QString mName; QStringList mArguments; + bool mIgnoreErrors = false; + private slots: void processError(QProcess::ProcessError error); void processFinished(int exitCode, QProcess::ExitStatus exitStatus); diff --git a/components/resource/animation.cpp b/components/resource/animation.cpp new file mode 100644 index 0000000000..d2d7d08d55 --- /dev/null +++ b/components/resource/animation.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include + +namespace Resource +{ + Animation::Animation(const Animation& anim, const osg::CopyOp& copyop): osg::Object(anim, copyop), + mDuration(0.0f), + mStartTime(0.0f) + { + const osgAnimation::ChannelList& channels = anim.getChannels(); + for (const auto& channel: channels) + addChannel(channel.get()->clone()); + } + + void Animation::addChannel(osg::ref_ptr pChannel) + { + mChannels.push_back(pChannel); + } + + std::vector>& Animation::getChannels() + { + return mChannels; + } + + const std::vector>& Animation::getChannels() const + { + return mChannels; + } + + bool Animation::update (double time) + { + for (const auto& channel: mChannels) + { + channel->update(time, 1.0f, 0); + } + return true; + } +} diff --git a/components/resource/animation.hpp b/components/resource/animation.hpp new file mode 100644 index 0000000000..885394747e --- /dev/null +++ b/components/resource/animation.hpp @@ -0,0 +1,39 @@ +#ifndef OPENMW_COMPONENTS_RESOURCE_ANIMATION_HPP +#define OPENMW_COMPONENTS_RESOURCE_ANIMATION_HPP + +#include + +#include +#include +#include + +namespace Resource +{ + /// Stripped down class of osgAnimation::Animation, only needed for OSG's plugin formats like dae + class Animation : public osg::Object + { + public: + META_Object(Resource, Animation) + + Animation() : + mDuration(0.0), mStartTime(0) {} + + Animation(const Animation&, const osg::CopyOp&); + ~Animation() {} + + void addChannel (osg::ref_ptr pChannel); + + std::vector>& getChannels(); + + const std::vector>& getChannels() const; + + bool update (double time); + + protected: + double mDuration; + double mStartTime; + std::vector> mChannels; + }; +} + +#endif diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 7dd0964e8b..2d7fd87aed 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -6,87 +6,77 @@ #include #include #include +#include namespace Resource { - -BulletShape::BulletShape() - : mCollisionShape(nullptr) - , mAvoidCollisionShape(nullptr) +namespace { + CollisionShapePtr duplicateCollisionShape(const btCollisionShape *shape) + { + if (shape == nullptr) + return nullptr; -} + if (shape->isCompound()) + { + const btCompoundShape *comp = static_cast(shape); + std::unique_ptr newShape(new btCompoundShape); -BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) - : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape)) - , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape)) - , mCollisionBoxHalfExtents(copy.mCollisionBoxHalfExtents) - , mCollisionBoxTranslate(copy.mCollisionBoxTranslate) - , mAnimatedShapes(copy.mAnimatedShapes) -{ -} + for (int i = 0, n = comp->getNumChildShapes(); i < n; ++i) + { + auto child = duplicateCollisionShape(comp->getChildShape(i)); + const btTransform& trans = comp->getChildTransform(i); + newShape->addChildShape(trans, child.release()); + } -BulletShape::~BulletShape() -{ - deleteShape(mAvoidCollisionShape); - deleteShape(mCollisionShape); -} + return newShape; + } -void BulletShape::deleteShape(btCollisionShape* shape) -{ - if(shape!=nullptr) - { - if(shape->isCompound()) + if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) { - btCompoundShape* ms = static_cast(shape); - int a = ms->getNumChildShapes(); - for(int i=0; i getChildShape(i)); + const btBvhTriangleMeshShape* trishape = static_cast(shape); + return CollisionShapePtr(new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f))); } - delete shape; - } -} -btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *shape) const -{ - if(shape->isCompound()) - { - const btCompoundShape *comp = static_cast(shape); - btCompoundShape *newShape = new btCompoundShape; - - int numShapes = comp->getNumChildShapes(); - for(int i = 0;i < numShapes;++i) + if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) { - btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i)); - btTransform trans = comp->getChildTransform(i); - newShape->addChildShape(trans, child); + const btBoxShape* boxshape = static_cast(shape); + return CollisionShapePtr(new btBoxShape(*boxshape)); } - return newShape; - } + if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) + return CollisionShapePtr(new btHeightfieldTerrainShape(static_cast(*shape))); - if(const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) - { - btScaledBvhTriangleMeshShape* newShape = new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); - return newShape; + throw std::logic_error(std::string("Unhandled Bullet shape duplication: ") + shape->getName()); } - if (const btBoxShape* boxshape = dynamic_cast(shape)) + void deleteShape(btCollisionShape* shape) { - return new btBoxShape(*boxshape); - } + if (shape->isCompound()) + { + btCompoundShape* compound = static_cast(shape); + for (int i = 0, n = compound->getNumChildShapes(); i < n; i++) + if (btCollisionShape* child = compound->getChildShape(i)) + deleteShape(child); + } - throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName()); + delete shape; + } } -btCollisionShape *BulletShape::getCollisionShape() const +void DeleteCollisionShape::operator()(btCollisionShape* shape) const { - return mCollisionShape; + deleteShape(shape); } -btCollisionShape *BulletShape::getAvoidCollisionShape() const +BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) + : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape.get())) + , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape.get())) + , mCollisionBox(copy.mCollisionBox) + , mAnimatedShapes(copy.mAnimatedShapes) + , mFileName(copy.mFileName) + , mFileHash(copy.mFileHash) { - return mAvoidCollisionShape; } void BulletShape::setLocalScaling(const btVector3& scale) @@ -96,26 +86,19 @@ void BulletShape::setLocalScaling(const btVector3& scale) mAvoidCollisionShape->setLocalScaling(scale); } -osg::ref_ptr BulletShape::makeInstance() const +osg::ref_ptr makeInstance(osg::ref_ptr source) { - osg::ref_ptr instance (new BulletShapeInstance(this)); - return instance; + return {new BulletShapeInstance(std::move(source))}; } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) - : BulletShape() - , mSource(source) + : mSource(std::move(source)) { - mCollisionBoxHalfExtents = source->mCollisionBoxHalfExtents; - mCollisionBoxTranslate = source->mCollisionBoxTranslate; - - mAnimatedShapes = source->mAnimatedShapes; - - if (source->mCollisionShape) - mCollisionShape = duplicateCollisionShape(source->mCollisionShape); - - if (source->mAvoidCollisionShape) - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); + mCollisionBox = mSource->mCollisionBox; + mAnimatedShapes = mSource->mAnimatedShapes; + mCollisionType = mSource->mCollisionType; + mCollisionShape = duplicateCollisionShape(mSource->mCollisionShape.get()); + mAvoidCollisionShape = duplicateCollisionShape(mSource->mAvoidCollisionShape.get()); } } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index e77c96327f..63db8ec482 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -1,7 +1,9 @@ #ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H +#include #include +#include #include #include @@ -11,26 +13,39 @@ class btCollisionShape; +namespace NifBullet +{ + class BulletNifLoader; +} + namespace Resource { + struct DeleteCollisionShape + { + void operator()(btCollisionShape* shape) const; + }; + + using CollisionShapePtr = std::unique_ptr; - class BulletShapeInstance; class BulletShape : public osg::Object { public: - BulletShape(); + BulletShape() = default; BulletShape(const BulletShape& copy, const osg::CopyOp& copyop); - virtual ~BulletShape(); META_Object(Resource, BulletShape) - btCollisionShape* mCollisionShape; - btCollisionShape* mAvoidCollisionShape; + CollisionShapePtr mCollisionShape; + CollisionShapePtr mAvoidCollisionShape; - // Used for actors. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. + struct CollisionBox + { + osg::Vec3f mExtents; + osg::Vec3f mCenter; + }; + // Used for actors and projectiles. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. // For now, use one file <-> one resource for simplicity. - osg::Vec3f mCollisionBoxHalfExtents; - osg::Vec3f mCollisionBoxTranslate; + CollisionBox mCollisionBox; // Stores animated collision shapes. If any collision nodes in the NIF are animated, then mCollisionShape // will be a btCompoundShape (which consists of one or more child shapes). @@ -38,19 +53,19 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - osg::ref_ptr makeInstance() const; - - btCollisionShape* duplicateCollisionShape(const btCollisionShape* shape) const; - - btCollisionShape* getCollisionShape() const; - - btCollisionShape* getAvoidCollisionShape() const; + std::string mFileName; + std::string mFileHash; void setLocalScaling(const btVector3& scale); - private: + bool isAnimated() const { return !mAnimatedShapes.empty(); } - void deleteShape(btCollisionShape* shape); + unsigned int mCollisionType = 0; + enum CollisionType + { + None = 0x1, + Camera = 0x2 + }; }; @@ -61,10 +76,14 @@ namespace Resource public: BulletShapeInstance(osg::ref_ptr source); + const osg::ref_ptr& getSource() const { return mSource; } + private: osg::ref_ptr mSource; }; + osg::ref_ptr makeInstance(osg::ref_ptr source); + // Subclass btBhvTriangleMeshShape to auto-delete the meshInterface struct TriangleMeshShape : public btBvhTriangleMeshShape { diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index bcadf51c4e..da4672757a 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -1,14 +1,18 @@ #include "bulletshapemanager.hpp" +#include + #include #include #include #include -#include #include +#include +#include #include +#include #include @@ -43,11 +47,7 @@ struct GetTriangleFunctor return btVector3(vec.x(), vec.y(), vec.z()); } -#if OSG_MIN_VERSION_REQUIRED(3,5,6) - void inline operator()( const osg::Vec3 v1, const osg::Vec3 v2, const osg::Vec3 v3 ) -#else - void inline operator()( const osg::Vec3 v1, const osg::Vec3 v2, const osg::Vec3 v3, bool _temp ) -#endif + void inline operator()( const osg::Vec3& v1, const osg::Vec3& v2, const osg::Vec3& v3, bool _temp=false ) // Note: unused temp argument left here for OSG versions less than 3.5.6 { if (mTriMesh) mTriMesh->addTriangle( toBullet(mMatrix.preMult(v1)), toBullet(mMatrix.preMult(v2)), toBullet(mMatrix.preMult(v3))); @@ -86,7 +86,18 @@ public: return osg::ref_ptr(); osg::ref_ptr shape (new BulletShape); - shape->mCollisionShape = new TriangleMeshShape(mTriangleMesh.release(), true); + + auto triangleMeshShape = std::make_unique(mTriangleMesh.release(), true); + btVector3 aabbMin = triangleMeshShape->getLocalAabbMin(); + btVector3 aabbMax = triangleMeshShape->getLocalAabbMax(); + shape->mCollisionBox.mExtents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; + shape->mCollisionBox.mExtents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; + shape->mCollisionBox.mExtents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; + shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, + (aabbMax[1] + aabbMin[1]) / 2.0f, + (aabbMax[2] + aabbMin[2]) / 2.0f ); + shape->mCollisionShape.reset(triangleMeshShape.release()); + return shape; } @@ -110,8 +121,7 @@ BulletShapeManager::~BulletShapeManager() osg::ref_ptr BulletShapeManager::getShape(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr shape; osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); @@ -119,12 +129,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & shape = osg::ref_ptr(static_cast(obj.get())); else { - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); - - if (ext == "nif") + if (Misc::getFileExtension(normalized) == "nif") { NifBullet::BulletNifLoader loader; shape = loader.load(*mNifFileManager->get(normalized)); @@ -135,11 +140,37 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & osg::ref_ptr constNode (mSceneManager->getTemplate(normalized)); osg::ref_ptr node (const_cast(constNode.get())); // const-trickery required because there is no const version of NodeVisitor - NodeToShapeVisitor visitor; - node->accept(visitor); - shape = visitor.getShape(); + + // Check first if there's a custom collision node + unsigned int visitAllNodesMask = 0xffffffff; + SceneUtil::FindByNameVisitor nameFinder("Collision"); + nameFinder.setTraversalMask(visitAllNodesMask); + nameFinder.setNodeMaskOverride(visitAllNodesMask); + node->accept(nameFinder); + if (nameFinder.mFoundNode) + { + NodeToShapeVisitor visitor; + visitor.setTraversalMask(visitAllNodesMask); + visitor.setNodeMaskOverride(visitAllNodesMask); + nameFinder.mFoundNode->accept(visitor); + shape = visitor.getShape(); + } + + // Generate a collision shape from the mesh if (!shape) - return osg::ref_ptr(); + { + NodeToShapeVisitor visitor; + node->accept(visitor); + shape = visitor.getShape(); + if (!shape) + return osg::ref_ptr(); + } + + if (shape != nullptr) + { + shape->mFileName = normalized; + constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash); + } } mCache->addEntryToObjectCache(normalized, shape); @@ -149,8 +180,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & osg::ref_ptr BulletShapeManager::cacheInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr instance = createInstance(normalized); if (instance) @@ -160,8 +190,7 @@ osg::ref_ptr BulletShapeManager::cacheInstance(const std::s osg::ref_ptr BulletShapeManager::getInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); if (obj.get()) @@ -174,9 +203,8 @@ osg::ref_ptr BulletShapeManager::createInstance(const std:: { osg::ref_ptr shape = getShape(name); if (shape) - return shape->makeInstance(); - else - return osg::ref_ptr(); + return makeInstance(std::move(shape)); + return osg::ref_ptr(); } void BulletShapeManager::updateCache(double referenceTime) diff --git a/components/resource/foreachbulletobject.cpp b/components/resource/foreachbulletobject.cpp new file mode 100644 index 0000000000..dc154802e0 --- /dev/null +++ b/components/resource/foreachbulletobject.cpp @@ -0,0 +1,162 @@ +#include "foreachbulletobject.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Resource +{ + namespace + { + struct CellRef + { + ESM::RecNameInts mType; + ESM::RefNum mRefNum; + std::string mRefId; + float mScale; + ESM::Position mPos; + + CellRef(ESM::RecNameInts type, ESM::RefNum refNum, std::string&& refId, float scale, const ESM::Position& pos) + : mType(type), mRefNum(refNum), mRefId(std::move(refId)), mScale(scale), mPos(pos) {} + }; + + ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, std::string_view refId) + { + const auto it = std::lower_bound(esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(), + refId, EsmLoader::LessById {}); + if (it == esmData.mRefIdTypes.end() || it->mId != refId) + return {}; + return it->mType; + } + + std::vector loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, + ESM::ReadersCache& readers) + { + std::vector> cellRefs; + + for (std::size_t i = 0; i < cell.mContextList.size(); i++) + { + const ESM::ReadersCache::BusyItem reader = readers.get(static_cast(cell.mContextList[i].index)); + cell.restore(*reader, static_cast(i)); + ESM::CellRef cellRef; + bool deleted = false; + while (ESM::Cell::getNextRef(*reader, cellRef, deleted)) + { + Misc::StringUtils::lowerCaseInPlace(cellRef.mRefID); + const ESM::RecNameInts type = getType(esmData, cellRef.mRefID); + if (type == ESM::RecNameInts {}) + continue; + cellRefs.emplace_back(deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID), + cellRef.mScale, cellRef.mPos); + } + } + + Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; + + const auto getKey = [] (const EsmLoader::Record& v) -> const ESM::RefNum& { return v.mValue.mRefNum; }; + std::vector result = prepareRecords(cellRefs, getKey); + + Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; + + return result; + } + + template + void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, ESM::ReadersCache& readers, + F&& f) + { + std::vector cellRefs = loadCellRefs(cell, esmData, readers); + + Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; + + for (CellRef& cellRef : cellRefs) + { + std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + if (model.empty()) + continue; + + if (cellRef.mType != ESM::REC_STAT) + model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs); + + osg::ref_ptr shape = [&] + { + try + { + return bulletShapeManager.getShape("meshes/" + model); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model << "\": " << e.what(); + return osg::ref_ptr(); + } + } (); + + if (shape == nullptr) + continue; + + switch (cellRef.mType) + { + case ESM::REC_ACTI: + case ESM::REC_CONT: + case ESM::REC_DOOR: + case ESM::REC_STAT: + f(BulletObject {std::move(shape), cellRef.mPos, cellRef.mScale}); + break; + default: + break; + } + } + } + } + + void forEachBulletObject(ESM::ReadersCache& readers, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + std::function callback) + { + Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; + + for (std::size_t i = 0; i < esmData.mCells.size(); ++i) + { + const ESM::Cell& cell = esmData.mCells[i]; + const bool exterior = cell.isExterior(); + + Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + + std::size_t objects = 0; + + forEachObject(cell, esmData, vfs, bulletShapeManager, readers, + [&] (const BulletObject& object) + { + callback(cell, object); + ++objects; + }); + + Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cell.getDescription() + << " with " << objects << " objects"; + } + } +} diff --git a/components/resource/foreachbulletobject.hpp b/components/resource/foreachbulletobject.hpp new file mode 100644 index 0000000000..0ccaf729ad --- /dev/null +++ b/components/resource/foreachbulletobject.hpp @@ -0,0 +1,48 @@ +#ifndef OPENMW_COMPONENTS_RESOURCE_FOREACHBULLETOBJECT_H +#define OPENMW_COMPONENTS_RESOURCE_FOREACHBULLETOBJECT_H + +#include +#include +#include + +#include + +#include +#include + +namespace ESM +{ + class ReadersCache; + struct Cell; +} + +namespace VFS +{ + class Manager; +} + +namespace Resource +{ + class BulletShapeManager; +} + +namespace EsmLoader +{ + struct EsmData; +} + +namespace Resource +{ + struct BulletObject + { + osg::ref_ptr mShape; + ESM::Position mPosition; + float mScale; + }; + + void forEachBulletObject(ESM::ReadersCache& readers, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + std::function callback); +} + +#endif diff --git a/components/resource/imagemanager.cpp b/components/resource/imagemanager.cpp index ff6fb04a65..ef90353a73 100644 --- a/components/resource/imagemanager.cpp +++ b/components/resource/imagemanager.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "objectcache.hpp" @@ -48,6 +49,7 @@ namespace Resource : ResourceManager(vfs) , mWarningImage(createWarningImage()) , mOptions(new osgDB::Options("dds_flip dds_dxt1_detect_rgba ignoreTga2Fields")) + , mOptionsNoFlip(new osgDB::Options("dds_dxt1_detect_rgba ignoreTga2Fields")) { } @@ -81,10 +83,9 @@ namespace Resource return true; } - osg::ref_ptr ImageManager::getImage(const std::string &filename) + osg::ref_ptr ImageManager::getImage(const std::string &filename, bool disableFlip) { - std::string normalized = filename; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(filename); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -94,7 +95,7 @@ namespace Resource Files::IStreamPtr stream; try { - stream = mVFS->get(normalized.c_str()); + stream = mVFS->get(normalized); } catch (std::exception& e) { @@ -103,10 +104,7 @@ namespace Resource return mWarningImage; } - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); + const std::string ext(Misc::getFileExtension(normalized)); osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); if (!reader) { @@ -138,7 +136,7 @@ namespace Resource stream->seekg(0); } - osgDB::ReaderWriter::ReadResult result = reader->readImage(*stream, mOptions); + osgDB::ReaderWriter::ReadResult result = reader->readImage(*stream, disableFlip ? mOptionsNoFlip : mOptions); if (!result.success()) { Log(Debug::Error) << "Error loading " << filename << ": " << result.message() << " code " << result.status(); @@ -151,7 +149,7 @@ namespace Resource image->setFileName(normalized); if (!checkSupported(image, filename)) { - static bool uncompress = (getenv("OPENMW_DECOMPRESS_TEXTURES") != 0); + static bool uncompress = (getenv("OPENMW_DECOMPRESS_TEXTURES") != nullptr); if (!uncompress) { Log(Debug::Error) << "Error loading " << filename << ": no S3TC texture compression support installed"; diff --git a/components/resource/imagemanager.hpp b/components/resource/imagemanager.hpp index 64954af54b..85ea69795a 100644 --- a/components/resource/imagemanager.hpp +++ b/components/resource/imagemanager.hpp @@ -28,7 +28,7 @@ namespace Resource /// Create or retrieve an Image /// Returns the dummy image if the given image is not found. - osg::ref_ptr getImage(const std::string& filename); + osg::ref_ptr getImage(const std::string& filename, bool disableFlip = false); osg::Image* getWarningImage(); @@ -37,6 +37,7 @@ namespace Resource private: osg::ref_ptr mWarningImage; osg::ref_ptr mOptions; + osg::ref_ptr mOptionsNoFlip; ImageManager(const ImageManager&); void operator = (const ImageManager&); diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 8c5c50adca..8aa32a28bc 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -2,13 +2,130 @@ #include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "animation.hpp" #include "objectcache.hpp" +#include "scenemanager.hpp" namespace Resource { - KeyframeManager::KeyframeManager(const VFS::Manager* vfs) + RetrieveAnimationsVisitor::RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager, + const std::string& normalized, const VFS::Manager* vfs) : + osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mTarget(target), mAnimationManager(animationManager), mNormalized(normalized), mVFS(vfs) {} + + void RetrieveAnimationsVisitor::apply(osg::Node& node) + { + if (node.libraryName() == std::string("osgAnimation") && node.className() == std::string("Bone") && Misc::StringUtils::lowerCase(node.getName()) == std::string("bip01")) + { + osg::ref_ptr callback = new SceneUtil::OsgAnimationController(); + + std::vector emulatedAnimations; + + for (const auto& animation : mAnimationManager->getAnimationList()) + { + if (animation) + { + if (animation->getName() == "Default") //"Default" is osg dae plugin's default naming scheme for unnamed animations + { + animation->setName(std::string("idle")); // animation naming scheme "idle: start" and "idle: stop" is the default idle animation that OpenMW seems to want to play + } + + osg::ref_ptr mergedAnimationTrack = new Resource::Animation; + const std::string animationName = animation->getName(); + mergedAnimationTrack->setName(animationName); + + const osgAnimation::ChannelList& channels = animation->getChannels(); + for (const auto& channel: channels) + { + mergedAnimationTrack->addChannel(channel.get()->clone()); // is ->clone needed? + } + + callback->addMergedAnimationTrack(mergedAnimationTrack); + + float startTime = animation->getStartTime(); + float stopTime = startTime + animation->getDuration(); + + SceneUtil::EmulatedAnimation emulatedAnimation; + emulatedAnimation.mStartTime = startTime; + emulatedAnimation.mStopTime = stopTime; + emulatedAnimation.mName = animationName; + emulatedAnimations.emplace_back(emulatedAnimation); + } + } + + // mTextKeys is a nif-thing, used by OpenMW's animation system + // Format is likely "AnimationName: [Keyword_optional] [Start OR Stop]" + // AnimationNames are keywords like idle2, idle3... AiPackages and various mechanics control which animations are played + // Keywords can be stuff like Loop, Equip, Unequip, Block, InventoryHandtoHand, InventoryWeaponOneHand, PickProbe, Slash, Thrust, Chop... even "Slash Small Follow" + // osgAnimation formats should have a .txt file with the same name, each line holding a textkey and whitespace separated time value + // e.g. idle: start 0.0333 + try + { + Files::IStreamPtr textKeysFile = mVFS->get(changeFileExtension(mNormalized, "txt")); + std::string line; + while ( getline (*textKeysFile, line) ) + { + mTarget.mTextKeys.emplace(parseTimeSignature(line), parseTextKey(line)); + } + } + catch (std::exception&) + { + Log(Debug::Warning) << "No textkey file found for " << mNormalized; + } + + callback->setEmulatedAnimations(emulatedAnimations); + mTarget.mKeyframeControllers.emplace(node.getName(), callback); + } + + traverse(node); + } + + std::string RetrieveAnimationsVisitor::parseTextKey(const std::string& line) + { + size_t spacePos = line.find_last_of(' '); + if (spacePos != std::string::npos) + return line.substr(0, spacePos); + return ""; + } + + double RetrieveAnimationsVisitor::parseTimeSignature(const std::string& line) + { + size_t spacePos = line.find_last_of(' '); + double time = 0.0; + if (spacePos != std::string::npos && spacePos + 1 < line.size()) + time = std::stod(line.substr(spacePos + 1)); + return time; + } + + std::string RetrieveAnimationsVisitor::changeFileExtension(const std::string& file, const std::string& ext) + { + size_t extPos = file.find_last_of('.'); + if (extPos != std::string::npos && extPos+1 < file.size()) + { + return file.substr(0, extPos + 1) + ext; + } + return file; + } + +} + +namespace Resource +{ + + KeyframeManager::KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager) : ResourceManager(vfs) + , mSceneManager(sceneManager) { } @@ -16,19 +133,30 @@ namespace Resource { } - osg::ref_ptr KeyframeManager::get(const std::string &name) + osg::ref_ptr KeyframeManager::get(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) - return osg::ref_ptr(static_cast(obj.get())); + return osg::ref_ptr(static_cast(obj.get())); else { - osg::ref_ptr loaded (new NifOsg::KeyframeHolder); - NifOsg::Loader::loadKf(Nif::NIFFilePtr(new Nif::NIFFile(mVFS->getNormalized(normalized), normalized)), *loaded.get()); - + osg::ref_ptr loaded (new SceneUtil::KeyframeHolder); + if (Misc::getFileExtension(normalized) == "kf") + { + NifOsg::Loader::loadKf(Nif::NIFFilePtr(new Nif::NIFFile(mVFS->getNormalized(normalized), normalized)), *loaded.get()); + } + else + { + osg::ref_ptr scene = const_cast ( mSceneManager->getTemplate(normalized).get() ); + osg::ref_ptr bam = dynamic_cast (scene->getUpdateCallback()); + if (bam) + { + Resource::RetrieveAnimationsVisitor rav(*loaded.get(), bam, normalized, mVFS); + scene->accept(rav); + } + } mCache->addEntryToObjectCache(normalized, loaded); return loaded; } diff --git a/components/resource/keyframemanager.hpp b/components/resource/keyframemanager.hpp index fe1c4014e0..75c9cc6ff4 100644 --- a/components/resource/keyframemanager.hpp +++ b/components/resource/keyframemanager.hpp @@ -2,28 +2,58 @@ #define OPENMW_COMPONENTS_KEYFRAMEMANAGER_H #include +#include #include -#include +#include #include "resourcemanager.hpp" namespace Resource { + /// @brief extract animations to OpenMW's animation system + class RetrieveAnimationsVisitor : public osg::NodeVisitor + { + public: + RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager, + const std::string& normalized, const VFS::Manager* vfs); + + virtual void apply(osg::Node& node) override; + + private: + + std::string changeFileExtension(const std::string& file, const std::string& ext); + std::string parseTextKey(const std::string& line); + double parseTimeSignature(const std::string& line); + + SceneUtil::KeyframeHolder& mTarget; + osg::ref_ptr mAnimationManager; + std::string mNormalized; + const VFS::Manager* mVFS; + + }; +} + +namespace Resource +{ + + class SceneManager; /// @brief Managing of keyframe resources /// @note May be used from any thread. class KeyframeManager : public ResourceManager { public: - KeyframeManager(const VFS::Manager* vfs); + KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager); ~KeyframeManager(); /// Retrieve a read-only keyframe resource by name (case-insensitive). /// @note Throws an exception if the resource is not found. - osg::ref_ptr get(const std::string& name); + osg::ref_ptr get(const std::string& name); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; + private: + SceneManager* mSceneManager; }; } diff --git a/components/resource/objectcache.hpp b/components/resource/objectcache.hpp index 6e309a7f77..5c4b511f0a 100644 --- a/components/resource/objectcache.hpp +++ b/components/resource/objectcache.hpp @@ -119,7 +119,7 @@ class GenericObjectCache : public osg::Referenced typename ObjectCacheMap::iterator itr = _objectCache.find(key); if (itr!=_objectCache.end()) return itr->second.first; - else return 0; + else return nullptr; } /** Check if an object is in the cache, and if it is, update its usage time stamp. */ diff --git a/components/resource/resourcemanager.hpp b/components/resource/resourcemanager.hpp index ccb065e3bf..b2b71f4635 100644 --- a/components/resource/resourcemanager.hpp +++ b/components/resource/resourcemanager.hpp @@ -59,6 +59,7 @@ namespace Resource /// How long to keep objects in cache after no longer being referenced. void setExpiryDelay (double expiryDelay) override { mExpiryDelay = expiryDelay; } + float getExpiryDelay() const { return mExpiryDelay; } const VFS::Manager* getVFS() const { return mVFS; } diff --git a/components/resource/resourcesystem.cpp b/components/resource/resourcesystem.cpp index 2015ba874d..a62dd0016a 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -13,10 +13,10 @@ namespace Resource ResourceSystem::ResourceSystem(const VFS::Manager *vfs) : mVFS(vfs) { - mNifFileManager.reset(new NifFileManager(vfs)); - mKeyframeManager.reset(new KeyframeManager(vfs)); - mImageManager.reset(new ImageManager(vfs)); - mSceneManager.reset(new SceneManager(vfs, mImageManager.get(), mNifFileManager.get())); + mNifFileManager = std::make_unique(vfs); + mImageManager = std::make_unique(vfs); + mSceneManager = std::make_unique(vfs, mImageManager.get(), mNifFileManager.get()); + mKeyframeManager = std::make_unique(vfs, mSceneManager.get()); addResourceManager(mNifFileManager.get()); addResourceManager(mKeyframeManager.get()); diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 44ba7e6878..094220938b 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -1,14 +1,20 @@ #include "scenemanager.hpp" #include +#include +#include +#include #include #include +#include + #include #include +#include #include #include @@ -17,7 +23,11 @@ #include #include +#include #include +#include +#include +#include #include @@ -25,28 +35,33 @@ #include #include #include +#include +#include +#include +#include +#include #include #include +#include +#include + #include "imagemanager.hpp" #include "niffilemanager.hpp" #include "objectcache.hpp" -#include "multiobjectcache.hpp" namespace { - class InitWorldSpaceParticlesCallback : public osg::NodeCallback + class InitWorldSpaceParticlesCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osgParticle::ParticleSystem* node, osg::NodeVisitor* nv) { - osgParticle::ParticleSystem* partsys = static_cast(node); - // HACK: Ignore the InverseWorldMatrix transform the particle system is attached to - if (partsys->getNumParents() && partsys->getParent(0)->getNumParents()) - transformInitialParticles(partsys, partsys->getParent(0)->getParent(0)); + if (node->getNumParents() && node->getParent(0)->getNumParents()) + transformInitialParticles(node, node->getParent(0)->getParent(0)); node->removeUpdateCallback(this); } @@ -110,6 +125,10 @@ namespace namespace Resource { + void TemplateMultiRef::addRef(const osg::Node* node) + { + mObjects.emplace_back(node); + } class SharedStateManager : public osgDB::SharedStateManager { @@ -211,7 +230,110 @@ namespace Resource int mMaxAnisotropy; }; + // Check Collada extra descriptions + class ColladaDescriptionVisitor : public osg::NodeVisitor + { + public: + ColladaDescriptionVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), + mSkeleton(nullptr) + { + } + + osg::AlphaFunc::ComparisonFunction getTestMode(std::string mode) + { + if (mode == "ALWAYS") return osg::AlphaFunc::ALWAYS; + if (mode == "LESS") return osg::AlphaFunc::LESS; + if (mode == "EQUAL") return osg::AlphaFunc::EQUAL; + if (mode == "LEQUAL") return osg::AlphaFunc::LEQUAL; + if (mode == "GREATER") return osg::AlphaFunc::GREATER; + if (mode == "NOTEQUAL") return osg::AlphaFunc::NOTEQUAL; + if (mode == "GEQUAL") return osg::AlphaFunc::GEQUAL; + if (mode == "NEVER") return osg::AlphaFunc::NEVER; + + Log(Debug::Warning) << "Unexpected alpha testing mode: " << mode; + return osg::AlphaFunc::LEQUAL; + } + + void apply(osg::Node& node) override + { + if (osg::StateSet* stateset = node.getStateSet()) + { + if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN) + { + osg::ref_ptr depth = new osg::Depth; + depth->setWriteMask(false); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + else if (stateset->getRenderingHint() == osg::StateSet::OPAQUE_BIN) + { + osg::ref_ptr depth = new osg::Depth; + depth->setWriteMask(true); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + } + /* Check if the has + correct format for OpenMW: alphatest mode value MaterialName + e.g alphatest GEQUAL 0.8 MyAlphaTestedMaterial */ + std::vector descriptions = node.getDescriptions(); + for (const auto & description : descriptions) + { + mDescriptions.emplace_back(description); + } + + // Iterate each description, and see if the current node uses the specified material for alpha testing + if (node.getStateSet()) + { + for (const auto & description : mDescriptions) + { + std::vector descriptionParts; + std::istringstream descriptionStringStream(description); + for (std::string part; std::getline(descriptionStringStream, part, ' ');) + { + descriptionParts.emplace_back(part); + } + + if (descriptionParts.size() > (3) && descriptionParts.at(3) == node.getStateSet()->getName()) + { + if (descriptionParts.at(0) == "alphatest") + { + osg::AlphaFunc::ComparisonFunction mode = getTestMode(descriptionParts.at(1)); + osg::ref_ptr alphaFunc (new osg::AlphaFunc(mode, std::stod(descriptionParts.at(2)))); + node.getStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + } + } + + if (descriptionParts.size() > (0) && descriptionParts.at(0) == "bodypart") + { + SceneUtil::FindByClassVisitor osgaRigFinder("RigGeometryHolder"); + node.accept(osgaRigFinder); + for(osg::Node* foundRigNode : osgaRigFinder.mFoundNodes) + { + if (SceneUtil::RigGeometryHolder* rigGeometryHolder = dynamic_cast (foundRigNode)) + mRigGeometryHolders.emplace_back(osg::ref_ptr (rigGeometryHolder)); + else Log(Debug::Error) << "Converted RigGeometryHolder is of a wrong type."; + } + + if (!mRigGeometryHolders.empty()) + { + osgAnimation::RigGeometry::FindNearestParentSkeleton skeletonFinder; + mRigGeometryHolders[0]->accept(skeletonFinder); + if (skeletonFinder._root.valid()) mSkeleton = skeletonFinder._root; + } + } + } + } + traverse(node); + } + private: + std::vector mDescriptions; + public: + osgAnimation::Skeleton* mSkeleton; //pointer is valid only if the model is a bodypart, osg::ref_ptr + std::vector> mRigGeometryHolders; + }; SceneManager::SceneManager(const VFS::Manager *vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) : ResourceManager(vfs) @@ -221,7 +343,9 @@ namespace Resource , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) - , mInstanceCache(new MultiObjectCache) + , mLightingMethod(SceneUtil::LightingMethod::FFP) + , mConvertAlphaTestToAlphaToCoverage(false) + , mSupportsNormalsRT(false) , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) , mNifFileManager(nifFileManager) @@ -243,13 +367,22 @@ namespace Resource return mForceShaders; } - void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix) + void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix, bool forceShadersForNode, const osg::Program* programTemplate) { osg::ref_ptr shaderVisitor(createShaderVisitor(shaderPrefix)); shaderVisitor->setAllowedToModifyStateSets(false); + shaderVisitor->setProgramTemplate(programTemplate); + if (forceShadersForNode) + shaderVisitor->setForceShaders(true); node->accept(*shaderVisitor); } + void SceneManager::reinstateRemovedState(osg::ref_ptr node) + { + osg::ref_ptr reinstateRemovedStateVisitor = new Shader::ReinstateRemovedStateVisitor(false); + node->accept(*reinstateRemovedStateVisitor); + } + void SceneManager::setClampLighting(bool clamp) { mClampLighting = clamp; @@ -290,6 +423,48 @@ namespace Resource mApplyLightingToEnvMaps = apply; } + void SceneManager::setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported) + { + mSupportedLightingMethods = supported; + } + + bool SceneManager::isSupportedLightingMethod(SceneUtil::LightingMethod method) const + { + return mSupportedLightingMethods[static_cast(method)]; + } + + void SceneManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + { + osg::ref_ptr program = new osg::Program; + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); + mShaderManager->setProgramTemplate(program); + } + } + + SceneUtil::LightingMethod SceneManager::getLightingMethod() const + { + return mLightingMethod; + } + + void SceneManager::setConvertAlphaTestToAlphaToCoverage(bool convert) + { + mConvertAlphaTestToAlphaToCoverage = convert; + } + + void SceneManager::setOpaqueDepthTex(osg::ref_ptr texturePing, osg::ref_ptr texturePong) + { + mOpaqueDepthTex = { texturePing, texturePong }; + } + + osg::ref_ptr SceneManager::getOpaqueDepthTex(size_t frame) + { + return mOpaqueDepthTex[frame % 2]; + } + SceneManager::~SceneManager() { // this has to be defined in the .cpp file as we can't delete incomplete types @@ -307,10 +482,7 @@ namespace Resource bool SceneManager::checkLoaded(const std::string &name, double timeStamp) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - return mCache->checkInObjectCache(normalized, timeStamp); + return mCache->checkInObjectCache(mVFS->normalizeFilename(name), timeStamp); } /// @brief Callback to read image files from the VFS. @@ -324,9 +496,15 @@ namespace Resource osgDB::ReaderWriter::ReadResult readImage(const std::string& filename, const osgDB::Options* options) override { + std::filesystem::path filePath(filename); + if (filePath.is_absolute()) + // It is a hack. Needed because either OSG or libcollada-dom tries to make an absolute path from + // our relative VFS path by adding current working directory. + filePath = std::filesystem::relative(filename, osgDB::getCurrentWorkingDirectory()); try { - return osgDB::ReaderWriter::ReadResult(mImageManager->getImage(filename), osgDB::ReaderWriter::ReadResult::FILE_LOADED); + return osgDB::ReaderWriter::ReadResult(mImageManager->getImage(filePath.string()), + osgDB::ReaderWriter::ReadResult::FILE_LOADED); } catch (std::exception& e) { @@ -338,22 +516,12 @@ namespace Resource Resource::ImageManager* mImageManager; }; - std::string getFileExtension(const std::string& file) + namespace { - size_t extPos = file.find_last_of('.'); - if (extPos != std::string::npos && extPos+1 < file.size()) - return file.substr(extPos+1); - return std::string(); - } - - osg::ref_ptr load (Files::IStreamPtr file, const std::string& normalizedFilename, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) - { - std::string ext = getFileExtension(normalizedFilename); - if (ext == "nif") - return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); - else + osg::ref_ptr loadNonNif(const std::string& normalizedFilename, std::istream& model, Resource::ImageManager* imageManager) { - osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); + auto ext = Misc::getFileExtension(normalizedFilename); + osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext)); if (!reader) { std::stringstream errormsg; @@ -368,17 +536,97 @@ namespace Resource options->setReadFileCallback(new ImageReadCallback(imageManager)); if (ext == "dae") options->setOptionString("daeUseSequencedTextureUnits"); - osgDB::ReaderWriter::ReadResult result = reader->readNode(*file, options); + const std::array fileHash = Files::getHash(normalizedFilename, model); + + osgDB::ReaderWriter::ReadResult result = reader->readNode(model, options); if (!result.success()) { std::stringstream errormsg; errormsg << "Error loading " << normalizedFilename << ": " << result.message() << " code " << result.status() << std::endl; throw std::runtime_error(errormsg.str()); } - return result.getNode(); + + // Recognize and hide collision node + unsigned int hiddenNodeMask = 0; + SceneUtil::FindByNameVisitor nameFinder("Collision"); + + auto node = result.getNode(); + node->accept(nameFinder); + if (nameFinder.mFoundNode) + nameFinder.mFoundNode->setNodeMask(hiddenNodeMask); + + // Recognize and convert osgAnimation::RigGeometry to OpenMW-optimized type + SceneUtil::FindByClassVisitor rigFinder("RigGeometry"); + node->accept(rigFinder); + for(osg::Node* foundRigNode : rigFinder.mFoundNodes) + { + if (foundRigNode->libraryName() == std::string("osgAnimation")) + { + osgAnimation::RigGeometry* foundRigGeometry = static_cast (foundRigNode); + osg::ref_ptr newRig = new SceneUtil::RigGeometryHolder(*foundRigGeometry, osg::CopyOp::DEEP_COPY_ALL); + + if (foundRigGeometry->getStateSet()) newRig->setStateSet(foundRigGeometry->getStateSet()); + + if (osg::Group* parent = dynamic_cast (foundRigGeometry->getParent(0))) + { + parent->removeChild(foundRigGeometry); + parent->addChild(newRig); + } + } + } + + if (ext == "dae") + { + Resource::ColladaDescriptionVisitor colladaDescriptionVisitor; + node->accept(colladaDescriptionVisitor); + + if (colladaDescriptionVisitor.mSkeleton) + { + if ( osg::Group* group = dynamic_cast (node) ) + { + group->removeChildren(0, group->getNumChildren()); + for (osg::ref_ptr newRiggeometryHolder : colladaDescriptionVisitor.mRigGeometryHolders) + { + osg::ref_ptr backToOriginTrans = new osg::MatrixTransform(); + + newRiggeometryHolder->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(newRiggeometryHolder->getGeometry(0))); + backToOriginTrans->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(newRiggeometryHolder->getGeometry(0))); + + newRiggeometryHolder->setBodyPart(true); + + for (int i = 0; i < 2; ++i) + { + if (newRiggeometryHolder->getGeometry(i)) newRiggeometryHolder->getGeometry(i)->setSkeleton(nullptr); + } + + backToOriginTrans->addChild(newRiggeometryHolder); + group->addChild(backToOriginTrans); + } + } + } + + node->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false)); + } + + node->setUserValue(Misc::OsgUserValues::sFileHash, + std::string(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t))); + + return node; } } + osg::ref_ptr load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) + { + auto ext = Misc::getFileExtension(normalizedFilename); + if (ext == "nif") + return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); + else + return loadNonNif(normalizedFilename, *vfs->get(normalizedFilename), imageManager); + } + class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback { public: @@ -392,7 +640,8 @@ namespace Resource { const char* reserved[] = {"Head", "Neck", "Chest", "Groin", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield Bone", "Right Forearm", "Left Forearm", "Right Upper Arm", "Left Upper Arm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Upper Leg", "Left Upper Leg", "Right Clavicle", - "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera"}; + "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera", "Collision", "Right_Wrist", "Left_Wrist", + "Shield_Bone", "Right_Forearm", "Left_Forearm", "Right_Upper_Arm", "Left_Clavicle", "Weapon_Bone", "Root_Bone"}; reservedNames = std::vector(reserved, reserved + sizeof(reserved)/sizeof(reserved[0])); @@ -402,7 +651,7 @@ namespace Resource std::sort(reservedNames.begin(), reservedNames.end(), Misc::StringUtils::ciLess); } - std::vector::iterator it = Misc::StringUtils::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); + std::vector::iterator it = Misc::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); return it != reservedNames.end(); } @@ -459,7 +708,7 @@ namespace Resource { std::string str(env); - if(str.find("OFF")!=std::string::npos || str.find("0")!= std::string::npos) options = 0; + if(str.find("OFF")!=std::string::npos || str.find('0')!= std::string::npos) options = 0; if(str.find("~FLATTEN_STATIC_TRANSFORMS")!=std::string::npos) options ^= Optimizer::FLATTEN_STATIC_TRANSFORMS; else if(str.find("FLATTEN_STATIC_TRANSFORMS")!=std::string::npos) options |= Optimizer::FLATTEN_STATIC_TRANSFORMS; @@ -473,10 +722,15 @@ namespace Resource return options; } + void SceneManager::shareState(osg::ref_ptr node) { + mSharedStateMutex.lock(); + mSharedStateManager->share(node.get()); + mSharedStateMutex.unlock(); + } + osg::ref_ptr SceneManager::getTemplate(const std::string &name, bool compile) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -486,28 +740,28 @@ namespace Resource osg::ref_ptr loaded; try { - Files::IStreamPtr file = mVFS->get(normalized); + loaded = load(normalized, mVFS, mImageManager, mNifFileManager); - loaded = load(file, normalized, mImageManager, mNifFileManager); + SceneUtil::ProcessExtraDataVisitor extraDataVisitor(this); + loaded->accept(extraDataVisitor); } - catch (std::exception& e) + catch (const std::exception& e) { - static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2" }; + static osg::ref_ptr errorMarkerNode = [&] { + static const char* const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" }; - for (unsigned int i=0; iexists(normalized)) + for (unsigned int i=0; iget(normalized); - loaded = load(file, normalized, mImageManager, mNifFileManager); - break; + normalized = "meshes/marker_error." + std::string(sMeshTypes[i]); + if (mVFS->exists(normalized)) + return load(normalized, mVFS, mImageManager, mNifFileManager); } - } + Files::IMemStream file(Misc::errorMarker.data(), Misc::errorMarker.size()); + return loadNonNif("error_marker.osgt", file, mImageManager); + }(); - if (!loaded) - throw; + Log(Debug::Error) << "Failed to load '" << name << "': " << e.what() << ", using marker_error instead"; + loaded = static_cast(errorMarkerNode->clone(osg::CopyOp::DEEP_COPY_ALL)); } // set filtering settings @@ -516,25 +770,24 @@ namespace Resource SetFilterSettingsControllerVisitor setFilterSettingsControllerVisitor(mMinFilter, mMagFilter, mMaxAnisotropy); loaded->accept(setFilterSettingsControllerVisitor); + SceneUtil::ReplaceDepthVisitor replaceDepthVisitor; + loaded->accept(replaceDepthVisitor); + osg::ref_ptr shaderVisitor (createShaderVisitor()); loaded->accept(*shaderVisitor); - // share state - // do this before optimizing so the optimizer will be able to combine nodes more aggressively - // note, because StateSets will be shared at this point, StateSets can not be modified inside the optimizer - mSharedStateMutex.lock(); - mSharedStateManager->share(loaded.get()); - mSharedStateMutex.unlock(); - if (canOptimize(normalized)) { SceneUtil::Optimizer optimizer; + optimizer.setSharedStateManager(mSharedStateManager, &mSharedStateMutex); optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); - static const unsigned int options = getOptimizationOptions(); + static const unsigned int options = getOptimizationOptions()|SceneUtil::Optimizer::SHARE_DUPLICATE_STATE; optimizer.optimize(loaded, options); } + else + shareState(loaded); if (compile && mIncrementalCompileOperation) mIncrementalCompileOperation->add(loaded); @@ -546,49 +799,33 @@ namespace Resource } } - osg::ref_ptr SceneManager::cacheInstance(const std::string &name) - { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr node = createInstance(normalized); - - // Note: osg::clone() does not calculate bound volumes. - // Do it immediately, otherwise we will need to update them for all objects - // during first update traversal, what may lead to stuttering during cell transitions - node->getBound(); - - mInstanceCache->addEntryToObjectCache(normalized, node.get()); - return node; - } - - class TemplateRef : public osg::Object - { - public: - TemplateRef(const Object* object) - : mObject(object) {} - TemplateRef() {} - TemplateRef(const TemplateRef& copy, const osg::CopyOp&) : mObject(copy.mObject) {} - - META_Object(Resource, TemplateRef) - - private: - osg::ref_ptr mObject; - }; - - osg::ref_ptr SceneManager::createInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); - return createInstance(scene); + return getInstance(scene); } - osg::ref_ptr SceneManager::createInstance(const osg::Node *base) + osg::ref_ptr SceneManager::cloneNode(const osg::Node* base) { - osg::ref_ptr cloned = static_cast(base->clone(SceneUtil::CopyOp())); - - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + SceneUtil::CopyOp copyop; + if (const osg::Drawable* drawable = base->asDrawable()) + { + if (drawable->asGeometry()) + { + Log(Debug::Warning) << "SceneManager::cloneNode: attempting to clone osg::Geometry. For safety reasons this will be expensive. Consider avoiding this call."; + copyop.setCopyFlags(copyop.getCopyFlags()|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES); + } + } + osg::ref_ptr cloned = static_cast(base->clone(copyop)); + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, if this node is managed by a cache, we hint to the cache that it's still being used and should be kept in cache cloned->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(base)); + return cloned; + } + osg::ref_ptr SceneManager::getInstance(const osg::Node *base) + { + osg::ref_ptr cloned = cloneNode(base); // we can skip any scene graphs without update callbacks since we know that particle emitters will have an update callback set if (cloned->getNumChildrenRequiringUpdateTraversal() > 0) { @@ -599,19 +836,6 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string &name) - { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); - if (obj.get()) - return static_cast(obj.get()); - - return createInstance(normalized); - - } - osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); @@ -627,7 +851,6 @@ namespace Resource void SceneManager::releaseGLObjects(osg::State *state) { mCache->releaseGLObjects(state); - mInstanceCache->releaseGLObjects(state); mShaderManager->releaseGLObjects(state); @@ -715,8 +938,6 @@ namespace Resource { ResourceManager::updateCache(referenceTime); - mInstanceCache->removeUnreferencedObjectsInCache(); - mSharedStateMutex.lock(); mSharedStateManager->prune(); mSharedStateMutex.unlock(); @@ -746,7 +967,6 @@ namespace Resource std::lock_guard lock(mSharedStateMutex); mSharedStateManager->clearCache(); - mInstanceCache->clear(); } void SceneManager::reportStats(unsigned int frameNumber, osg::Stats *stats) const @@ -764,12 +984,11 @@ namespace Resource } stats->setAttribute(frameNumber, "Node", mCache->getCacheSize()); - stats->setAttribute(frameNumber, "Node Instance", mInstanceCache->getCacheSize()); } Shader::ShaderVisitor *SceneManager::createShaderVisitor(const std::string& shaderPrefix) { - Shader::ShaderVisitor* shaderVisitor = new Shader::ShaderVisitor(*mShaderManager.get(), *mImageManager, shaderPrefix+"_vertex.glsl", shaderPrefix+"_fragment.glsl"); + Shader::ShaderVisitor* shaderVisitor = new Shader::ShaderVisitor(*mShaderManager.get(), *mImageManager, shaderPrefix); shaderVisitor->setForceShaders(mForceShaders); shaderVisitor->setAutoUseNormalMaps(mAutoUseNormalMaps); shaderVisitor->setNormalMapPattern(mNormalMapPattern); @@ -777,7 +996,8 @@ namespace Resource shaderVisitor->setAutoUseSpecularMaps(mAutoUseSpecularMaps); shaderVisitor->setSpecularMapPattern(mSpecularMapPattern); shaderVisitor->setApplyLightingToEnvMaps(mApplyLightingToEnvMaps); + shaderVisitor->setConvertAlphaTestToAlphaToCoverage(mConvertAlphaTestToAlphaToCoverage); + shaderVisitor->setSupportsNormalsRT(mSupportsNormalsRT); return shaderVisitor; } - } diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index 8df556158e..d3ad868a99 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -9,9 +9,12 @@ #include #include #include +#include #include "resourcemanager.hpp" +#include + namespace Resource { class ImageManager; @@ -37,8 +40,31 @@ namespace Shader namespace Resource { + class TemplateRef : public osg::Object + { + public: + TemplateRef(const Object* object) : mObject(object) {} + TemplateRef() {} + TemplateRef(const TemplateRef& copy, const osg::CopyOp&) : mObject(copy.mObject) {} + + META_Object(Resource, TemplateRef) + + private: + osg::ref_ptr mObject; + }; + + class TemplateMultiRef : public osg::Object + { + public: + TemplateMultiRef() {} + TemplateMultiRef(const TemplateMultiRef& copy, const osg::CopyOp&) : mObjects(copy.mObjects) {} + void addRef(const osg::Node* node); + + META_Object(Resource, TemplateMultiRef) - class MultiObjectCache; + private: + std::vector> mObjects; + }; /// @brief Handles loading and caching of scenes, e.g. .nif files or .osg files /// @note Some methods of the scene manager can be used from any thread, see the methods documentation for more details. @@ -50,8 +76,13 @@ namespace Resource Shader::ShaderManager& getShaderManager(); - /// Re-create shaders for this node, need to call this if texture stages or vertex color mode have changed. - void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects"); + /// Re-create shaders for this node, need to call this if alpha testing, texture stages or vertex color mode have changed. + void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects", bool forceShadersForNode = false, const osg::Program* programTemplate = nullptr); + + /// Applying shaders to a node may replace some fixed-function state. + /// This restores it. + /// When editing such state, it should be reinstated before the edits, and shaders should be recreated afterwards. + void reinstateRemovedState(osg::ref_ptr node); /// @see ShaderVisitor::setForceShaders void setForceShaders(bool force); @@ -75,6 +106,24 @@ namespace Resource void setApplyLightingToEnvMaps(bool apply); + void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); + bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + + void setOpaqueDepthTex(osg::ref_ptr texturePing, osg::ref_ptr texturePong); + + osg::ref_ptr getOpaqueDepthTex(size_t frame); + + enum class UBOBinding + { + // If we add more UBO's, we should probably assign their bindings dynamically according to the current count of UBO's in the programTemplate + LightBuffer, + PostProcessor + }; + void setLightingMethod(SceneUtil::LightingMethod method); + SceneUtil::LightingMethod getLightingMethod() const; + + void setConvertAlphaTestToAlphaToCoverage(bool convert); + void setShaderPath(const std::string& path); /// Check if a given scene is loaded and if so, update its usage timestamp to prevent it from being unloaded @@ -86,22 +135,22 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - /// Create an instance of the given scene template and cache it for later use, so that future calls to getInstance() can simply - /// return this cached object instead of creating a new one. - /// @note The returned ref_ptr may be kept around by the caller to ensure that the object stays in cache for as long as needed. + /// Clone osg::Node safely. /// @note Thread safe. - osg::ref_ptr cacheInstance(const std::string& name); + static osg::ref_ptr cloneNode(const osg::Node* base); - osg::ref_ptr createInstance(const std::string& name); + void shareState(osg::ref_ptr node); - osg::ref_ptr createInstance(const osg::Node* base); + /// Clone osg::Node and adjust it according to SceneManager's settings. + /// @note Thread safe. + osg::ref_ptr getInstance(const osg::Node* base); - /// Get an instance of the given scene template + /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. osg::ref_ptr getInstance(const std::string& name); - /// Get an instance of the given scene template and immediately attach it to a parent node + /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); @@ -146,6 +195,12 @@ namespace Resource void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; + void setSupportsNormalsRT(bool supports) { mSupportsNormalsRT = supports; } + bool getSupportsNormalsRT() const { return mSupportsNormalsRT; } + + void setSoftParticles(bool enabled) { mSoftParticles = enabled; } + bool getSoftParticles() const { return mSoftParticles; } + private: Shader::ShaderVisitor* createShaderVisitor(const std::string& shaderPrefix = "objects"); @@ -159,8 +214,12 @@ namespace Resource bool mAutoUseSpecularMaps; std::string mSpecularMapPattern; bool mApplyLightingToEnvMaps; - - osg::ref_ptr mInstanceCache; + SceneUtil::LightingMethod mLightingMethod; + SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; + bool mConvertAlphaTestToAlphaToCoverage; + bool mSupportsNormalsRT; + std::array, 2> mOpaqueDepthTex; + bool mSoftParticles = false; osg::ref_ptr mSharedStateManager; mutable std::mutex mSharedStateMutex; @@ -181,6 +240,7 @@ namespace Resource void operator = (const SceneManager&); }; + std::string getFileExtension(const std::string& file); } #endif diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index 942bd92d80..a204bf61f8 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -6,6 +6,7 @@ #include +#include #include #include @@ -15,16 +16,112 @@ #include +#include + namespace Resource { -StatsHandler::StatsHandler(): +static bool collectStatRendering = false; +static bool collectStatCameraObjects = false; +static bool collectStatViewerObjects = false; +static bool collectStatResource = false; +static bool collectStatGPU = false; +static bool collectStatEvent = false; +static bool collectStatFrameRate = false; +static bool collectStatUpdate = false; +static bool collectStatEngine = false; + +constexpr std::string_view sFontName = "Fonts/DejaVuLGCSansMono.ttf"; + +static void setupStatCollection() +{ + const char* envList = getenv("OPENMW_OSG_STATS_LIST"); + if (envList == nullptr) + return; + + std::string_view kwList(envList); + + auto kwBegin = kwList.begin(); + + while (kwBegin != kwList.end()) + { + auto kwEnd = std::find(kwBegin, kwList.end(), ';'); + + const auto kw = kwList.substr(std::distance(kwList.begin(), kwBegin), std::distance(kwBegin, kwEnd)); + + if (kw.compare("gpu") == 0) + collectStatGPU = true; + else if (kw.compare("event") == 0) + collectStatEvent = true; + else if (kw.compare("frame_rate") == 0) + collectStatFrameRate = true; + else if (kw.compare("update") == 0) + collectStatUpdate = true; + else if (kw.compare("engine") == 0) + collectStatEngine = true; + else if (kw.compare("rendering") == 0) + collectStatRendering = true; + else if (kw.compare("cameraobjects") == 0) + collectStatCameraObjects = true; + else if (kw.compare("viewerobjects") == 0) + collectStatViewerObjects = true; + else if (kw.compare("resource") == 0) + collectStatResource = true; + else if (kw.compare("times") == 0) + { + collectStatGPU = true; + collectStatEvent = true; + collectStatFrameRate = true; + collectStatUpdate = true; + collectStatEngine = true; + collectStatRendering = true; + } + + if (kwEnd == kwList.end()) + break; + + kwBegin = std::next(kwEnd); + } +} + +class SetFontVisitor : public osg::NodeVisitor +{ +public: + SetFontVisitor(osgText::Font* font) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mFont(font) + {} + + void apply(osg::Drawable& node) override + { + if (osgText::Text* text = dynamic_cast(&node)) + { + text->setFont(mFont); + } + } + +private: + osgText::Font* mFont; +}; + +osg::ref_ptr getMonoFont(VFS::Manager* vfs) +{ + if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf") && vfs->exists(sFontName)) + { + Files::IStreamPtr streamPtr = vfs->get(sFontName); + return osgText::readRefFontStream(*streamPtr.get()); + } + + return nullptr; +} + +StatsHandler::StatsHandler(bool offlineCollect, VFS::Manager* vfs): _key(osgGA::GUIEventAdapter::KEY_F4), _initialized(false), _statsType(false), + _offlineCollect(offlineCollect), _statsWidth(1280.0f), _statsHeight(1024.0f), - _font(""), _characterSize(18.0f) { _camera = new osg::Camera; @@ -34,20 +131,54 @@ StatsHandler::StatsHandler(): _resourceStatsChildNum = 0; - if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf")) - _font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); + _textFont = getMonoFont(vfs); } -Profiler::Profiler() +Profiler::Profiler(bool offlineCollect, VFS::Manager* vfs): + _offlineCollect(offlineCollect), + _initFonts(false) { - if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf")) - _font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); - else - _font = ""; - _characterSize = 18; + _font.clear(); + + _textFont = getMonoFont(vfs); setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3); + setupStatCollection(); +} + +void Profiler::setUpFonts() +{ + if (_textFont != nullptr) + { + SetFontVisitor visitor(_textFont); + _switch->accept(visitor); + } + + _initFonts = true; +} + +bool Profiler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) +{ + osgViewer::ViewerBase* viewer = nullptr; + + bool handled = StatsHandler::handle(ea, aa); + if (_initialized && !_initFonts) + setUpFonts(); + + auto* view = dynamic_cast(&aa); + if (view) + viewer = view->getViewerBase(); + + if (viewer) + { + // Add/remove openmw stats to the osd as necessary + viewer->getViewerStats()->collectStats("engine", _statsType >= StatsHandler::StatsType::VIEWER_STATS); + + if (_offlineCollect) + CollectStatistics(viewer); + } + return handled; } bool StatsHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) @@ -67,6 +198,9 @@ bool StatsHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdap toggle(viewer); + if (_offlineCollect) + CollectStatistics(viewer); + aa.requestRedraw(); return true; } @@ -287,31 +421,36 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "Texture", "StateSet", "Node", - "Node Instance", "Shape", "Shape Instance", "Image", "Nif", "Keyframe", "", + "Groundcover Chunk", "Object Chunk", "Terrain Chunk", "Terrain Texture", "Land", "Composite", "", - "UnrefQueue", - "", - "NavMesh UpdateJobs", + "NavMesh Jobs", + "NavMesh Waiting", + "NavMesh Pushed", + "NavMesh Processing", + "NavMesh DbJobs", + "NavMesh DbCacheHitRate", "NavMesh CacheSize", "NavMesh UsedTiles", "NavMesh CachedTiles", + "NavMesh CacheHitRate", "", "Mechanics Actors", "Mechanics Objects", "", "Physics Actors", "Physics Objects", + "Physics Projectiles", "Physics HeightFields", }); @@ -330,7 +469,6 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) osg::ref_ptr staticText = new osgText::Text; group->addChild( staticText.get() ); staticText->setColor(staticTextColor); - staticText->setFont(_font); staticText->setCharacterSize(_characterSize); staticText->setPosition(pos); @@ -356,11 +494,16 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) group->addChild( statsText.get() ); statsText->setColor(dynamicTextColor); - statsText->setFont(_font); statsText->setCharacterSize(_characterSize); statsText->setPosition(pos); statsText->setText(""); statsText->setDrawCallback(new ResourceStatsTextDrawCallback(viewer->getViewerStats(), statNames)); + + if (_textFont) + { + staticText->setFont(_textFont); + statsText->setFont(_textFont); + } } } @@ -370,6 +513,22 @@ void StatsHandler::getUsage(osg::ApplicationUsage &usage) const usage.addKeyboardMouseBinding(_key, "On screen resource usage stats."); } - +void CollectStatistics(osgViewer::ViewerBase* viewer) +{ + osgViewer::Viewer::Cameras cameras; + viewer->getCameras(cameras); + for (auto* camera : cameras) + { + if (collectStatGPU) camera->getStats()->collectStats("gpu", true); + if (collectStatRendering) camera->getStats()->collectStats("rendering", true); + if (collectStatCameraObjects) camera->getStats()->collectStats("scene", true); + } + if (collectStatEvent) viewer->getViewerStats()->collectStats("event", true); + if (collectStatFrameRate) viewer->getViewerStats()->collectStats("frame_rate", true); + if (collectStatUpdate) viewer->getViewerStats()->collectStats("update", true); + if (collectStatResource) viewer->getViewerStats()->collectStats("resource", true); + if (collectStatViewerObjects) viewer->getViewerStats()->collectStats("scene", true); + if (collectStatEngine) viewer->getViewerStats()->collectStats("engine", true); +} } diff --git a/components/resource/stats.hpp b/components/resource/stats.hpp index 9fa583cca4..e51474c6cb 100644 --- a/components/resource/stats.hpp +++ b/components/resource/stats.hpp @@ -13,18 +13,36 @@ namespace osg class Switch; } +namespace osgText +{ + class Font; +} + +namespace VFS +{ + class Manager; +} + namespace Resource { class Profiler : public osgViewer::StatsHandler { public: - Profiler(); + Profiler(bool offlineCollect, VFS::Manager* vfs); + bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) override; + + private: + void setUpFonts(); + + bool _offlineCollect; + bool _initFonts; + osg::ref_ptr _textFont; }; class StatsHandler : public osgGA::GUIEventHandler { public: - StatsHandler(); + StatsHandler(bool offlineCollect, VFS::Manager* vfs); void setKey(int key) { _key = key; } int getKey() const { return _key; } @@ -47,17 +65,20 @@ namespace Resource osg::ref_ptr _camera; bool _initialized; bool _statsType; + bool _offlineCollect; float _statsWidth; float _statsHeight; - std::string _font; float _characterSize; int _resourceStatsChildNum; + osg::ref_ptr _textFont; }; + void CollectStatistics(osgViewer::ViewerBase* viewer); + } #endif diff --git a/components/sceneutil/actorutil.cpp b/components/sceneutil/actorutil.cpp index 988a61f60e..a0785e413d 100644 --- a/components/sceneutil/actorutil.cpp +++ b/components/sceneutil/actorutil.cpp @@ -1,5 +1,7 @@ #include "actorutil.hpp" +#include + namespace SceneUtil { std::string getActorSkeleton(bool firstPerson, bool isFemale, bool isBeast, bool isWerewolf) @@ -7,24 +9,24 @@ namespace SceneUtil if (!firstPerson) { if (isWerewolf) - return "meshes\\wolf\\skin.nif"; + return Settings::Manager::getString("wolfskin", "Models"); else if (isBeast) - return "meshes\\base_animkna.nif"; + return Settings::Manager::getString("baseanimkna", "Models"); else if (isFemale) - return "meshes\\base_anim_female.nif"; + return Settings::Manager::getString("baseanimfemale", "Models"); else - return "meshes\\base_anim.nif"; + return Settings::Manager::getString("baseanim", "Models"); } else { if (isWerewolf) - return "meshes\\wolf\\skin.1st.nif"; + return Settings::Manager::getString("wolfskin1st", "Models"); else if (isBeast) - return "meshes\\base_animkna.1st.nif"; + return Settings::Manager::getString("baseanimkna1st", "Models"); else if (isFemale) - return "meshes\\base_anim_female.1st.nif"; + return Settings::Manager::getString("baseanimfemale1st", "Models"); else - return "meshes\\base_anim.1st.nif"; + return Settings::Manager::getString("xbaseanim1st", "Models"); } } } diff --git a/components/sceneutil/agentpath.cpp b/components/sceneutil/agentpath.cpp index abe332f758..e1fa9fe3c8 100644 --- a/components/sceneutil/agentpath.cpp +++ b/components/sceneutil/agentpath.cpp @@ -1,7 +1,10 @@ #include "agentpath.hpp" #include "detourdebugdraw.hpp" +#include + #include +#include #include @@ -34,17 +37,17 @@ namespace namespace SceneUtil { osg::ref_ptr createAgentPathGroup(const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, - const DetourNavigator::Settings& settings) + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::RecastSettings& settings) { using namespace DetourNavigator; const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1); + DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1); - const auto agentRadius = halfExtents.x(); - const auto agentHeight = 2.0f * halfExtents.z(); + const auto agentRadius = DetourNavigator::getAgentRadius(agentBounds); + const auto agentHeight = DetourNavigator::getAgentHeight(agentBounds); const auto agentClimb = settings.mMaxClimb; const auto startColor = duRGBA(128, 25, 0, 192); const auto endColor = duRGBA(51, 102, 0, 129); @@ -65,6 +68,10 @@ namespace SceneUtil debugDraw.depthMask(true); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + group->getOrCreateStateSet()->setAttribute(material); + return group; } } diff --git a/components/sceneutil/agentpath.hpp b/components/sceneutil/agentpath.hpp index a8965d852e..53a41e02d8 100644 --- a/components/sceneutil/agentpath.hpp +++ b/components/sceneutil/agentpath.hpp @@ -13,14 +13,15 @@ namespace osg namespace DetourNavigator { - struct Settings; + struct RecastSettings; + struct AgentBounds; } namespace SceneUtil { osg::ref_ptr createAgentPathGroup(const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, - const DetourNavigator::Settings& settings); + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::RecastSettings& settings); } #endif diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 597c7adf48..02c3456425 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "visitor.hpp" @@ -48,27 +49,22 @@ namespace SceneUtil if (!filterMatches(drawable.getName())) return; - osg::Node* node = &drawable; - while (node->getNumParents()) + const osg::Node* node = &drawable; + for (auto it = getNodePath().rbegin()+1; it != getNodePath().rend(); ++it) { - osg::Group* parent = node->getParent(0); - if (!parent || !filterMatches(parent->getName())) + const osg::Node* parent = *it; + if (!filterMatches(parent->getName())) break; node = parent; } mToCopy.emplace(node); } - void doCopy() + void doCopy(Resource::SceneManager* sceneManager) { - for (const osg::ref_ptr& node : mToCopy) + for (const osg::ref_ptr& node : mToCopy) { - if (node->getNumParents() > 1) - Log(Debug::Error) << "Error CopyRigVisitor: node has " << node->getNumParents() << " parents"; - while (node->getNumParents()) - node->getParent(0)->removeChild(node); - - mParent->addChild(node); + mParent->addChild(sceneManager->getInstance(node)); } mToCopy.clear(); } @@ -82,7 +78,7 @@ namespace SceneUtil || (lowerName.size() >= mFilter2.size() && lowerName.compare(0, mFilter2.size(), mFilter2) == 0); } - using NodeSet = std::set>; + using NodeSet = std::set>; NodeSet mToCopy; osg::ref_ptr mParent; @@ -90,26 +86,31 @@ namespace SceneUtil std::string mFilter2; }; - void mergeUserData(osg::UserDataContainer* source, osg::Object* target) + void mergeUserData(const osg::UserDataContainer* source, osg::Object* target) { + if (!source) + return; + if (!target->getUserDataContainer()) - target->setUserDataContainer(source); + target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY)); else { for (unsigned int i=0; igetNumUserObjects(); ++i) - target->getUserDataContainer()->addUserObject(source->getUserObject(i)); + target->getUserDataContainer()->addUserObject(osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY)); } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager, const osg::Quat* attitude) { - if (dynamic_cast(toAttach.get())) + if (dynamic_cast(toAttach.get())) { osg::ref_ptr handle = new osg::Group; CopyRigVisitor copyVisitor(handle, filter); - toAttach->accept(copyVisitor); - copyVisitor.doCopy(); + const_cast(toAttach.get())->accept(copyVisitor); + copyVisitor.doCopy(sceneManager); + // add a ref to the original template to hint to the cache that it is still being used and should be kept in cache. + handle->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(toAttach)); if (handle->getNumChildren() == 1) { @@ -122,14 +123,16 @@ namespace SceneUtil else { master->asGroup()->addChild(handle); - handle->setUserDataContainer(toAttach->getUserDataContainer()); + mergeUserData(toAttach->getUserDataContainer(), handle); return handle; } } else { + osg::ref_ptr clonedToAttach = sceneManager->getInstance(toAttach); + FindByNameVisitor findBoneOffset("BoneOffset"); - toAttach->accept(findBoneOffset); + clonedToAttach->accept(findBoneOffset); osg::ref_ptr trans; @@ -141,8 +144,6 @@ namespace SceneUtil trans = new osg::PositionAttitudeTransform; trans->setPosition(boneOffset->getMatrix().getTrans()); - // The BoneOffset rotation seems to be incorrect - trans->setAttitude(osg::Quat(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0))); // Now that we used it, get rid of the redundant node. if (boneOffset->getNumChildren() == 0 && boneOffset->getNumParents() == 1) @@ -169,16 +170,23 @@ namespace SceneUtil trans->setStateSet(frontFaceStateSet); } + if(attitude) + { + if (!trans) + trans = new osg::PositionAttitudeTransform; + trans->setAttitude(*attitude); + } + if (trans) { attachNode->addChild(trans); - trans->addChild(toAttach); + trans->addChild(clonedToAttach); return trans; } else { - attachNode->addChild(toAttach); - return toAttach; + attachNode->addChild(clonedToAttach); + return clonedToAttach; } } } diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index a8a2239a84..ed0299dece 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -9,17 +9,22 @@ namespace osg { class Node; class Group; + class Quat; +} +namespace Resource +{ + class SceneManager; } namespace SceneUtil { - /// Attach parts of the \a toAttach scenegraph to the \a master scenegraph, using the specified filter and attachment node. + /// Clone and attach parts of the \a toAttach scenegraph to the \a master scenegraph, using the specified filter and attachment node. /// If the \a toAttach scene graph contains skinned objects, we will attach only those (filtered by the \a filter). /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager, const osg::Quat* attitude = nullptr); } diff --git a/components/sceneutil/clearcolor.hpp b/components/sceneutil/clearcolor.hpp new file mode 100755 index 0000000000..e6e6468ecc --- /dev/null +++ b/components/sceneutil/clearcolor.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_CLEARCOLOR_H +#define OPENMW_COMPONENTS_SCENEUTIL_CLEARCOLOR_H + +#include +#include + +namespace SceneUtil +{ + class ClearColor : public osg::StateAttribute + { + public: + ClearColor() : mMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) {} + ClearColor(const osg::Vec4f& color, GLbitfield mask) : mColor(color), mMask(mask) {} + + ClearColor(const ClearColor& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mColor(copy.mColor), mMask(copy.mMask) {} + + META_StateAttribute(fx, ClearColor, static_cast(100)) + + int compare(const StateAttribute& sa) const override + { + COMPARE_StateAttribute_Types(ClearColor, sa); + + COMPARE_StateAttribute_Parameter(mColor); + COMPARE_StateAttribute_Parameter(mMask); + + return 0; + } + + void apply(osg::State& state) const override + { + glClearColor(mColor[0], mColor[1], mColor[2], mColor[3]); + glClear(mMask); + } + + private: + osg::Vec4f mColor; + GLbitfield mMask; + }; +} + +#endif diff --git a/components/sceneutil/clone.cpp b/components/sceneutil/clone.cpp index 1de7bfd91e..99a12c1677 100644 --- a/components/sceneutil/clone.cpp +++ b/components/sceneutil/clone.cpp @@ -2,12 +2,16 @@ #include +#include +#include + #include #include #include #include #include +#include namespace SceneUtil { @@ -38,7 +42,9 @@ namespace SceneUtil if (const osgParticle::ParticleSystem* partsys = dynamic_cast(drawable)) return operator()(partsys); - if (dynamic_cast(drawable) || dynamic_cast(drawable)) + if (dynamic_cast(drawable) || dynamic_cast(drawable) || + dynamic_cast(drawable) || dynamic_cast(drawable) || + dynamic_cast(drawable)) { return static_cast(drawable->clone(*this)); } diff --git a/components/sceneutil/clone.hpp b/components/sceneutil/clone.hpp index 1cf00c9e58..35240cbfba 100644 --- a/components/sceneutil/clone.hpp +++ b/components/sceneutil/clone.hpp @@ -18,6 +18,7 @@ namespace SceneUtil /// @par Defines the cloning behaviour we need: /// * Assigns updated ParticleSystem pointers on cloned emitters and programs. /// * Deep copies RigGeometry and MorphGeometry so they can animate without affecting clones. + /// @warning Avoid using this class directly. The safety of cloning operations depends on the copy flags and the objects involved. Consider using SceneManager::cloneNode for additional safety. /// @warning Do not use an object of this class for more than one copy operation. class CopyOp : public osg::CopyOp { diff --git a/components/sceneutil/color.cpp b/components/sceneutil/color.cpp new file mode 100644 index 0000000000..825d7f65fd --- /dev/null +++ b/components/sceneutil/color.cpp @@ -0,0 +1,157 @@ +#include "color.hpp" + +#include +#include + +#include + +#include +#include + +namespace SceneUtil +{ + + bool isColorFormat(GLenum format) + { + static constexpr std::array formats = { + GL_RGB, + GL_RGB4, + GL_RGB5, + GL_RGB8, + GL_RGB8_SNORM, + GL_RGB10, + GL_RGB12, + GL_RGB16, + GL_RGB16_SNORM, + GL_SRGB, + GL_SRGB8, + GL_RGB16F, + GL_RGB32F, + GL_R11F_G11F_B10F, + GL_RGB9_E5, + GL_RGB8I, + GL_RGB8UI, + GL_RGB16I, + GL_RGB16UI, + GL_RGB32I, + GL_RGB32UI, + GL_RGBA, + GL_RGBA2, + GL_RGBA4, + GL_RGB5_A1, + GL_RGBA8, + GL_RGBA8_SNORM, + GL_RGB10_A2, + GL_RGB10_A2UI, + GL_RGBA12, + GL_RGBA16, + GL_RGBA16_SNORM, + GL_SRGB_ALPHA8, + GL_SRGB8_ALPHA8, + GL_RGBA16F, + GL_RGBA32F, + GL_RGBA8I, + GL_RGBA8UI, + GL_RGBA16I, + GL_RGBA16UI, + GL_RGBA32I, + GL_RGBA32UI, + }; + + return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); + } + + bool isFloatingPointColorFormat(GLenum format) + { + static constexpr std::array formats = { + GL_RGB16F, + GL_RGB32F, + GL_R11F_G11F_B10F, + GL_RGBA16F, + GL_RGBA32F, + }; + + return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); + } + + int getColorFormatChannelCount(GLenum format) + { + static constexpr std::array formats = { + GL_RGBA, + GL_RGBA2, + GL_RGBA4, + GL_RGB5_A1, + GL_RGBA8, + GL_RGBA8_SNORM, + GL_RGB10_A2, + GL_RGB10_A2UI, + GL_RGBA12, + GL_RGBA16, + GL_RGBA16_SNORM, + GL_SRGB_ALPHA8, + GL_SRGB8_ALPHA8, + GL_RGBA16F, + GL_RGBA32F, + GL_RGBA8I, + GL_RGBA8UI, + GL_RGBA16I, + GL_RGBA16UI, + GL_RGBA32I, + GL_RGBA32UI, + }; + if (std::find(formats.cbegin(), formats.cend(), format) != formats.cend()) + return 4; + return 3; + } + + void getColorFormatSourceFormatAndType(GLenum internalFormat, GLenum& sourceFormat, GLenum& sourceType) + { + if (getColorFormatChannelCount(internalFormat == 4)) + sourceFormat = GL_RGBA; + else + sourceFormat = GL_RGB; + + if (isFloatingPointColorFormat(internalFormat)) + sourceType = GL_FLOAT; + else + sourceType = GL_UNSIGNED_BYTE; + } + + namespace Color + { + GLenum sColorInternalFormat; + GLenum sColorSourceFormat; + GLenum sColorSourceType; + + GLenum colorInternalFormat() + { + return sColorInternalFormat; + } + + GLenum colorSourceFormat() + { + return sColorSourceFormat; + } + + GLenum colorSourceType() + { + return sColorSourceType; + } + + void SelectColorFormatOperation::operator()([[maybe_unused]] osg::GraphicsContext* graphicsContext) + { + sColorInternalFormat = GL_RGB; + + for (auto supportedFormat : mSupportedFormats) + { + if (isColorFormat(supportedFormat)) + { + sColorInternalFormat = supportedFormat; + break; + } + } + + getColorFormatSourceFormatAndType(sColorInternalFormat, sColorSourceFormat, sColorSourceType); + } + } +} diff --git a/components/sceneutil/color.hpp b/components/sceneutil/color.hpp new file mode 100644 index 0000000000..e4812d1149 --- /dev/null +++ b/components/sceneutil/color.hpp @@ -0,0 +1,207 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_COLOR_H +#define OPENMW_COMPONENTS_SCENEUTIL_COLOR_H + +#include + +namespace SceneUtil +{ + bool isColorFormat(GLenum format); + bool isFloatingPointColorFormat(GLenum format); + int getColorFormatChannelCount(GLenum format); + void getColorFormatSourceFormatAndType(GLenum internalFormat, GLenum& sourceFormat, GLenum& sourceType); + + namespace Color + { + GLenum colorSourceFormat(); + GLenum colorSourceType(); + GLenum colorInternalFormat(); + + class SelectColorFormatOperation final : public osg::GraphicsOperation + { + public: + SelectColorFormatOperation() : GraphicsOperation("SelectColorFormatOperation", false) + {} + + void operator()(osg::GraphicsContext* graphicsContext) override; + + void setSupportedFormats(const std::vector& supportedFormats) + { + mSupportedFormats = supportedFormats; + } + + private: + std::vector mSupportedFormats; + }; + } +} + +#ifndef GL_RGB +#define GL_RGB 0x1907 +#endif + +#ifndef GL_RGBA +#define GL_RGBA 0x1908 +#endif + +#ifndef GL_RGB4 +#define GL_RGB4 0x804F +#endif + +#ifndef GL_RGB5 +#define GL_RGB5 0x8050 +#endif + +#ifndef GL_RGB8 +#define GL_RGB8 0x8051 +#endif + +#ifndef GL_RGB8_SNORM +#define GL_RGB8_SNORM 0x8F96 +#endif + +#ifndef GL_RGB10 +#define GL_RGB10 0x8052 +#endif + +#ifndef GL_RGB12 +#define GL_RGB12 0x8053 +#endif + +#ifndef GL_RGB16 +#define GL_RGB16 0x8054 +#endif + +#ifndef GL_RGB16_SNORM +#define GL_RGB16_SNORM 0x8F9A +#endif + +#ifndef GL_RGBA2 +#define GL_RGBA2 0x8055 +#endif + +#ifndef GL_RGBA4 +#define GL_RGBA4 0x8056 +#endif + +#ifndef GL_RGB5_A1 +#define GL_RGB5_A1 0x8057 +#endif + +#ifndef GL_RGBA8 +#define GL_RGBA8 0x8058 +#endif + +#ifndef GL_RGBA8_SNORM +#define GL_RGBA8_SNORM 0x8F97 +#endif + +#ifndef GL_RGB10_A2 +#define GL_RGB10_A2 0x906F +#endif + +#ifndef GL_RGB10_A2UI +#define GL_RGB10_A2UI 0x906F +#endif + +#ifndef GL_RGBA12 +#define GL_RGBA12 0x805A +#endif + +#ifndef GL_RGBA16 +#define GL_RGBA16 0x805B +#endif + +#ifndef GL_RGBA16_SNORM +#define GL_RGBA16_SNORM 0x8F9B +#endif + +#ifndef GL_SRGB +#define GL_SRGB 0x8C40 +#endif + +#ifndef GL_SRGB8 +#define GL_SRGB8 0x8C41 +#endif + +#ifndef GL_SRGB_ALPHA8 +#define GL_SRGB_ALPHA8 0x8C42 +#endif + +#ifndef GL_SRGB8_ALPHA8 +#define GL_SRGB8_ALPHA8 0x8C43 +#endif + +#ifndef GL_RGB16F +#define GL_RGB16F 0x881B +#endif + +#ifndef GL_RGBA16F +#define GL_RGBA16F 0x881A +#endif + +#ifndef GL_RGB32F +#define GL_RGB32F 0x8815 +#endif + +#ifndef GL_RGBA32F +#define GL_RGBA32F 0x8814 +#endif + +#ifndef GL_R11F_G11F_B10F +#define GL_R11F_G11F_B10F 0x8C3A +#endif + + +#ifndef GL_RGB8I +#define GL_RGB8I 0x8D8F +#endif + +#ifndef GL_RGB8UI +#define GL_RGB8UI 0x8D7D +#endif + +#ifndef GL_RGB16I +#define GL_RGB16I 0x8D89 +#endif + +#ifndef GL_RGB16UI +#define GL_RGB16UI 0x8D77 +#endif + +#ifndef GL_RGB32I +#define GL_RGB32I 0x8D83 +#endif + +#ifndef GL_RGB32UI +#define GL_RGB32UI 0x8D71 +#endif + +#ifndef GL_RGBA8I +#define GL_RGBA8I 0x8D8E +#endif + +#ifndef GL_RGBA8UI +#define GL_RGBA8UI 0x8D7C +#endif + +#ifndef GL_RGBA16I +#define GL_RGBA16I 0x8D88 +#endif + +#ifndef GL_RGBA16UI +#define GL_RGBA16UI 0x8D76 +#endif + +#ifndef GL_RGBA32I +#define GL_RGBA32I 0x8D82 +#endif + +#ifndef GL_RGBA32UI +#define GL_RGBA32UI 0x8D70 +#endif + +#ifndef GL_RGB9_E5 +#define GL_RGB9_E5 0x8C3D +#endif + +#endif diff --git a/components/sceneutil/controller.cpp b/components/sceneutil/controller.cpp index dfc72918aa..2c7507d0a3 100644 --- a/components/sceneutil/controller.cpp +++ b/components/sceneutil/controller.cpp @@ -121,6 +121,21 @@ namespace SceneUtil ctrl.setSource(mToAssign); } + ForceControllerSourcesVisitor::ForceControllerSourcesVisitor() + : AssignControllerSourcesVisitor() + { + } + + ForceControllerSourcesVisitor::ForceControllerSourcesVisitor(std::shared_ptr toAssign) + : AssignControllerSourcesVisitor(toAssign) + { + } + + void ForceControllerSourcesVisitor::visit(osg::Node&, Controller &ctrl) + { + ctrl.setSource(mToAssign); + } + FindMaxControllerLengthVisitor::FindMaxControllerLengthVisitor() : SceneUtil::ControllerVisitor() , mMaxLength(0) diff --git a/components/sceneutil/controller.hpp b/components/sceneutil/controller.hpp index 2656d654e1..6ef800f05b 100644 --- a/components/sceneutil/controller.hpp +++ b/components/sceneutil/controller.hpp @@ -85,10 +85,20 @@ namespace SceneUtil /// By default assigns the ControllerSource passed to the constructor of this class if no ControllerSource is assigned to that controller yet. void visit(osg::Node& node, Controller& ctrl) override; - private: + protected: std::shared_ptr mToAssign; }; + class ForceControllerSourcesVisitor : public AssignControllerSourcesVisitor + { + public: + ForceControllerSourcesVisitor(); + ForceControllerSourcesVisitor(std::shared_ptr toAssign); + + /// Assign the wanted ControllerSource even if one is already assigned to the controller. + void visit(osg::Node& node, Controller& ctrl) override; + }; + /// Finds the maximum of all controller functions in the given scene graph class FindMaxControllerLengthVisitor : public ControllerVisitor { diff --git a/components/sceneutil/depth.cpp b/components/sceneutil/depth.cpp new file mode 100644 index 0000000000..f51c973389 --- /dev/null +++ b/components/sceneutil/depth.cpp @@ -0,0 +1,186 @@ +#include "depth.hpp" + +#include + +#include +#include + +namespace SceneUtil +{ + void setCameraClearDepth(osg::Camera* camera) + { + camera->setClearDepth(AutoDepth::isReversed() ? 0.0 : 1.0); + } + + osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near) + { + double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); + return osg::Matrix( + A/aspect, 0, 0, 0, + 0, A, 0, 0, + 0, 0, 0, -1, + 0, 0, near, 0 + ); + } + + osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far) + { + double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); + return osg::Matrix( + A/aspect, 0, 0, 0, + 0, A, 0, 0, + 0, 0, near/(far-near), -1, + 0, 0, (far*near)/(far - near), 0 + ); + } + + osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far) + { + return osg::Matrix( + 2/(right-left), 0, 0, 0, + 0, 2/(top-bottom), 0, 0, + 0, 0, 1/(far-near), 0, + (right+left)/(left-right), (top+bottom)/(bottom-top), far/(far-near), 1 + ); + } + + bool isDepthFormat(GLenum format) + { + constexpr std::array formats = { + GL_DEPTH_COMPONENT32F, + GL_DEPTH_COMPONENT32F_NV, + GL_DEPTH_COMPONENT16, + GL_DEPTH_COMPONENT24, + GL_DEPTH_COMPONENT32, + GL_DEPTH32F_STENCIL8, + GL_DEPTH32F_STENCIL8_NV, + GL_DEPTH24_STENCIL8, + }; + + return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); + } + + bool isDepthStencilFormat(GLenum format) + { + constexpr std::array formats = { + GL_DEPTH32F_STENCIL8, + GL_DEPTH32F_STENCIL8_NV, + GL_DEPTH24_STENCIL8, + }; + + return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); + } + + void getDepthFormatSourceFormatAndType(GLenum internalFormat, GLenum& sourceFormat, GLenum& sourceType) + { + switch (internalFormat) + { + case GL_DEPTH_COMPONENT16: + case GL_DEPTH_COMPONENT24: + case GL_DEPTH_COMPONENT32: + sourceType = GL_UNSIGNED_INT; + sourceFormat = GL_DEPTH_COMPONENT; + break; + case GL_DEPTH_COMPONENT32F: + case GL_DEPTH_COMPONENT32F_NV: + sourceType = GL_FLOAT; + sourceFormat = GL_DEPTH_COMPONENT; + break; + case GL_DEPTH24_STENCIL8: + sourceType = GL_UNSIGNED_INT_24_8_EXT; + sourceFormat = GL_DEPTH_STENCIL_EXT; + break; + case GL_DEPTH32F_STENCIL8: + case GL_DEPTH32F_STENCIL8_NV: + sourceType = GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + sourceFormat = GL_DEPTH_STENCIL_EXT; + break; + default: + sourceType = GL_UNSIGNED_INT; + sourceFormat = GL_DEPTH_COMPONENT; + break; + } + } + + GLenum getDepthFormatOfDepthStencilFormat(GLenum internalFormat) + { + switch (internalFormat) + { + case GL_DEPTH24_STENCIL8: + return GL_DEPTH_COMPONENT24; + break; + case GL_DEPTH32F_STENCIL8: + return GL_DEPTH_COMPONENT32F; + break; + case GL_DEPTH32F_STENCIL8_NV: + return GL_DEPTH_COMPONENT32F_NV; + break; + default: + return internalFormat; + break; + } + } + + void SelectDepthFormatOperation::operator()(osg::GraphicsContext* graphicsContext) + { + bool enableReverseZ = false; + + if (Settings::Manager::getBool("reverse z", "Camera")) + { + osg::ref_ptr exts = osg::GLExtensions::Get(0, false); + if (exts && exts->isClipControlSupported) + { + enableReverseZ = true; + Log(Debug::Info) << "Using reverse-z depth buffer"; + } + else + Log(Debug::Warning) << "GL_ARB_clip_control not supported: disabling reverse-z depth buffer"; + } + else + Log(Debug::Info) << "Using standard depth buffer"; + + SceneUtil::AutoDepth::setReversed(enableReverseZ); + + constexpr char errPreamble[] = "Postprocessing and floating point depth buffers disabled: "; + std::vector requestedFormats; + unsigned int contextID = graphicsContext->getState()->getContextID(); + if (SceneUtil::AutoDepth::isReversed()) + { + if (osg::isGLExtensionSupported(contextID, "GL_ARB_depth_buffer_float")) + { + requestedFormats.push_back(GL_DEPTH32F_STENCIL8); + } + else if (osg::isGLExtensionSupported(contextID, "GL_NV_depth_buffer_float")) + { + requestedFormats.push_back(GL_DEPTH32F_STENCIL8_NV); + } + else + { + Log(Debug::Warning) << errPreamble << "'GL_ARB_depth_buffer_float' and 'GL_NV_depth_buffer_float' unsupported."; + } + } + + requestedFormats.push_back(GL_DEPTH24_STENCIL8); + if (mSupportedFormats.empty()) + { + SceneUtil::AutoDepth::setDepthFormat(requestedFormats.front()); + } + else + { + for (auto requestedFormat : requestedFormats) + { + if (std::find(mSupportedFormats.cbegin(), mSupportedFormats.cend(), requestedFormat) != mSupportedFormats.cend()) + { + SceneUtil::AutoDepth::setDepthFormat(requestedFormat); + break; + } + } + } + } + + void AutoDepth::setDepthFormat(GLenum format) + { + sDepthInternalFormat = format; + getDepthFormatSourceFormatAndType(sDepthInternalFormat, sDepthSourceFormat, sDepthSourceType); + } +} diff --git a/components/sceneutil/depth.hpp b/components/sceneutil/depth.hpp new file mode 100644 index 0000000000..f7b1875206 --- /dev/null +++ b/components/sceneutil/depth.hpp @@ -0,0 +1,187 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H +#define OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H + +#include + +#include "util.hpp" + +#ifndef GL_DEPTH32F_STENCIL8_NV +#define GL_DEPTH32F_STENCIL8_NV 0x8DAC +#endif + +#ifndef GL_DEPTH32F_STENCIL8 +#define GL_DEPTH32F_STENCIL8 0x8CAD +#endif + +#ifndef GL_FLOAT_32_UNSIGNED_INT_24_8_REV +#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD +#endif + +#ifndef GL_DEPTH24_STENCIL8 +#define GL_DEPTH24_STENCIL8 0x88F0 +#endif + +#ifndef GL_DEPTH_STENCIL_EXT +#define GL_DEPTH_STENCIL_EXT 0x84F9 +#endif + +#ifndef GL_UNSIGNED_INT_24_8_EXT +#define GL_UNSIGNED_INT_24_8_EXT 0x84FA +#endif + +namespace SceneUtil +{ + // Sets camera clear depth to 0 if reversed depth buffer is in use, 1 otherwise. + void setCameraClearDepth(osg::Camera* camera); + + // Returns a perspective projection matrix for use with a reversed z-buffer + // and an infinite far plane. This is derived by mapping the default z-range + // of [0,1] to [1,0], then taking the limit as far plane approaches infinity. + osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near); + + // Returns a perspective projection matrix for use with a reversed z-buffer. + osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far); + + // Returns an orthographic projection matrix for use with a reversed z-buffer. + osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far); + + // Returns true if the GL format is a depth format + bool isDepthFormat(GLenum format); + + // Returns true if the GL format is a depth+stencil format + bool isDepthStencilFormat(GLenum format); + + // Returns the corresponding source format and type for the given internal format + void getDepthFormatSourceFormatAndType(GLenum internalFormat, GLenum& sourceFormat, GLenum& sourceType); + + // Converts depth-stencil formats to their corresponding depth formats. + GLenum getDepthFormatOfDepthStencilFormat(GLenum internalFormat); + + // Brief wrapper around an osg::Depth that applies the reversed depth function when a reversed depth buffer is in use + class AutoDepth : public osg::Depth + { + public: + AutoDepth(osg::Depth::Function func=osg::Depth::LESS, double zNear=0.0, double zFar=1.0, bool writeMask=true) + { + setFunction(func); + setZNear(zNear); + setZFar(zFar); + setWriteMask(writeMask); + } + + AutoDepth(const osg::Depth& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) : osg::Depth(copy, copyop) {} + + osg::Object* cloneType() const override { return new AutoDepth; } + osg::Object* clone(const osg::CopyOp& copyop) const override { return new AutoDepth(*this,copyop); } + + void apply(osg::State& state) const override + { + glDepthFunc(static_cast(AutoDepth::isReversed() ? getReversedDepthFunction() : getFunction())); + glDepthMask(static_cast(getWriteMask())); + #if defined(OSG_GLES1_AVAILABLE) || defined(OSG_GLES2_AVAILABLE) || defined(OSG_GLES3_AVAILABLE) + glDepthRangef(getZNear(),getZFar()); + #else + glDepthRange(getZNear(),getZFar()); + #endif + } + + static void setReversed(bool reverseZ) + { + static bool init = false; + + if (!init) + { + AutoDepth::sReversed = reverseZ; + init = true; + } + } + + static bool isReversed() + { + return AutoDepth::sReversed; + } + + static void setDepthFormat(GLenum format); + + static GLenum depthInternalFormat() + { + return AutoDepth::sDepthInternalFormat; + } + + static GLenum depthSourceFormat() + { + return AutoDepth::sDepthSourceFormat; + } + + static GLenum depthSourceType() + { + return AutoDepth::sDepthSourceType; + } + + private: + + static inline bool sReversed = false; + static inline GLenum sDepthSourceFormat = GL_DEPTH_COMPONENT; + static inline GLenum sDepthInternalFormat = GL_DEPTH_COMPONENT24; + static inline GLenum sDepthSourceType = GL_UNSIGNED_INT; + + osg::Depth::Function getReversedDepthFunction() const + { + const osg::Depth::Function func = getFunction(); + + switch (func) + { + case osg::Depth::LESS: + return osg::Depth::GREATER; + case osg::Depth::LEQUAL: + return osg::Depth::GEQUAL; + case osg::Depth::GREATER: + return osg::Depth::LESS; + case osg::Depth::GEQUAL: + return osg::Depth::LEQUAL; + default: + return func; + } + } + + }; + + // Replaces all nodes osg::Depth state attributes with SceneUtil::AutoDepth. + class ReplaceDepthVisitor : public osg::NodeVisitor + { + public: + ReplaceDepthVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {} + + void apply(osg::Node& node) override + { + osg::StateSet* stateSet = node.getStateSet(); + + if (stateSet) + { + if (osg::Depth* depth = static_cast(stateSet->getAttribute(osg::StateAttribute::DEPTH))) + stateSet->setAttribute(new SceneUtil::AutoDepth(*depth)); + }; + + traverse(node); + } + }; + + class SelectDepthFormatOperation : public osg::GraphicsOperation + { + public: + SelectDepthFormatOperation() : GraphicsOperation("SelectDepthFormatOperation", false) + {} + + void operator()(osg::GraphicsContext* graphicsContext) override; + + void setSupportedFormats(const std::vector& supportedFormats) + { + mSupportedFormats = supportedFormats; + } + + private: + std::vector mSupportedFormats; + }; +} + +#endif diff --git a/components/sceneutil/detourdebugdraw.cpp b/components/sceneutil/detourdebugdraw.cpp index 7ef329fc16..62e7ea71c0 100644 --- a/components/sceneutil/detourdebugdraw.cpp +++ b/components/sceneutil/detourdebugdraw.cpp @@ -1,16 +1,12 @@ #include "detourdebugdraw.hpp" #include "util.hpp" -#include - #include #include #include namespace { - using DetourNavigator::operator<<; - osg::PrimitiveSet::Mode toOsgPrimitiveSetMode(duDebugDrawPrimitives value) { switch (value) @@ -32,19 +28,19 @@ namespace namespace SceneUtil { - DebugDraw::DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor) + DebugDraw::DebugDraw(osg::Group& group, const osg::ref_ptr& stateSet, + const osg::Vec3f& shift, float recastInvertedScaleFactor) : mGroup(group) + , mStateSet(stateSet) , mShift(shift) , mRecastInvertedScaleFactor(recastInvertedScaleFactor) - , mDepthMask(false) , mMode(osg::PrimitiveSet::POINTS) , mSize(1.0f) { } - void DebugDraw::depthMask(bool state) + void DebugDraw::depthMask(bool) { - mDepthMask = state; } void DebugDraw::texture(bool) @@ -56,7 +52,7 @@ namespace SceneUtil mMode = mode; mVertices = new osg::Vec3Array; mColors = new osg::Vec4Array; - mSize = size * mRecastInvertedScaleFactor; + mSize = size; } void DebugDraw::begin(duDebugDrawPrimitives prim, float size) @@ -88,16 +84,8 @@ namespace SceneUtil void DebugDraw::end() { - osg::ref_ptr stateSet(new osg::StateSet); - stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); - stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - stateSet->setMode(GL_DEPTH, (mDepthMask ? osg::StateAttribute::ON : osg::StateAttribute::OFF)); - stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateSet->setAttributeAndModes(new osg::LineWidth(mSize)); - stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - osg::ref_ptr geometry(new osg::Geometry); - geometry->setStateSet(stateSet); + geometry->setStateSet(mStateSet); geometry->setVertexArray(mVertices); geometry->setColorArray(mColors, osg::Array::BIND_PER_VERTEX); geometry->addPrimitiveSet(new osg::DrawArrays(mMode, 0, static_cast(mVertices->size()))); @@ -117,4 +105,16 @@ namespace SceneUtil { mColors->push_back(value); } + + osg::ref_ptr DebugDraw::makeStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateSet->setMode(GL_DEPTH, osg::StateAttribute::OFF); + stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateSet->setAttributeAndModes(new osg::LineWidth()); + stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + return stateSet; + } } diff --git a/components/sceneutil/detourdebugdraw.hpp b/components/sceneutil/detourdebugdraw.hpp index 9b6a28acea..1d00735ea0 100644 --- a/components/sceneutil/detourdebugdraw.hpp +++ b/components/sceneutil/detourdebugdraw.hpp @@ -15,7 +15,10 @@ namespace SceneUtil class DebugDraw : public duDebugDraw { public: - DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor); + explicit DebugDraw(osg::Group& group, const osg::ref_ptr& stateSet, + const osg::Vec3f& shift, float recastInvertedScaleFactor); + + static osg::ref_ptr makeStateSet(); void depthMask(bool state) override; @@ -38,9 +41,9 @@ namespace SceneUtil private: osg::Group& mGroup; + osg::ref_ptr mStateSet; osg::Vec3f mShift; float mRecastInvertedScaleFactor; - bool mDepthMask; osg::PrimitiveSet::Mode mMode; float mSize; osg::ref_ptr mVertices; diff --git a/components/sceneutil/extradata.cpp b/components/sceneutil/extradata.cpp new file mode 100644 index 0000000000..3f5bb6df88 --- /dev/null +++ b/components/sceneutil/extradata.cpp @@ -0,0 +1,69 @@ +#include "extradata.hpp" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace SceneUtil +{ + void ProcessExtraDataVisitor::setupSoftEffect(osg::Node& node, float size, bool falloff) + { + if (!mSceneMgr->getSoftParticles()) + return; + + const int unitSoftEffect = mSceneMgr->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::OpaqueDepthTexture); + static const osg::ref_ptr depth = new SceneUtil::AutoDepth(osg::Depth::LESS, 0, 1, false); + + osg::StateSet* stateset = node.getOrCreateStateSet(); + + stateset->addUniform(new osg::Uniform("opaqueDepthTex", unitSoftEffect)); + stateset->addUniform(new osg::Uniform("particleSize", size)); + stateset->addUniform(new osg::Uniform("particleFade", falloff)); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + node.setUserValue(Misc::OsgUserValues::sXSoftEffect, true); + } + + void ProcessExtraDataVisitor::apply(osg::Node& node) + { + std::string source; + + if (node.getUserValue(Misc::OsgUserValues::sExtraData, source) && !source.empty()) + { + YAML::Node root = YAML::Load(source); + + for (const auto& it : root["shader"]) + { + std::string key = it.first.as(); + + if (key == "soft_effect") + { + auto size = it.second["size"].as(45.f); + auto falloff = it.second["falloff"].as(false); + + setupSoftEffect(node, size, falloff); + } + } + + node.setUserValue(Misc::OsgUserValues::sExtraData, std::string{}); + } + else if (osgParticle::ParticleSystem* partsys = dynamic_cast(&node)) + { + setupSoftEffect(node, partsys->getDefaultParticleTemplate().getSizeRange().maximum, false); + } + + traverse(node); + } +} \ No newline at end of file diff --git a/components/sceneutil/extradata.hpp b/components/sceneutil/extradata.hpp new file mode 100644 index 0000000000..b460bd04d0 --- /dev/null +++ b/components/sceneutil/extradata.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_COMPONENTS_RESOURCE_EXTRADATA_H +#define OPENMW_COMPONENTS_RESOURCE_EXTRADATA_H + +#include + +#include +#include + +namespace Resource +{ + class SceneManager; +} + +namespace osg +{ + class Node; +} + +namespace SceneUtil +{ + class ProcessExtraDataVisitor : public osg::NodeVisitor + { + public: + ProcessExtraDataVisitor(Resource::SceneManager* sceneMgr) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mSceneMgr(sceneMgr) {} + + void apply(osg::Node& node) override; + + private: + void setupSoftEffect(osg::Node& node, float size, bool falloff); + + Resource::SceneManager* mSceneMgr; + }; +} + +#endif diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp new file mode 100644 index 0000000000..eee8d6646c --- /dev/null +++ b/components/sceneutil/keyframe.hpp @@ -0,0 +1,67 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP +#define OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP + +#include + +#include + +#include +#include +#include + +namespace SceneUtil +{ + /// @note Derived classes are expected to derive from osg::Callback and implement getAsCallback(). + class KeyframeController : public SceneUtil::Controller, public virtual osg::Object + { + public: + KeyframeController() {} + + KeyframeController(const KeyframeController& copy) + : SceneUtil::Controller(copy) {} + + virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } + + /// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on. + virtual osg::Callback* getAsCallback() = 0; + }; + + /// Wrapper object containing an animation track as a ref-countable osg::Object. + struct TextKeyMapHolder : public osg::Object + { + public: + TextKeyMapHolder() {} + TextKeyMapHolder(const TextKeyMapHolder& copy, const osg::CopyOp& copyop) + : osg::Object(copy, copyop) + , mTextKeys(copy.mTextKeys) + {} + + TextKeyMap mTextKeys; + + META_Object(SceneUtil, TextKeyMapHolder) + + }; + + /// Wrapper object containing the animation track and its KeyframeControllers. + class KeyframeHolder : public osg::Object + { + public: + KeyframeHolder() {} + KeyframeHolder(const KeyframeHolder& copy, const osg::CopyOp& copyop) + : mTextKeys(copy.mTextKeys) + , mKeyframeControllers(copy.mKeyframeControllers) + { + } + + TextKeyMap mTextKeys; + + META_Object(SceneUtil, KeyframeHolder) + + /// Controllers mapped to node name. + typedef std::map > KeyframeControllerMap; + KeyframeControllerMap mKeyframeControllers; + }; + +} + +#endif diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index c759fabc79..13e367baa4 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -26,7 +26,7 @@ namespace SceneUtil mType = type; } - void LightController::operator ()(osg::Node* node, osg::NodeVisitor* nv) + void LightController::operator ()(SceneUtil::LightSource* node, osg::NodeVisitor* nv) { double time = nv->getFrameStamp()->getSimulationTime(); if (mStartTime == 0) @@ -38,7 +38,7 @@ namespace SceneUtil if (mType == LT_Normal) { - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor); + node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor); traverse(node, nv); return; } @@ -62,7 +62,7 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness); + node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * node->getActorFade()); traverse(node, nv); } diff --git a/components/sceneutil/lightcontroller.hpp b/components/sceneutil/lightcontroller.hpp index 36b2e868e5..b67cd59472 100644 --- a/components/sceneutil/lightcontroller.hpp +++ b/components/sceneutil/lightcontroller.hpp @@ -1,15 +1,16 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_LIGHTCONTROLLER_H #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTCONTROLLER_H -#include +#include #include namespace SceneUtil { + class LightSource; + /// @brief Controller class to handle a pulsing and/or flickering light - /// @note Must be set on a SceneUtil::LightSource. - class LightController : public osg::NodeCallback + class LightController : public SceneUtil::NodeCallback { public: enum LightType { @@ -26,7 +27,7 @@ namespace SceneUtil void setDiffuse(const osg::Vec4f& color); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(SceneUtil::LightSource* node, osg::NodeVisitor* nv); private: LightType mType; diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 673e01f33c..be3fe4679b 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,34 +1,369 @@ #include "lightmanager.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + #include #include +#include + +#include +#include +#include + +#include + +namespace +{ + constexpr int maxLightsLowerLimit = 2; + constexpr int maxLightsUpperLimit = 64; + constexpr int ffpMaxLights = 8; + + bool sortLights(const SceneUtil::LightManager::LightSourceViewBound* left, const SceneUtil::LightManager::LightSourceViewBound* right) + { + static auto constexpr illuminationBias = 81.f; + return left->mViewBound.center().length2() - left->mViewBound.radius2()*illuminationBias < right->mViewBound.center().length2() - right->mViewBound.radius2()*illuminationBias; + } + + void configurePosition(osg::Matrixf& mat, const osg::Vec4& pos) + { + mat(0, 0) = pos.x(); + mat(0, 1) = pos.y(); + mat(0, 2) = pos.z(); + } + + void configureAmbient(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(1, 0) = color.r(); + mat(1, 1) = color.g(); + mat(1, 2) = color.b(); + } + + void configureDiffuse(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(2, 0) = color.r(); + mat(2, 1) = color.g(); + mat(2, 2) = color.b(); + } + + void configureSpecular(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(3, 0) = color.r(); + mat(3, 1) = color.g(); + mat(3, 2) = color.b(); + mat(3, 3) = color.a(); + } + + void configureAttenuation(osg::Matrixf& mat, float c, float l, float q, float r) + { + mat(0, 3) = c; + mat(1, 3) = l; + mat(2, 3) = q; + mat(3, 3) = r; + } +} namespace SceneUtil { + namespace + { + const std::unordered_map lightingMethodSettingMap = { + {"legacy", LightingMethod::FFP}, + {"shaders compatibility", LightingMethod::PerObjectUniform}, + {"shaders", LightingMethod::SingleUBO}, + }; + } - class LightStateCache + static int sLightId = 0; + + // Handles a GLSL shared layout by using configured offsets and strides to fill a continuous buffer, making the data upload to GPU simpler. + class LightBuffer : public osg::Referenced { public: - osg::Light* lastAppliedLight[8]; + + enum LayoutOffset + { + Diffuse, + DiffuseSign, + Ambient, + Specular, + Position, + AttenuationRadius + }; + + LightBuffer(int count) + : mData(new osg::FloatArray(3*4*count)) + , mEndian(osg::getCpuByteOrder()) + , mCount(count) + , mCachedSunPos(osg::Vec4()) + { + } + + LightBuffer(const LightBuffer&) = delete; + + void setDiffuse(int index, const osg::Vec4& value) + { + // Deal with negative lights (negative diffuse) by passing a sign bit in the unused alpha component + auto positiveColor = value; + unsigned int signBit = 1; + if (value[0] < 0) + { + positiveColor *= -1.0; + signBit = ~0u; + } + unsigned int packedColor = asRGBA(positiveColor); + std::memcpy(&(*mData)[getOffset(index, Diffuse)], &packedColor, sizeof(unsigned int)); + std::memcpy(&(*mData)[getOffset(index, DiffuseSign)], &signBit, sizeof(unsigned int)); + } + + void setAmbient(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Ambient)], &packed, sizeof(unsigned int)); + } + + void setSpecular(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Specular)], &packed, sizeof(unsigned int)); + } + + void setPosition(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, Position)], value.ptr(), sizeof(osg::Vec4f)); + } + + void setAttenuationRadius(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, AttenuationRadius)], value.ptr(), sizeof(osg::Vec4f)); + } + + auto& getData() + { + return mData; + } + + void dirty() + { + mData->dirty(); + } + + static constexpr int queryBlockSize(int sz) + { + return 3 * osg::Vec4::num_components * sizeof(GLfloat) * sz; + } + + void setCachedSunPos(const osg::Vec4& pos) + { + mCachedSunPos = pos; + } + + void uploadCachedSunPos(const osg::Matrix& viewMat) + { + osg::Vec4 viewPos = mCachedSunPos * viewMat; + std::memcpy(&(*mData)[getOffset(0, Position)], viewPos.ptr(), sizeof(osg::Vec4f)); + } + + unsigned int asRGBA(const osg::Vec4& value) const + { + return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA(); + } + + int getOffset(int index, LayoutOffset slot) const + { + return mOffsets.get(index, slot); + } + + void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) + { + configureLayout(Offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride), size); + } + + void configureLayout(const LightBuffer* other) + { + mOffsets = other->mOffsets; + int size = other->mData->size(); + + configureLayout(mOffsets, size); + } + + private: + class Offsets + { + public: + Offsets() + : mStride(12) + { + mValues[Diffuse] = 0; + mValues[Ambient] = 1; + mValues[Specular] = 2; + mValues[DiffuseSign] = 3; + mValues[Position] = 4; + mValues[AttenuationRadius] = 8; + } + + Offsets(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int stride) + : mStride((offsetAttenuationRadius + sizeof(GLfloat) * osg::Vec4::num_components + stride) / 4) + { + constexpr auto sizeofFloat = sizeof(GLfloat); + const auto diffuseOffset = offsetColors / sizeofFloat; + + mValues[Diffuse] = diffuseOffset; + mValues[Ambient] = diffuseOffset + 1; + mValues[Specular] = diffuseOffset + 2; + mValues[DiffuseSign] = diffuseOffset + 3; + mValues[Position] = offsetPosition / sizeofFloat; + mValues[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; + } + + int get(int index, LayoutOffset slot) const + { + return mStride * index + mValues[slot]; + } + + private: + int mStride; + std::array mValues; + }; + + void configureLayout(const Offsets& offsets, int size) + { + // Copy cloned data using current layout into current data using new layout. + // This allows to preserve osg::FloatArray buffer object in mData. + const auto data = mData->asVector(); + mData->resizeArray(static_cast(size)); + for (int i = 0; i < mCount; ++i) + { + std::memcpy(&(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f)); + } + mOffsets = offsets; + } + + osg::ref_ptr mData; + osg::Endian mEndian; + int mCount; + Offsets mOffsets; + osg::Vec4 mCachedSunPos; }; - LightStateCache* getLightStateCache(unsigned int contextid) + struct LightStateCache + { + std::vector lastAppliedLight; + }; + + LightStateCache* getLightStateCache(size_t contextid, size_t size = 8) { static std::vector cacheVector; if (cacheVector.size() < contextid+1) cacheVector.resize(contextid+1); + cacheVector[contextid].lastAppliedLight.resize(size); return &cacheVector[contextid]; } - // Resets the modelview matrix to just the view matrix before applying lights. - class LightStateAttribute : public osg::StateAttribute + void configureStateSetSunOverride(LightManager* lightManager, const osg::Light* light, osg::StateSet* stateset, int mode) + { + auto method = lightManager->getLightingMethod(); + switch (method) + { + case LightingMethod::FFP: + { + break; + } + case LightingMethod::PerObjectUniform: + { + osg::Matrixf lightMat; + configurePosition(lightMat, light->getPosition()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureSpecular(lightMat, light->getSpecular()); + + stateset->addUniform(lightManager->generateLightBufferUniform(lightMat), mode); + break; + } + case LightingMethod::SingleUBO: + { + osg::ref_ptr buffer = new LightBuffer(lightManager->getMaxLightsInScene()); + + buffer->setDiffuse(0, light->getDiffuse()); + buffer->setAmbient(0, light->getAmbient()); + buffer->setSpecular(0, light->getSpecular()); + buffer->setPosition(0, light->getPosition()); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + buffer->getData()->setBufferObject(ubo); +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, mode); + + break; + } + } + } + + class DisableLight : public osg::StateAttribute { public: - LightStateAttribute() : mIndex(0) {} - LightStateAttribute(unsigned int index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + DisableLight() : mIndex(0) {} + DisableLight(int index) : mIndex(index) {} + + DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} + + META_StateAttribute(SceneUtil, DisableLight, osg::StateAttribute::LIGHT) + + unsigned int getMember() const override + { + return mIndex; + } - LightStateAttribute(const LightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + bool getModeUsage(ModeUsage& usage) const override + { + usage.usesMode(GL_LIGHT0 + mIndex); + return true; + } + + int compare(const StateAttribute& sa) const override + { + throw std::runtime_error("DisableLight::compare: unimplemented"); + } + + void apply(osg::State& state) const override + { + int lightNum = GL_LIGHT0 + mIndex; + glLightfv(lightNum, GL_AMBIENT, mNullptr.ptr()); + glLightfv(lightNum, GL_DIFFUSE, mNullptr.ptr()); + glLightfv(lightNum, GL_SPECULAR, mNullptr.ptr()); + + LightStateCache* cache = getLightStateCache(state.getContextID()); + cache->lastAppliedLight[mIndex] = nullptr; + } + + private: + size_t mIndex; + osg::Vec4f mNullptr; + }; + + class FFPLightStateAttribute : public osg::StateAttribute + { + public: + FFPLightStateAttribute() : mIndex(0) {} + FFPLightStateAttribute(size_t index, const std::vector>& lights) : mIndex(index), mLights(lights) {} + + FFPLightStateAttribute(const FFPLightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex), mLights(copy.mLights) {} unsigned int getMember() const override @@ -36,19 +371,19 @@ namespace SceneUtil return mIndex; } - bool getModeUsage(ModeUsage & usage) const override + bool getModeUsage(ModeUsage& usage) const override { - for (unsigned int i=0; ilastAppliedLight[i+mIndex]; if (current != mLights[i].get()) @@ -75,29 +410,147 @@ namespace SceneUtil void applyLight(GLenum lightNum, const osg::Light* light) const { - glLightfv( lightNum, GL_AMBIENT, light->getAmbient().ptr() ); - glLightfv( lightNum, GL_DIFFUSE, light->getDiffuse().ptr() ); - glLightfv( lightNum, GL_SPECULAR, light->getSpecular().ptr() ); - glLightfv( lightNum, GL_POSITION, light->getPosition().ptr() ); + glLightfv(lightNum, GL_AMBIENT, light->getAmbient().ptr()); + glLightfv(lightNum, GL_DIFFUSE, light->getDiffuse().ptr()); + glLightfv(lightNum, GL_SPECULAR, light->getSpecular().ptr()); + glLightfv(lightNum, GL_POSITION, light->getPosition().ptr()); // TODO: enable this once spot lights are supported // need to transform SPOT_DIRECTION by the world matrix? - //glLightfv( lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr() ); - //glLightf ( lightNum, GL_SPOT_EXPONENT, light->getSpotExponent() ); - //glLightf ( lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff() ); - glLightf ( lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation() ); - glLightf ( lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation() ); - glLightf ( lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation() ); + //glLightfv(lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr()); + //glLightf(lightNum, GL_SPOT_EXPONENT, light->getSpotExponent()); + //glLightf(lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff()); + glLightf(lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation()); + glLightf(lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation()); + glLightf(lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation()); } private: - unsigned int mIndex; + size_t mIndex; + std::vector> mLights; + }; + + struct StateSetGenerator + { + LightManager* mLightManager; + + virtual ~StateSetGenerator() {} + + virtual osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) = 0; + + virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {} + + osg::Matrix mViewMatrix; + }; + + struct StateSetGeneratorFFP : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + std::vector> lights; + lights.reserve(lightList.size()); + for (size_t i = 0; i < lightList.size(); ++i) + lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum)); + + // the first light state attribute handles the actual state setting for all lights + // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary + // don't use setAttributeAndModes, that does not support light indices! + stateset->setAttribute(new FFPLightStateAttribute(mLightManager->getStartLight(), std::move(lights)), osg::StateAttribute::ON); + + for (size_t i = 0; i < lightList.size(); ++i) + stateset->setMode(GL_LIGHT0 + mLightManager->getStartLight() + i, osg::StateAttribute::ON); + + // need to push some dummy attributes to ensure proper state tracking + // lights need to reset to their default when the StateSet is popped + for (size_t i = 1; i < lightList.size(); ++i) + stateset->setAttribute(mLightManager->getDummies()[i + mLightManager->getStartLight()].get(), osg::StateAttribute::ON); + + return stateset; + } + }; + + struct StateSetGeneratorSingleUBO : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr indices = new osg::IntArray(mLightManager->getMaxLights()); + osg::ref_ptr indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", indices->size()); + int pointCount = 0; + + for (size_t i = 0; i < lightList.size(); ++i) + { + int bufIndex = mLightManager->getLightIndexMap(frameNum)[lightList[i]->mLightSource->getId()]; + indices->at(pointCount++) = bufIndex; + } + indicesUni->setArray(indices); + stateset->addUniform(indicesUni); + stateset->addUniform(new osg::Uniform("PointLightCount", pointCount)); + + return stateset; + } + + // Cached statesets must be revalidated in case the light indices change. There is no actual link between + // a light's ID and the buffer index it will eventually be assigned (or reassigned) to. + void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) override + { + int newCount = 0; + int oldCount; + + auto uOldArray = stateset->getUniform("PointLightIndex"); + auto uOldCount = stateset->getUniform("PointLightCount"); + + uOldCount->get(oldCount); + + // max lights count can change during runtime + oldCount = std::min(mLightManager->getMaxLights(), oldCount); + + auto& lightData = mLightManager->getLightIndexMap(frameNum); + + for (int i = 0; i < oldCount; ++i) + { + auto* lightSource = lightList[i]->mLightSource; + auto it = lightData.find(lightSource->getId()); + if (it != lightData.end()) + uOldArray->setElement(newCount++, it->second); + } - std::vector > mLights; + uOldArray->dirty(); + uOldCount->set(newCount); + } + }; + + struct StateSetGeneratorPerObjectUniform : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr data = mLightManager->generateLightBufferUniform(mLightManager->getSunlightBuffer(frameNum)); + + for (size_t i = 0; i < lightList.size(); ++i) + { + auto* light = lightList[i]->mLightSource->getLight(frameNum); + osg::Matrixf lightMat; + configurePosition(lightMat, light->getPosition() * mViewMatrix); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightList[i]->mLightSource->getRadius()); + + data->setElement(i+1, lightMat); + } + + stateset->addUniform(data); + stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); + + return stateset; + } }; LightManager* findLightManager(const osg::NodePath& path) { - for (unsigned int i=0;i(path[i])) return lightManager; @@ -107,19 +560,19 @@ namespace SceneUtil // Set on a LightSource. Adds the light source to its light manager for the current frame. // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager. - class CollectLightCallback : public osg::NodeCallback + class CollectLightCallback : public NodeCallback { public: CollectLightCallback() - : mLightManager(0) { } + : mLightManager(nullptr) { } CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) - , mLightManager(0) { } + : NodeCallback(copy, copyop) + , mLightManager(nullptr) { } - META_Object(SceneUtil, SceneUtil::CollectLightCallback) + META_Object(SceneUtil, CollectLightCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (!mLightManager) { @@ -139,224 +592,624 @@ namespace SceneUtil }; // Set on a LightManager. Clears the data from the previous frame. - class LightManagerUpdateCallback : public osg::NodeCallback + class LightManagerUpdateCallback : public SceneUtil::NodeCallback { public: - LightManagerUpdateCallback() - { } - - LightManagerUpdateCallback(const LightManagerUpdateCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) - { } - META_Object(SceneUtil, LightManagerUpdateCallback) - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { LightManager* lightManager = static_cast(node); - lightManager->update(); + lightManager->update(nv->getTraversalNumber()); traverse(node, nv); } }; - LightManager::LightManager() + class LightManagerCullCallback : public SceneUtil::NodeCallback + { + public: + void operator()(LightManager* node, osgUtil::CullVisitor* cv) + { + osg::ref_ptr stateset = new osg::StateSet; + + if (node->getLightingMethod() == LightingMethod::SingleUBO) + { + auto buffer = node->getUBOManager()->getLightBuffer(cv->getTraversalNumber()); + +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData()->getBufferObject(), 0, buffer->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); + + if (auto sun = node->getSunlight()) + { + buffer->setCachedSunPos(sun->getPosition()); + buffer->setAmbient(0, sun->getAmbient()); + buffer->setDiffuse(0, sun->getDiffuse()); + buffer->setSpecular(0, sun->getSpecular()); + } + } + else if (node->getLightingMethod() == LightingMethod::PerObjectUniform) + { + if (auto sun = node->getSunlight()) + { + osg::Matrixf lightMat; + configurePosition(lightMat, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); + configureAmbient(lightMat, sun->getAmbient()); + configureDiffuse(lightMat, sun->getDiffuse()); + configureSpecular(lightMat, sun->getSpecular()); + node->setSunlightBuffer(lightMat, cv->getTraversalNumber()); + stateset->addUniform(node->generateLightBufferUniform(lightMat)); + } + } + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + + if (node->getPPLightsBuffer() && cv->getCurrentCamera()->getName() == Constants::SceneCamera) + node->getPPLightsBuffer()->updateCount(cv->getTraversalNumber()); + } + }; + + UBOManager::UBOManager(int lightCount) + : mDummyProgram(new osg::Program) + , mInitLayout(false) + , mDirty({ true, true }) + , mTemplate(new LightBuffer(lightCount)) + { + static const std::string dummyVertSource = generateDummyShader(lightCount); + + // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably + // available, regardless of extensions, until GLSL 140. + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); + + for (size_t i = 0; i < mLightBuffers.size(); ++i) + { + mLightBuffers[i] = new LightBuffer(lightCount); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + ubo->setUsage(GL_STREAM_DRAW); + + mLightBuffers[i]->getData()->setBufferObject(ubo); + } + } + + UBOManager::UBOManager(const UBOManager& copy, const osg::CopyOp& copyop) : osg::StateAttribute(copy,copyop), mDummyProgram(copy.mDummyProgram), mInitLayout(copy.mInitLayout) {} + + void UBOManager::releaseGLObjects(osg::State* state) const + { + mDummyProgram->releaseGLObjects(state); + } + + int UBOManager::compare(const StateAttribute &sa) const + { + throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + } + + void UBOManager::apply(osg::State& state) const + { + unsigned int frame = state.getFrameStamp()->getFrameNumber(); + unsigned int index = frame % 2; + + if (!mInitLayout) + { + mDummyProgram->apply(state); + auto handle = mDummyProgram->getPCP(state)->getHandle(); + auto* ext = state.get(); + + int activeUniformBlocks = 0; + ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); + + // wait until the UBO binding is created + if (activeUniformBlocks > 0) + { + initSharedLayout(ext, handle, frame); + mInitLayout = true; + } + } + else if (mDirty[index]) + { + mDirty[index] = false; + mLightBuffers[index]->configureLayout(mTemplate); + } + + mLightBuffers[index]->uploadCachedSunPos(state.getInitialViewMatrix()); + mLightBuffers[index]->dirty(); + } + + std::string UBOManager::generateDummyShader(int maxLightsInScene) + { + const std::string define = "@maxLightsInScene"; + + std::string shader = R"GLSL( + #version 120 + #extension GL_ARB_uniform_buffer_object : require + struct LightData { + ivec4 packedColors; + vec4 position; + vec4 attenuation; + }; + uniform LightBufferBinding { + LightData LightBuffer[@maxLightsInScene]; + }; + void main() + { + gl_Position = vec4(0.0); + } + )GLSL"; + + shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); + return shader; + } + + void UBOManager::initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const + { + constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; + int totalBlockSize = -1; + int stride = -1; + + ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); + ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); + + std::array names = { + "LightBuffer[0].packedColors", + "LightBuffer[0].position", + "LightBuffer[0].attenuation", + }; + std::vector indices(names.size()); + std::vector offsets(names.size()); + + ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); + ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); + + mTemplate->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } + + LightingMethod LightManager::getLightingMethodFromString(const std::string& value) + { + auto it = lightingMethodSettingMap.find(value); + if (it != lightingMethodSettingMap.end()) + return it->second; + + constexpr const char* fallback = "shaders compatibility"; + Log(Debug::Warning) << "Unknown lighting method '" << value << "', returning fallback '" << fallback << "'"; + return LightingMethod::PerObjectUniform; + } + + std::string LightManager::getLightingMethodString(LightingMethod method) + { + for (const auto& p : lightingMethodSettingMap) + if (p.second == method) + return p.first; + return ""; + } + + LightManager::LightManager(bool ffp) : mStartLight(0) , mLightingMask(~0u) + , mSun(nullptr) + , mPointLightRadiusMultiplier(1.f) + , mPointLightFadeEnd(0.f) + , mPointLightFadeStart(0.f) { + osg::GLExtensions* exts = osg::GLExtensions::Get(0, false); + bool supportsUBO = exts && exts->isUniformBufferObjectSupported; + bool supportsGPU4 = exts && exts->isGpuShader4Supported; + + mSupported[static_cast(LightingMethod::FFP)] = true; + mSupported[static_cast(LightingMethod::PerObjectUniform)] = true; + mSupported[static_cast(LightingMethod::SingleUBO)] = supportsUBO && supportsGPU4; + setUpdateCallback(new LightManagerUpdateCallback); - for (unsigned int i=0; i<8; ++i) - mDummies.push_back(new LightStateAttribute(i, std::vector >())); + + if (ffp) + { + initFFP(ffpMaxLights); + return; + } + + std::string lightingMethodString = Settings::Manager::getString("lighting method", "Shaders"); + auto lightingMethod = LightManager::getLightingMethodFromString(lightingMethodString); + + static bool hasLoggedWarnings = false; + + if (lightingMethod == LightingMethod::SingleUBO && !hasLoggedWarnings) + { + if (!supportsUBO) + Log(Debug::Warning) << "GL_ARB_uniform_buffer_object not supported: switching to shader compatibility lighting mode"; + if (!supportsGPU4) + Log(Debug::Warning) << "GL_EXT_gpu_shader4 not supported: switching to shader compatibility lighting mode"; + hasLoggedWarnings = true; + } + + const int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), + maxLightsLowerLimit, maxLightsUpperLimit); + + if (!supportsUBO || !supportsGPU4 || lightingMethod == LightingMethod::PerObjectUniform) + initPerObjectUniform(targetLights); + else + initSingleUBO(targetLights); + + updateSettings(); + + getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); + + addCullCallback(new LightManagerCullCallback); } LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) : osg::Group(copy, copyop) , mStartLight(copy.mStartLight) , mLightingMask(copy.mLightingMask) + , mSun(copy.mSun) + , mLightingMethod(copy.mLightingMethod) + , mPointLightRadiusMultiplier(copy.mPointLightRadiusMultiplier) + , mPointLightFadeEnd(copy.mPointLightFadeEnd) + , mPointLightFadeStart(copy.mPointLightFadeStart) + , mMaxLights(copy.mMaxLights) + , mPPLightBuffer(copy.mPPLightBuffer) { + } + + LightingMethod LightManager::getLightingMethod() const + { + return mLightingMethod; + } + bool LightManager::usingFFP() const + { + return mLightingMethod == LightingMethod::FFP; + } + + int LightManager::getMaxLights() const + { + return mMaxLights; + } + + void LightManager::setMaxLights(int value) + { + mMaxLights = value; } - void LightManager::setLightingMask(unsigned int mask) + int LightManager::getMaxLightsInScene() const + { + static constexpr int max = 16384 / LightBuffer::queryBlockSize(1); + return max; + } + + Shader::ShaderManager::DefineMap LightManager::getLightDefines() const + { + Shader::ShaderManager::DefineMap defines; + + defines["maxLights"] = std::to_string(getMaxLights()); + defines["maxLightsInScene"] = std::to_string(getMaxLightsInScene()); + defines["lightingMethodFFP"] = getLightingMethod() == LightingMethod::FFP ? "1" : "0"; + defines["lightingMethodPerObjectUniform"] = getLightingMethod() == LightingMethod::PerObjectUniform ? "1" : "0"; + defines["lightingMethodUBO"] = getLightingMethod() == LightingMethod::SingleUBO ? "1" : "0"; + defines["useUBO"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + // exposes bitwise operators + defines["useGPUShader4"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + defines["getLight"] = getLightingMethod() == LightingMethod::FFP ? "gl_LightSource" : "LightBuffer"; + defines["startLight"] = getLightingMethod() == LightingMethod::SingleUBO ? "0" : "1"; + defines["endLight"] = getLightingMethod() == LightingMethod::FFP ? defines["maxLights"] : "PointLightCount"; + + return defines; + } + + void LightManager::processChangedSettings(const Settings::CategorySettingVector& changed) + { + updateSettings(); + } + + void LightManager::updateMaxLights() + { + if (usingFFP()) + return; + + setMaxLights(std::clamp(Settings::Manager::getInt("max lights", "Shaders"), maxLightsLowerLimit, maxLightsUpperLimit)); + + if (getLightingMethod() == LightingMethod::PerObjectUniform) + { + getStateSet()->removeUniform("LightBuffer"); + getStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); + } + + for (auto& cache : mStateSetCache) + cache.clear(); + } + + void LightManager::updateSettings() + { + if (getLightingMethod() == LightingMethod::FFP) + return; + + mPointLightRadiusMultiplier = std::clamp(Settings::Manager::getFloat("light bounds multiplier", "Shaders"), 0.f, 5.f); + + mPointLightFadeEnd = std::max(0.f, Settings::Manager::getFloat("maximum light distance", "Shaders")); + if (mPointLightFadeEnd > 0) + { + mPointLightFadeStart = std::clamp(Settings::Manager::getFloat("light fade start", "Shaders"), 0.f, 1.f); + mPointLightFadeStart = mPointLightFadeEnd * mPointLightFadeStart; + } + } + + void LightManager::initFFP(int targetLights) + { + setLightingMethod(LightingMethod::FFP); + setMaxLights(targetLights); + + for (int i = 0; i < getMaxLights(); ++i) + mDummies.push_back(new FFPLightStateAttribute(i, std::vector>())); + } + + void LightManager::initPerObjectUniform(int targetLights) + { + setLightingMethod(LightingMethod::PerObjectUniform); + setMaxLights(targetLights); + + getOrCreateStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); + } + + void LightManager::initSingleUBO(int targetLights) + { + setLightingMethod(LightingMethod::SingleUBO); + setMaxLights(targetLights); + + mUBOManager = new UBOManager(getMaxLightsInScene()); + getOrCreateStateSet()->setAttributeAndModes(mUBOManager); + } + + void LightManager::setLightingMethod(LightingMethod method) + { + mLightingMethod = method; + switch (method) + { + case LightingMethod::FFP: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::SingleUBO: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::PerObjectUniform: + mStateSetGenerator = std::make_unique(); + break; + } + mStateSetGenerator->mLightManager = this; + } + + void LightManager::setLightingMask(size_t mask) { mLightingMask = mask; } - unsigned int LightManager::getLightingMask() const + size_t LightManager::getLightingMask() const { return mLightingMask; } - void LightManager::update() + void LightManager::setStartLight(int start) + { + mStartLight = start; + + if (!usingFFP()) return; + + // Set default light state to zero + // This is necessary because shaders don't respect glDisable(GL_LIGHTX) so in addition to disabling + // we'll have to set a light state that has no visible effect + for (int i = start; i < getMaxLights(); ++i) + { + osg::ref_ptr defaultLight (new DisableLight(i)); + getOrCreateStateSet()->setAttributeAndModes(defaultLight, osg::StateAttribute::OFF); + } + } + + int LightManager::getStartLight() const { + return mStartLight; + } + + void LightManager::update(size_t frameNum) + { + if (mPPLightBuffer) + mPPLightBuffer->clear(frameNum); + + getLightIndexMap(frameNum).clear(); mLights.clear(); mLightsInViewSpace.clear(); - // do an occasional cleanup for orphaned lights - for (int i=0; i<2; ++i) + // Do an occasional cleanup for orphaned lights. + for (int i = 0; i < 2; ++i) { if (mStateSetCache[i].size() > 5000) mStateSetCache[i].clear(); } } - void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum) + void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum) { LightSourceTransform l; l.mLightSource = lightSource; l.mWorldMatrix = worldMat; - lightSource->getLight(frameNum)->setPosition(osg::Vec4f(worldMat.getTrans().x(), - worldMat.getTrans().y(), - worldMat.getTrans().z(), 1.f)); + osg::Vec3f pos = osg::Vec3f(worldMat.getTrans().x(), worldMat.getTrans().y(), worldMat.getTrans().z()); + lightSource->getLight(frameNum)->setPosition(osg::Vec4f(pos, 1.f)); + mLights.push_back(l); } - /* similar to the boost::hash_combine */ - template - inline void hash_combine(std::size_t& seed, const T& v) + void LightManager::setSunlight(osg::ref_ptr sun) { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + if (usingFFP()) return; + + mSun = sun; } - osg::ref_ptr LightManager::getLightListStateSet(const LightList &lightList, unsigned int frameNum) + osg::ref_ptr LightManager::getSunlight() + { + return mSun; + } + + size_t LightManager::HashLightIdList::operator()(const LightIdList& lightIdList) const { - // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) size_t hash = 0; - for (unsigned int i=0; imLightSource->getId()); + for (size_t i = 0; i < lightIdList.size(); ++i) + Misc::hashCombine(hash, lightIdList[i]); + return hash; + } + + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) + { + if (getLightingMethod() == LightingMethod::PerObjectUniform) + { + mStateSetGenerator->mViewMatrix = *viewMatrix; + return mStateSetGenerator->generate(lightList, frameNum); + } - LightStateSetMap& stateSetCache = mStateSetCache[frameNum%2]; + // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - LightStateSetMap::iterator found = stateSetCache.find(hash); - if (found != stateSetCache.end()) - return found->second; - else + if (getLightingMethod() == LightingMethod::SingleUBO) { - osg::ref_ptr stateset = new osg::StateSet; - std::vector > lights; - lights.reserve(lightList.size()); - for (unsigned int i=0; imLightSource->getLight(frameNum)); + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; - // the first light state attribute handles the actual state setting for all lights - // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary - // don't use setAttributeAndModes, that does not support light indices! - stateset->setAttribute(new LightStateAttribute(mStartLight, std::move(lights)), osg::StateAttribute::ON); + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(id, index); + } + } - for (unsigned int i=0; isetMode(GL_LIGHT0 + mStartLight + i, osg::StateAttribute::ON); + auto& stateSetCache = mStateSetCache[frameNum%2]; - // need to push some dummy attributes to ensure proper state tracking - // lights need to reset to their default when the StateSet is popped - for (unsigned int i=1; isetAttribute(mDummies[i+mStartLight].get(), osg::StateAttribute::ON); + LightIdList lightIdList; + lightIdList.reserve(lightList.size()); + std::transform(lightList.begin(), lightList.end(), std::back_inserter(lightIdList), [] (const LightSourceViewBound* l) { return l->mLightSource->getId(); }); - stateSetCache.emplace(hash, stateset); - return stateset; + auto found = stateSetCache.find(lightIdList); + if (found != stateSetCache.end()) + { + mStateSetGenerator->update(found->second, lightList, frameNum); + return found->second; } - } - const std::vector& LightManager::getLights() const - { - return mLights; + auto stateset = mStateSetGenerator->generate(lightList, frameNum); + stateSetCache.emplace(lightIdList, stateset); + return stateset; } - const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix) + const std::vector& LightManager::getLightsInViewSpace(osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum) { + osg::Camera* camera = cv->getCurrentCamera(); + osg::observer_ptr camPtr (camera); - std::map, LightSourceViewBoundCollection>::iterator it = mLightsInViewSpace.find(camPtr); + auto it = mLightsInViewSpace.find(camPtr); if (it == mLightsInViewSpace.end()) { it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; - for (std::vector::iterator lightIt = mLights.begin(); lightIt != mLights.end(); ++lightIt) + for (const auto& transform : mLights) { - osg::Matrixf worldViewMat = lightIt->mWorldMatrix * (*viewMatrix); - osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), lightIt->mLightSource->getRadius()); + osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); + + float radius = transform.mLightSource->getRadius(); + + osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius); transformBoundingSphere(worldViewMat, viewBound); + if (transform.mLightSource->getLastAppliedFrame() != frameNum && mPointLightFadeEnd != 0.f) + { + const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; + const float viewDelta = viewBound.center().length() - mPointLightFadeStart; + float fade = 1 - std::clamp(viewDelta / fadeDelta, 0.f, 1.f); + if (fade == 0.f) + continue; + + auto* light = transform.mLightSource->getLight(frameNum); + light->setDiffuse(light->getDiffuse() * fade); + transform.mLightSource->setLastAppliedFrame(frameNum); + } + + // remove lights culled by this camera + if (!usingFFP()) + { + viewBound._radius *= 2.f; + if (cv->getModelViewCullingStack().front().isCulled(viewBound)) + continue; + viewBound._radius /= 2.f; + } + viewBound._radius *= mPointLightRadiusMultiplier; LightSourceViewBound l; - l.mLightSource = lightIt->mLightSource; + l.mLightSource = transform.mLightSource; l.mViewBound = viewBound; it->second.push_back(l); } - } - return it->second; - } - class DisableLight : public osg::StateAttribute - { - public: - DisableLight() : mIndex(0) {} - DisableLight(int index) : mIndex(index) {} + const bool fillPPLights = mPPLightBuffer && it->first->getName() == Constants::SceneCamera; - DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} - - osg::Object* cloneType() const override { return new DisableLight(mIndex); } - osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); } - bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast(obj)!=nullptr; } - const char* libraryName() const override { return "SceneUtil"; } - const char* className() const override { return "DisableLight"; } - Type getType() const override { return LIGHT; } + if (fillPPLights || getLightingMethod() == LightingMethod::SingleUBO) + { + auto sorter = [] (const LightSourceViewBound& left, const LightSourceViewBound& right) { + return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2(); + }; - unsigned int getMember() const override - { - return mIndex; - } + std::sort(it->second.begin(), it->second.end(), sorter); - bool getModeUsage(ModeUsage & usage) const override - { - usage.usesMode(GL_LIGHT0 + mIndex); - return true; - } + if (fillPPLights) + { + for (const auto& bound : it->second) + { + if (bound.mLightSource->getEmpty()) + continue; + const auto* light = bound.mLightSource->getLight(frameNum); + if (light->getDiffuse().x() >= 0.f) + mPPLightBuffer->setLight(frameNum, light, bound.mLightSource->getRadius()); + } + } - int compare(const StateAttribute &sa) const override - { - throw std::runtime_error("DisableLight::compare: unimplemented"); + if (it->second.size() > static_cast(getMaxLightsInScene() - 1)) + it->second.resize(getMaxLightsInScene() - 1); + } } - void apply(osg::State& state) const override - { - int lightNum = GL_LIGHT0 + mIndex; - glLightfv( lightNum, GL_AMBIENT, mnullptr.ptr() ); - glLightfv( lightNum, GL_DIFFUSE, mnullptr.ptr() ); - glLightfv( lightNum, GL_SPECULAR, mnullptr.ptr() ); - - LightStateCache* cache = getLightStateCache(state.getContextID()); - cache->lastAppliedLight[mIndex] = nullptr; - } + return it->second; + } - private: - unsigned int mIndex; - osg::Vec4f mnullptr; - }; + void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix) + { + auto* light = lightSource->getLight(frameNum); + auto& buf = getUBOManager()->getLightBuffer(frameNum); + buf->setDiffuse(index, light->getDiffuse()); + buf->setAmbient(index, light->getAmbient()); + buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); + buf->setPosition(index, light->getPosition() * (*viewMatrix)); + } - void LightManager::setStartLight(int start) + osg::ref_ptr LightManager::generateLightBufferUniform(const osg::Matrixf& sun) { - mStartLight = start; + osg::ref_ptr uniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); + uniform->setElement(0, sun); - // Set default light state to zero - // This is necessary because shaders don't respect glDisable(GL_LIGHTX) so in addition to disabling - // we'll have to set a light state that has no visible effect - for (int i=start; i<8; ++i) - { - osg::ref_ptr defaultLight (new DisableLight(i)); - getOrCreateStateSet()->setAttributeAndModes(defaultLight, osg::StateAttribute::OFF); - } + return uniform; } - int LightManager::getStartLight() const + void LightManager::setCollectPPLights(bool enabled) { - return mStartLight; + if (enabled) + mPPLightBuffer = std::make_shared(); + else + mPPLightBuffer = nullptr; } - static int sLightId = 0; - LightSource::LightSource() : mRadius(0.f) + , mActorFade(1.f) + , mLastAppliedFrame(0) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; @@ -365,25 +1218,19 @@ namespace SceneUtil LightSource::LightSource(const LightSource ©, const osg::CopyOp ©op) : osg::Node(copy, copyop) , mRadius(copy.mRadius) + , mActorFade(copy.mActorFade) + , mLastAppliedFrame(copy.mLastAppliedFrame) { mId = sLightId++; - for (int i=0; i<2; ++i) + for (size_t i = 0; i < mLight.size(); ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } - - bool sortLights (const LightManager::LightSourceViewBound* left, const LightManager::LightSourceViewBound* right) + void LightListCallback::operator()(osg::Node *node, osgUtil::CullVisitor *cv) { - return left->mViewBound.center().length2() - left->mViewBound.radius2()*81 < right->mViewBound.center().length2() - right->mViewBound.radius2()*81; - } - - void LightListCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) - { - osgUtil::CullVisitor* cv = static_cast(nv); - bool pushedState = pushLightState(node, cv); - traverse(node, nv); + traverse(node, cv); if (pushedState) cv->popStateSet(); } @@ -401,83 +1248,72 @@ namespace SceneUtil return false; // Possible optimizations: - // - cull list of lights by the camera frustum // - organize lights in a quad tree - // update light list if necessary - // makes sure we don't update it more than once per frame when rendering with multiple cameras - if (mLastFrameNumber != cv->getTraversalNumber()) - { - mLastFrameNumber = cv->getTraversalNumber(); + mLastFrameNumber = cv->getTraversalNumber(); - // Don't use Camera::getViewMatrix, that one might be relative to another camera! - const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); - const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix); + // Don't use Camera::getViewMatrix, that one might be relative to another camera! + const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); + const std::vector& lights = mLightManager->getLightsInViewSpace(cv, viewMatrix, mLastFrameNumber); - // get the node bounds in view space - // NB do not node->getBound() * modelView, that would apply the node's transformation twice - osg::BoundingSphere nodeBound; - osg::Transform* transform = node->asTransform(); - if (transform) - { - for (unsigned int i=0; igetNumChildren(); ++i) - nodeBound.expandBy(transform->getChild(i)->getBound()); - } - else - nodeBound = node->getBound(); - osg::Matrixf mat = *cv->getModelViewMatrix(); - transformBoundingSphere(mat, nodeBound); + // get the node bounds in view space + // NB do not node->getBound() * modelView, that would apply the node's transformation twice + osg::BoundingSphere nodeBound; + osg::Transform* transform = node->asTransform(); + if (transform) + { + for (size_t i = 0; i < transform->getNumChildren(); ++i) + nodeBound.expandBy(transform->getChild(i)->getBound()); + } + else + nodeBound = node->getBound(); + osg::Matrixf mat = *cv->getModelViewMatrix(); + transformBoundingSphere(mat, nodeBound); - mLightList.clear(); - for (unsigned int i=0; i (8 - mLightManager->getStartLight()); + size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); - osg::StateSet* stateset = nullptr; + osg::ref_ptr stateset = nullptr; if (mLightList.size() > maxLights) { - // remove lights culled by this camera LightManager::LightList lightList = mLightList; - for (LightManager::LightList::iterator it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights; ) - { - osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); - osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius*2; - osg::CullingSet& cullingSet = stack.front(); - if (cullingSet.isCulled(bs)) + if (mLightManager->usingFFP()) + { + for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) { - it = lightList.erase(it); - continue; + osg::BoundingSphere bs = (*it)->mViewBound; + bs._radius = bs._radius * 2.0; + if (cv->getModelViewCullingStack().front().isCulled(bs)) + it = lightList.erase(it); + else + ++it; } - else - ++it; } - if (lightList.size() > maxLights) - { - // sort by proximity to camera, then get rid of furthest away lights - std::sort(lightList.begin(), lightList.end(), sortLights); - while (lightList.size() > maxLights) - lightList.pop_back(); - } - stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber()); + // sort by proximity to camera, then get rid of furthest away lights + std::sort(lightList.begin(), lightList.end(), sortLights); + while (lightList.size() > maxLights) + lightList.pop_back(); + stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); } else - stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber()); + stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); cv->pushStateSet(stateset); diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index c370f1b7f0..c69f7a74fb 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -2,13 +2,20 @@ #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H #include +#include +#include +#include #include - #include #include #include +#include + +#include +#include + namespace osgUtil { class CullVisitor; @@ -16,6 +23,78 @@ namespace osgUtil namespace SceneUtil { + class LightBuffer; + struct StateSetGenerator; + + class PPLightBuffer + { + public: + inline static constexpr auto sMaxPPLights = 40; + inline static constexpr auto sMaxPPLightsArraySize = sMaxPPLights * 3; + + PPLightBuffer() + { + for (size_t i = 0; i < 2; ++i) + { + mIndex[i] = 0; + mUniformBuffers[i] = new osg::Uniform(osg::Uniform::FLOAT_VEC4, "omw_PointLights", sMaxPPLightsArraySize); + mUniformCount[i] = new osg::Uniform("omw_PointLightsCount", static_cast(0)); + } + } + + void applyUniforms(size_t frame, osg::StateSet* stateset) + { + size_t frameId = frame % 2; + + if (!stateset->getUniform("omw_PointLights")) + stateset->addUniform(mUniformBuffers[frameId]); + if (!stateset->getUniform("omw_PointLightsCount")) + stateset->addUniform(mUniformCount[frameId]); + + mUniformBuffers[frameId]->dirty(); + mUniformCount[frameId]->dirty(); + } + + void clear(size_t frame) + { + mIndex[frame % 2] = 0; + } + + void setLight(size_t frame, const osg::Light* light, float radius) + { + size_t frameId = frame % 2; + size_t i = mIndex[frameId]; + + if (i >= (sMaxPPLights - 1)) + return; + + i *= 3; + + mUniformBuffers[frameId]->setElement(i + 0, light->getPosition()); + mUniformBuffers[frameId]->setElement(i + 1, light->getDiffuse()); + mUniformBuffers[frameId]->setElement(i + 2, osg::Vec4f(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), radius)); + + mIndex[frameId]++; + } + + void updateCount(size_t frame) + { + size_t frameId = frame % 2; + mUniformCount[frameId]->set(static_cast(mIndex[frameId])); + } + + private: + std::array mIndex; + std::array, 2> mUniformBuffers; + std::array, 2> mUniformCount; + }; + + enum class LightingMethod + { + FFP, + PerObjectUniform, + SingleUBO, + }; /// LightSource managed by a LightManager. /// @par Typically used for point lights. Spot lights are not supported yet. Directional lights affect the whole scene @@ -28,13 +107,19 @@ namespace SceneUtil class LightSource : public osg::Node { // double buffered osg::Light's, since one of them may be in use by the draw thread at any given time - osg::ref_ptr mLight[2]; + std::array, 2> mLight; // LightSource will affect objects within this radius float mRadius; int mId; + float mActorFade; + + unsigned int mLastAppliedFrame; + + bool mEmpty = false; + public: META_Node(SceneUtil, LightSource) @@ -54,10 +139,30 @@ namespace SceneUtil mRadius = radius; } + void setActorFade(float alpha) + { + mActorFade = alpha; + } + + float getActorFade() const + { + return mActorFade; + } + + void setEmpty(bool empty) + { + mEmpty = empty; + } + + bool getEmpty() const + { + return mEmpty; + } + /// Get the osg::Light safe for modification in the given frame. /// @par May be used externally to animate the light's color/attenuation properties, /// and is used internally to synchronize the light's position with the position of the LightSource. - osg::Light* getLight(unsigned int frame) + osg::Light* getLight(size_t frame) { return mLight[frame % 2]; } @@ -77,16 +182,71 @@ namespace SceneUtil { return mId; } + + void setLastAppliedFrame(unsigned int lastAppliedFrame) + { + mLastAppliedFrame = lastAppliedFrame; + } + + unsigned int getLastAppliedFrame() const + { + return mLastAppliedFrame; + } + }; + + class UBOManager : public osg::StateAttribute + { + public: + UBOManager(int lightCount=1); + UBOManager(const UBOManager& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY); + + void releaseGLObjects(osg::State* state) const override; + + int compare(const StateAttribute& sa) const override; + + META_StateAttribute(SceneUtil, UBOManager, osg::StateAttribute::LIGHT) + + void apply(osg::State& state) const override; + + auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + + private: + std::string generateDummyShader(int maxLightsInScene); + void initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const; + + osg::ref_ptr mDummyProgram; + mutable bool mInitLayout; + mutable std::array, 2> mLightBuffers; + mutable std::array mDirty; + osg::ref_ptr mTemplate; }; /// @brief Decorator node implementing the rendering of any number of LightSources that can be anywhere in the subgraph. class LightManager : public osg::Group { public: + static LightingMethod getLightingMethodFromString(const std::string& value); + /// Returns string as used in settings file, or the empty string if the method is undefined + static std::string getLightingMethodString(LightingMethod method); + + struct LightSourceTransform + { + LightSource* mLightSource; + osg::Matrixf mWorldMatrix; + }; + + struct LightSourceViewBound + { + LightSource* mLightSource; + osg::BoundingSphere mViewBound; + }; + + using LightList = std::vector; + using SupportedMethods = std::array; META_Node(SceneUtil, LightManager) - LightManager(); + LightManager(bool ffp = true); LightManager(const LightManager& copy, const osg::CopyOp& copyop); @@ -94,57 +254,113 @@ namespace SceneUtil /// By default, it's ~0u i.e. always on. /// If you have some views that do not require lighting, then set the Camera's cull mask to not include /// the lightingMask for a much faster cull and rendering. - void setLightingMask (unsigned int mask); - - unsigned int getLightingMask() const; + void setLightingMask(size_t mask); + size_t getLightingMask() const; /// Set the first light index that should be used by this manager, typically the number of directional lights in the scene. void setStartLight(int start); - int getStartLight() const; /// Internal use only, called automatically by the LightManager's UpdateCallback - void update(); + void update(size_t frameNum); /// Internal use only, called automatically by the LightSource's UpdateCallback - void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum); + void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum); - struct LightSourceTransform - { - LightSource* mLightSource; - osg::Matrixf mWorldMatrix; - }; + const std::vector& getLightsInViewSpace(osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum); - const std::vector& getLights() const; + osg::ref_ptr getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix); - struct LightSourceViewBound - { - LightSource* mLightSource; - osg::BoundingSphere mViewBound; - }; + void setSunlight(osg::ref_ptr sun); + osg::ref_ptr getSunlight(); - const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix); + bool usingFFP() const; - typedef std::vector LightList; + LightingMethod getLightingMethod() const; - osg::ref_ptr getLightListStateSet(const LightList& lightList, unsigned int frameNum); + int getMaxLights() const; + + int getMaxLightsInScene() const; + + auto& getDummies() { return mDummies; } + + auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; } + + auto& getUBOManager() { return mUBOManager; } + + osg::Matrixf getSunlightBuffer(size_t frameNum) const { return mSunlightBuffers[frameNum%2]; } + void setSunlightBuffer(const osg::Matrixf& buffer, size_t frameNum) { mSunlightBuffers[frameNum%2] = buffer; } + + SupportedMethods getSupportedLightingMethods() { return mSupported; } + + std::map getLightDefines() const; + + void processChangedSettings(const Settings::CategorySettingVector& changed); + + /// Not thread safe, it is the responsibility of the caller to stop/start threading on the viewer + void updateMaxLights(); + + osg::ref_ptr generateLightBufferUniform(const osg::Matrixf& sun); + + // Whether to collect main scene camera points lights into a buffer to be later sent to postprocessing shaders + void setCollectPPLights(bool enabled); + + std::shared_ptr getPPLightsBuffer() { return mPPLightBuffer; } private: - // Lights collected from the scene graph. Only valid during the cull traversal. + void initFFP(int targetLights); + void initPerObjectUniform(int targetLights); + void initSingleUBO(int targetLights); + + void updateSettings(); + + void setLightingMethod(LightingMethod method); + void setMaxLights(int value); + + void updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum, const osg::RefMatrix* viewMatrix); + std::vector mLights; - typedef std::vector LightSourceViewBoundCollection; + using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; - // < Light list hash , StateSet > - typedef std::map > LightStateSetMap; + using LightIdList = std::vector; + struct HashLightIdList + { + size_t operator()(const LightIdList&) const; + }; + using LightStateSetMap = std::unordered_map, HashLightIdList>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; int mStartLight; - unsigned int mLightingMask; + size_t mLightingMask; + + osg::ref_ptr mSun; + + osg::Matrixf mSunlightBuffers[2]; + + // < Light ID , Buffer Index > + using LightIndexMap = std::unordered_map; + LightIndexMap mLightIndexMaps[2]; + + std::unique_ptr mStateSetGenerator; + + osg::ref_ptr mUBOManager; + + LightingMethod mLightingMethod; + + float mPointLightRadiusMultiplier; + float mPointLightFadeEnd; + float mPointLightFadeStart; + + int mMaxLights; + + SupportedMethods mSupported; + + std::shared_ptr mPPLightBuffer; }; /// To receive lighting, objects must be decorated by a LightListCallback. Light list callbacks must be added via @@ -156,7 +372,7 @@ namespace SceneUtil /// starting point is to attach a LightListCallback to each game object's base node. /// @note Not thread safe for CullThreadPerCamera threading mode. /// @note Due to lack of OSG support, the callback does not work on Drawables. - class LightListCallback : public osg::NodeCallback + class LightListCallback : public SceneUtil::NodeCallback { public: LightListCallback() @@ -164,7 +380,7 @@ namespace SceneUtil , mLastFrameNumber(0) {} LightListCallback(const LightListCallback& copy, const osg::CopyOp& copyop) - : osg::Object(copy, copyop), osg::NodeCallback(copy, copyop) + : osg::Object(copy, copyop), SceneUtil::NodeCallback(copy, copyop) , mLightManager(copy.mLightManager) , mLastFrameNumber(0) , mIgnoredLightSources(copy.mIgnoredLightSources) @@ -172,7 +388,7 @@ namespace SceneUtil META_Object(SceneUtil, LightListCallback) - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osgUtil::CullVisitor* nv); bool pushLightState(osg::Node* node, osgUtil::CullVisitor* nv); @@ -180,11 +396,13 @@ namespace SceneUtil private: LightManager* mLightManager; - unsigned int mLastFrameNumber; + size_t mLastFrameNumber; LightManager::LightList mLightList; std::set mIgnoredLightSources; }; + void configureStateSetSunOverride(LightManager* lightManager, const osg::Light* light, osg::StateSet* stateset, int mode = osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } #endif diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index e9be05908e..adf36ff04f 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -2,16 +2,43 @@ #include #include -#include -#include +#include + +#include #include #include "lightmanager.hpp" #include "lightcontroller.hpp" #include "util.hpp" #include "visitor.hpp" -#include "positionattitudetransform.hpp" + +namespace +{ + class CheckEmptyLightVisitor : public osg::NodeVisitor + { + public: + CheckEmptyLightVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {} + + void apply(osg::Drawable& drawable) override + { + if (!mEmpty) + return; + + if (dynamic_cast(&drawable)) + mEmpty = false; + else + traverse(drawable); + } + + void apply(osg::Geometry& geometry) override + { + mEmpty = false; + } + + bool mEmpty = true; + }; +} namespace SceneUtil { @@ -58,35 +85,21 @@ namespace SceneUtil light->setQuadraticAttenuation(quadraticAttenuation); } - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior) + osg::ref_ptr addLight(osg::Group* node, const ESM::Light* esmLight, unsigned int lightMask, bool isExterior) { SceneUtil::FindByNameVisitor visitor("AttachLight"); node->accept(visitor); - osg::Group* attachTo = nullptr; - if (visitor.mFoundNode) - { - attachTo = visitor.mFoundNode; - } - else - { - osg::ComputeBoundsVisitor computeBound; - computeBound.setTraversalMask(~partsysMask); - // We want the bounds of all children of the node, ignoring the node's local transformation - // So do a traverse(), not accept() - computeBound.traverse(*node); - - // PositionAttitudeTransform seems to be slightly faster than MatrixTransform - osg::ref_ptr trans(new SceneUtil::PositionAttitudeTransform); - trans->setPosition(computeBound.getBoundingBox().center()); + osg::Group* attachTo = visitor.mFoundNode ? visitor.mFoundNode : node; + osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1)); + attachTo->addChild(lightSource); - node->addChild(trans); + CheckEmptyLightVisitor emptyVisitor; + node->accept(emptyVisitor); - attachTo = trans; - } + lightSource->setEmpty(emptyVisitor.mEmpty); - osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior); - attachTo->addChild(lightSource); + return lightSource; } osg::ref_ptr createLightSource(const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient) diff --git a/components/sceneutil/lightutil.hpp b/components/sceneutil/lightutil.hpp index 7096c38b20..aabeec58ab 100644 --- a/components/sceneutil/lightutil.hpp +++ b/components/sceneutil/lightutil.hpp @@ -26,13 +26,12 @@ namespace SceneUtil /// @brief Convert an ESM::Light to a SceneUtil::LightSource, and add it to a sub graph. /// @note If the sub graph contains a node named "AttachLight" (case insensitive), then the light is added to that. - /// Otherwise, the light is added in the center of the node's bounds. + /// Otherwise, the light is attached directly to the root node of the subgraph. /// @param node The sub graph to add a light to /// @param esmLight The light definition coming from the game files containing radius, color, flicker, etc. - /// @param partsysMask Node mask to ignore when computing the sub graph's bounding box. /// @param lightMask Mask to assign to the newly created LightSource. /// @param isExterior Is the light outside? May be used for deciding which attenuation settings to use. - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior); + osg::ref_ptr addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int lightMask, bool isExterior); /// @brief Convert an ESM::Light to a SceneUtil::LightSource, and return it. /// @param esmLight The light definition coming from the game files containing radius, color, flicker, etc. diff --git a/components/sceneutil/morphgeometry.cpp b/components/sceneutil/morphgeometry.cpp index 04fd6fb365..3e34e3dedc 100644 --- a/components/sceneutil/morphgeometry.cpp +++ b/components/sceneutil/morphgeometry.cpp @@ -1,6 +1,7 @@ #include "morphgeometry.hpp" #include +#include #include @@ -27,11 +28,19 @@ MorphGeometry::MorphGeometry(const MorphGeometry ©, const osg::CopyOp ©o void MorphGeometry::setSourceGeometry(osg::ref_ptr sourceGeom) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeom; for (unsigned int i=0; i<2; ++i) { + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(*mSourceGeometry, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); const osg::Geometry& from = *mSourceGeometry; osg::Geometry& to = *mGeometry[i]; @@ -95,8 +104,8 @@ void MorphGeometry::accept(osg::PrimitiveFunctor& func) const osg::BoundingBox MorphGeometry::computeBoundingBox() const { bool anyMorphTarget = false; - for (unsigned int i=0; i 0) + for (unsigned int i=1; i(mSourceGeometry->getVertexArray()); - std::vector vertBounds(sourceVerts.size()); + const osg::Vec3Array* sourceVerts = static_cast(mSourceGeometry->getVertexArray()); + if (mMorphTargets.size() != 0) + sourceVerts = mMorphTargets[0].getOffsets(); + std::vector vertBounds(sourceVerts->size()); // Since we don't know what combinations of morphs are being applied we need to keep track of a bounding box for each vertex. // The minimum/maximum of the box is the minimum/maximum offset the vertex can have from its starting position. @@ -123,7 +134,7 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const for (unsigned int i=0; igetTraversalNumber() || !mDirty) + if (mLastFrameNumber == nv->getTraversalNumber() || !mDirty || mMorphTargets.size() == 0) { osg::Geometry& geom = *getGeometry(mLastFrameNumber); nv->pushOntoNodePath(&geom); @@ -160,13 +171,13 @@ void MorphGeometry::cull(osg::NodeVisitor *nv) mLastFrameNumber = nv->getTraversalNumber(); osg::Geometry& geom = *getGeometry(mLastFrameNumber); - const osg::Vec3Array* positionSrc = static_cast(mSourceGeometry->getVertexArray()); + const osg::Vec3Array* positionSrc = mMorphTargets[0].getOffsets(); osg::Vec3Array* positionDst = static_cast(geom.getVertexArray()); assert(positionSrc->size() == positionDst->size()); for (unsigned int vertex=0; vertexsize(); ++vertex) (*positionDst)[vertex] = (*positionSrc)[vertex]; - for (unsigned int i=0; idirty(); -#if OSG_MIN_VERSION_REQUIRED(3, 5, 6) - geom.dirtyGLObjects(); +#if OSG_MIN_VERSION_REQUIRED(3, 5, 10) + geom.osg::Drawable::dirtyGLObjects(); #endif nv->pushOntoNodePath(&geom); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 0411dbc431..3210bf4a4e 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -23,8 +23,12 @@ #include #include #include +#include #include +#include +#include + #include "shadowsbin.hpp" namespace { @@ -275,14 +279,7 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) } #endif // bin has to go inside camera cull or the rendertexture stage will override it - static osg::ref_ptr ss; - if (!ss) - { - ShadowsBinAdder adder("ShadowsBin"); - ss = new osg::StateSet; - ss->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, "ShadowsBin", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS); - } - cv->pushStateSet(ss); + cv->pushStateSet(_vdsm->getOrCreateShadowsBinStateSet()); if (_vdsm->getShadowedScene()) { _vdsm->getShadowedScene()->osg::Group::traverse(*nv); @@ -349,16 +346,16 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) } // namespace -MWShadowTechnique::ComputeLightSpaceBounds::ComputeLightSpaceBounds(osg::Viewport* viewport, const osg::Matrixd& projectionMatrix, osg::Matrixd& viewMatrix) : +MWShadowTechnique::ComputeLightSpaceBounds::ComputeLightSpaceBounds() : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN) { setCullingMode(osg::CullSettings::VIEW_FRUSTUM_CULLING); +} - pushViewport(viewport); - pushProjectionMatrix(new osg::RefMatrix(projectionMatrix)); - pushModelViewMatrix(new osg::RefMatrix(viewMatrix), osg::Transform::ABSOLUTE_RF); - - setName("SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds,AcceptedByComponentsTerrainQuadTreeWorld"); +void MWShadowTechnique::ComputeLightSpaceBounds::reset() +{ + osg::CullStack::reset(); + _bb = osg::BoundingBox(); } void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Node& node) @@ -374,6 +371,11 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Node& node) popCurrentMask(); } +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Group& node) +{ + apply(static_cast(node)); +} + void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Drawable& drawable) { if (isCulled(drawable)) return; @@ -387,12 +389,9 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Drawable& drawable) popCurrentMask(); } -void MWShadowTechnique::ComputeLightSpaceBounds::apply(Terrain::QuadTreeWorld & quadTreeWorld) +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Geometry& drawable) { - // For now, just expand the bounds fully as terrain will fill them up and possible ways to detect which terrain definitely won't cast shadows aren't implemented. - - update(osg::Vec3(-1.0, -1.0, 0.0)); - update(osg::Vec3(1.0, 1.0, 0.0)); + apply(static_cast(drawable)); } void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Billboard&) @@ -417,9 +416,9 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Transform& transform // absolute transforms won't affect a shadow map so their subgraphs should be ignored. if (transform.getReferenceFrame() == osg::Transform::RELATIVE_RF) { - osg::ref_ptr matrix = new osg::RefMatrix(*getModelViewMatrix()); + osg::RefMatrix* matrix = createOrReuseMatrix(*getModelViewMatrix()); transform.computeLocalToWorldMatrix(*matrix, this); - pushModelViewMatrix(matrix.get(), transform.getReferenceFrame()); + pushModelViewMatrix(matrix, transform.getReferenceFrame()); traverse(transform); @@ -428,7 +427,11 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Transform& transform // pop the culling mode. popCurrentMask(); +} +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::MatrixTransform& transform) +{ + apply(static_cast(transform)); } void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Camera&) @@ -492,7 +495,7 @@ void MWShadowTechnique::LightData::setLightData(osg::RefMatrix* lm, const osg::L lightDir.set(-lightPos.x(), -lightPos.y(), -lightPos.z()); lightDir.normalize(); OSG_INFO<<" Directional light, lightPos="<setName("ShadowCamera"); _camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); +#ifndef __APPLE__ // workaround shadow issue on macOS, https://gitlab.com/OpenMW/openmw/-/issues/6057 _camera->setImplicitBufferAttachmentMask(0, 0); - +#endif //_camera->setClearColor(osg::Vec4(1.0f,1.0f,1.0f,1.0f)); _camera->setClearColor(osg::Vec4(0.0f,0.0f,0.0f,0.0f)); @@ -629,6 +633,7 @@ void MWShadowTechnique::ShadowData::releaseGLObjects(osg::State* state) const // Frustum // MWShadowTechnique::Frustum::Frustum(osgUtil::CullVisitor* cv, double minZNear, double maxZFar): + useCustomClipSpace(false), corners(8), faces(6), edges(12) @@ -648,19 +653,40 @@ MWShadowTechnique::Frustum::Frustum(osgUtil::CullVisitor* cv, double minZNear, d OSG_INFO<<"zNear = "<setAttribute(new osg::CullFace(osg::CullFace::FRONT), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + _shadowCastingStateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + } } void SceneUtil::MWShadowTechnique::disableFrontFaceCulling() @@ -875,17 +911,30 @@ void SceneUtil::MWShadowTechnique::disableFrontFaceCulling() _useFrontFaceCulling = false; if (_shadowCastingStateSet) + { + _shadowCastingStateSet->removeAttribute(osg::StateAttribute::CULLFACE); _shadowCastingStateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + } } void SceneUtil::MWShadowTechnique::setupCastingShader(Shader::ShaderManager & shaderManager) { // This can't be part of the constructor as OSG mandates that there be a trivial constructor available - - _castingProgram = new osg::Program(); - _castingProgram->addShader(shaderManager.getShader("shadowcasting_vertex.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::VERTEX)); - _castingProgram->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::FRAGMENT)); + osg::ref_ptr castingVertexShader = shaderManager.getShader("shadowcasting_vertex.glsl", { }, osg::Shader::VERTEX); + osg::ref_ptr exts = osg::GLExtensions::Get(0, false); + std::string useGPUShader4 = exts && exts->isGpuShader4Supported ? "1" : "0"; + for (int alphaFunc = GL_NEVER; alphaFunc <= GL_ALWAYS; ++alphaFunc) + { + auto& program = _castingPrograms[alphaFunc - GL_NEVER]; + program = new osg::Program(); + program->addShader(castingVertexShader); + program->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", { {"alphaFunc", std::to_string(alphaFunc)}, + {"alphaToCoverage", "0"}, + {"adjustCoverage", "1"}, + {"useGPUShader4", useGPUShader4} + }, osg::Shader::FRAGMENT)); + } } MWShadowTechnique::ViewDependentData* MWShadowTechnique::createViewDependentData(osgUtil::CullVisitor* /*cv*/) @@ -904,6 +953,67 @@ MWShadowTechnique::ViewDependentData* MWShadowTechnique::getViewDependentData(os return vdd.release(); } +void SceneUtil::MWShadowTechnique::copyShadowMap(osgUtil::CullVisitor& cv, ViewDependentData* lhs, ViewDependentData* rhs) +{ + // Prepare for rendering shadows using the shadow map owned by rhs. + + // To achieve this i first copy all data that is not specific to this cv's camera and thus read-only, + // trusting openmw and osg won't overwrite that data before this frame is done rendering. + // This works due to the double buffering of CullVisitors by osg, but also requires that cull passes are serialized (relative to one another). + // Then initialize new copies of the data that will be written with view-specific data + // (the stateset and the texgens). + + lhs->_viewDependentShadowMap = rhs->_viewDependentShadowMap; + auto* stateset = lhs->getStateSet(cv.getTraversalNumber()); + stateset->clear(); + lhs->_lightDataList = rhs->_lightDataList; + lhs->_numValidShadows = rhs->_numValidShadows; + + ShadowDataList& sdl = lhs->getShadowDataList(); + ShadowDataList previous_sdl; + previous_sdl.swap(sdl); + for (const auto& rhs_sd : rhs->getShadowDataList()) + { + osg::ref_ptr lhs_sd; + + if (previous_sdl.empty()) + { + OSG_INFO << "Create new ShadowData" << std::endl; + lhs_sd = new ShadowData(lhs); + } + else + { + OSG_INFO << "Taking ShadowData from from of previous_sdl" << std::endl; + lhs_sd = previous_sdl.front(); + previous_sdl.erase(previous_sdl.begin()); + } + lhs_sd->_camera = rhs_sd->_camera; + lhs_sd->_textureUnit = rhs_sd->_textureUnit; + lhs_sd->_texture = rhs_sd->_texture; + sdl.push_back(lhs_sd); + } + + assignTexGenSettings(cv, lhs); + + if (lhs->_numValidShadows > 0) + { + prepareStateSetForRenderingShadow(*lhs, cv.getTraversalNumber()); + } +} + +void SceneUtil::MWShadowTechnique::setCustomFrustumCallback(CustomFrustumCallback* cfc) +{ + _customFrustumCallback = cfc; +} + +void SceneUtil::MWShadowTechnique::assignTexGenSettings(osgUtil::CullVisitor& cv, ViewDependentData* vdd) +{ + for (const auto& sd : vdd->getShadowDataList()) + { + assignTexGenSettings(&cv, sd->_camera, sd->_textureUnit, sd->_texgen); + } +} + void MWShadowTechnique::update(osg::NodeVisitor& nv) { OSG_INFO<<"MWShadowTechnique::update(osg::NodeVisitor& "<<&nv<<")"< dummyState = new osg::StateSet(); + + ShadowSettings* settings = getShadowedScene()->getShadowSettings(); + int baseUnit = settings->getBaseShadowTextureUnit(); + int endUnit = baseUnit + settings->getNumShadowMapsPerLight(); + for (int i = baseUnit; i < endUnit; ++i) + { + dummyState->setTextureAttributeAndModes(i, _fallbackShadowMapTexture, osg::StateAttribute::ON); + dummyState->addUniform(new osg::Uniform(("shadowTexture" + std::to_string(i - baseUnit)).c_str(), i)); + dummyState->addUniform(new osg::Uniform(("shadowTextureUnit" + std::to_string(i - baseUnit)).c_str(), i)); + } + + cv.pushStateSet(dummyState); + } + _shadowedScene->osg::Group::traverse(cv); + + if (mSetDummyStateWhenDisabled) + cv.popStateSet(); + return; } @@ -973,9 +1105,9 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) } // 1. Traverse main scene graph - cv.pushStateSet( _shadowRecievingPlaceholderStateSet.get() ); - - osg::ref_ptr decoratorStateGraph = cv.getCurrentStateGraph(); + auto* shadowReceiverStateSet = vdd->getStateSet(cv.getTraversalNumber()); + shadowReceiverStateSet->clear(); + cv.pushStateSet(shadowReceiverStateSet); cullShadowReceivingScene(&cv); @@ -988,7 +1120,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // are all done correctly. cv.computeNearPlane(); } - + // clamp the minZNear and maxZFar to those provided by ShadowSettings maxZFar = osg::minimum(settings->getMaximumShadowMapDistance(),maxZFar); if (minZNear>maxZFar) minZNear = maxZFar*settings->getMinimumShadowMapNearFarRatio(); @@ -999,6 +1131,36 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) cv.setNearFarRatio(minZNear / maxZFar); Frustum frustum(&cv, minZNear, maxZFar); + if (_customFrustumCallback) + { + OSG_INFO << "Calling custom frustum callback" << std::endl; + osgUtil::CullVisitor* sharedFrustumHint = nullptr; + _customClipSpace.init(); + _customFrustumCallback->operator()(cv, _customClipSpace, sharedFrustumHint); + frustum.setCustomClipSpace(_customClipSpace); + if (sharedFrustumHint) + { + // user hinted another view shares its frustum + std::lock_guard lock(_viewDependentDataMapMutex); + auto itr = _viewDependentDataMap.find(sharedFrustumHint); + if (itr != _viewDependentDataMap.end()) + { + OSG_INFO << "User provided a valid shared frustum hint, re-using previously generated shadow map" << std::endl; + + copyShadowMap(cv, vdd, itr->second); + + // return compute near far mode back to it's original settings + cv.setComputeNearFarMode(cachedNearFarMode); + return; + } + else + { + OSG_INFO << "User provided a shared frustum hint, but it was not valid." << std::endl; + } + } + } + + frustum.init(); if (_debugHud) { osg::ref_ptr vertexArray = new osg::Vec3Array(); @@ -1018,7 +1180,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) reducedNear = minZNear; reducedFar = maxZFar; } - + // return compute near far mode back to it's original settings cv.setComputeNearFarMode(cachedNearFarMode); @@ -1073,12 +1235,17 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // if we are using multiple shadow maps and CastShadowTraversalMask is being used // traverse the scene to compute the extents of the objects - if (/*numShadowMapsPerLight>1 &&*/ _shadowedScene->getCastsShadowTraversalMask()!=0xffffffff) + if (/*numShadowMapsPerLight>1 &&*/ (_shadowedScene->getCastsShadowTraversalMask() & _worldMask) == 0) { // osg::ElapsedTime timer; osg::ref_ptr viewport = new osg::Viewport(0,0,2048,2048); - ComputeLightSpaceBounds clsb(viewport.get(), projectionMatrix, viewMatrix); + if (!_clsb) _clsb = new ComputeLightSpaceBounds; + ComputeLightSpaceBounds& clsb = *_clsb; + clsb.reset(); + clsb.pushViewport(viewport); + clsb.pushProjectionMatrix(new osg::RefMatrix(projectionMatrix)); + clsb.pushModelViewMatrix(new osg::RefMatrix(viewMatrix), osg::Transform::ABSOLUTE_RF); clsb.setTraversalMask(_shadowedScene->getCastsShadowTraversalMask()); osg::Matrixd invertModelView; @@ -1092,6 +1259,12 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) _shadowedScene->accept(clsb); + clsb.popCullingSet(); + + clsb.popModelViewMatrix(); + clsb.popProjectionMatrix(); + clsb.popViewport(); + // OSG_NOTICE<<"Extents of LightSpace "< validRegionUniform; - for (auto uniform : _uniforms[cv.getTraversalNumber() % 2]) + for (const auto & uniform : _uniforms[cv.getTraversalNumber() % 2]) { if (uniform->getName() == validRegionUniformName) validRegionUniform = uniform; @@ -1371,7 +1544,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) vdsmCallback->getProjectionMatrix()->set(camera->getProjectionMatrix()); } } - + // 4.4 compute main scene graph TexGen + uniform settings + setup state // assignTexGenSettings(&cv, camera.get(), textureUnit, sd->_texgen.get()); @@ -1400,9 +1573,11 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) } } + vdd->setNumValidShadows(numValidShadows); + if (numValidShadows>0) { - decoratorStateGraph->setStateSet(selectStateSetForRenderingShadow(*vdd, cv.getTraversalNumber())); + prepareStateSetForRenderingShadow(*vdd, cv.getTraversalNumber()); } // OSG_NOTICE<<"End of shadow setup Projection matrix "<<*cv.getProjectionMatrix()<setWrap(osg::Texture2D::WRAP_T,osg::Texture2D::REPEAT); _fallbackShadowMapTexture->setFilter(osg::Texture2D::MIN_FILTER,osg::Texture2D::NEAREST); _fallbackShadowMapTexture->setFilter(osg::Texture2D::MAG_FILTER,osg::Texture2D::NEAREST); + _fallbackShadowMapTexture->setShadowComparison(true); + _fallbackShadowMapTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); } - if (!_castingProgram) + if (!_castingPrograms[GL_ALWAYS - GL_NEVER]) OSG_NOTICE << "Shadow casting shader has not been set up. Remember to call setupCastingShader(Shader::ShaderManager &)" << std::endl; - _shadowCastingStateSet->setAttributeAndModes(_castingProgram, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + // Always use the GL_ALWAYS shader as the shadows bin will change it if necessary + _shadowCastingStateSet->setAttributeAndModes(_castingPrograms[GL_ALWAYS - GL_NEVER], osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); // The casting program uses a sampler, so to avoid undefined behaviour, we must bind a dummy texture in case no other is supplied _shadowCastingStateSet->setTextureAttributeAndModes(0, _fallbackBaseTexture.get(), osg::StateAttribute::ON); _shadowCastingStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); _shadowCastingStateSet->addUniform(new osg::Uniform("alphaTestShadows", false)); osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(true); + osg::ref_ptr clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::NEGATIVE_ONE_TO_ONE); + _shadowCastingStateSet->setAttribute(clipcontrol, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); _shadowCastingStateSet->setAttribute(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); _shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON); @@ -1601,7 +1781,14 @@ osg::Polytope MWShadowTechnique::computeLightViewFrustumPolytope(Frustum& frustu OSG_INFO<<"computeLightViewFrustumPolytope()"< Vertices; - typedef std::pair< osg::Vec3d, osg::Vec3d > Edge; - typedef std::list< Edge > Edges; + typedef std::pair Edge; + typedef std::vector Edges; + typedef std::vector VertexSet; Edges _edges; @@ -1892,20 +2080,20 @@ struct ConvexHull void setToFrustum(MWShadowTechnique::Frustum& frustum) { - _edges.push_back( Edge(frustum.corners[0],frustum.corners[1]) ); - _edges.push_back( Edge(frustum.corners[1],frustum.corners[2]) ); - _edges.push_back( Edge(frustum.corners[2],frustum.corners[3]) ); - _edges.push_back( Edge(frustum.corners[3],frustum.corners[0]) ); + _edges.emplace_back(frustum.corners[0], frustum.corners[1]); + _edges.emplace_back(frustum.corners[1], frustum.corners[2]); + _edges.emplace_back(frustum.corners[2], frustum.corners[3]); + _edges.emplace_back(frustum.corners[3], frustum.corners[0]); - _edges.push_back( Edge(frustum.corners[4],frustum.corners[5]) ); - _edges.push_back( Edge(frustum.corners[5],frustum.corners[6]) ); - _edges.push_back( Edge(frustum.corners[6],frustum.corners[7]) ); - _edges.push_back( Edge(frustum.corners[7],frustum.corners[4]) ); + _edges.emplace_back(frustum.corners[4], frustum.corners[5]); + _edges.emplace_back(frustum.corners[5], frustum.corners[6]); + _edges.emplace_back(frustum.corners[6], frustum.corners[7]); + _edges.emplace_back(frustum.corners[7], frustum.corners[4]); - _edges.push_back( Edge(frustum.corners[0],frustum.corners[4]) ); - _edges.push_back( Edge(frustum.corners[1],frustum.corners[5]) ); - _edges.push_back( Edge(frustum.corners[2],frustum.corners[6]) ); - _edges.push_back( Edge(frustum.corners[3],frustum.corners[7]) ); + _edges.emplace_back(frustum.corners[0], frustum.corners[4]); + _edges.emplace_back(frustum.corners[1], frustum.corners[5]); + _edges.emplace_back(frustum.corners[2], frustum.corners[6]); + _edges.emplace_back(frustum.corners[3], frustum.corners[7]); } struct ConvexHull2D @@ -1919,22 +2107,22 @@ struct ConvexHull } // Calculates the 2D convex hull and returns it as a vector containing the points in CCW order with the first and last point being the same. - static std::vector convexHull(std::set &P) + static Vertices convexHull(const VertexSet &P) { size_t n = P.size(), k = 0; if (n <= 3) - return std::vector(P.cbegin(), P.cend()); + return Vertices(P.cbegin(), P.cend()); - std::vector H(2 * n); + Vertices H(2 * n); // Points are already sorted in a std::set // Build lower hull - for (auto pItr = P.cbegin(); pItr != P.cend(); ++pItr) + for(const auto& vert : P) { - while (k >= 2 && cross(H[k - 2], H[k - 1], *pItr) <= 0) + while (k >= 2 && cross(H[k - 2], H[k - 1], vert) <= 0) k--; - H[k++] = *pItr; + H[k++] = vert; } // Build upper hull @@ -1951,22 +2139,22 @@ struct ConvexHull } }; - Vertices findInternalEdges(osg::Vec3d mainVertex, Vertices connectedVertices) + Vertices findInternalEdges(const osg::Vec3d& mainVertex, const Vertices& connectedVertices) { Vertices internalEdgeVertices; - for (auto vertex : connectedVertices) + for (const auto& vertex : connectedVertices) { osg::Matrixd matrix; osg::Vec3d dir = vertex - mainVertex; matrix.makeLookAt(mainVertex, vertex, dir.z() == 0 ? osg::Vec3d(0, 0, 1) : osg::Vec3d(1, 0, 0)); Vertices testVertices; - for (auto testVertex : connectedVertices) + for (const auto& testVertex : connectedVertices) { if (vertex != testVertex) testVertices.push_back(testVertex); } std::vector bearings; - for (auto testVertex : testVertices) + for (const auto& testVertex : testVertices) { osg::Vec3d transformedVertex = testVertex * matrix; bearings.push_back(atan2(transformedVertex.y(), transformedVertex.x())); @@ -1991,112 +2179,109 @@ struct ConvexHull void extendTowardsNegativeZ() { - typedef std::set VertexSet; - // Collect the set of vertices VertexSet vertices; - for (Edge edge : _edges) + for (const Edge& edge : _edges) { - vertices.insert(edge.first); - vertices.insert(edge.second); + vertices.emplace_back(edge.first); + vertices.emplace_back(edge.second); } + // Sort and make unique. + std::sort(vertices.begin(), vertices.end()); + vertices.erase(std::unique(vertices.begin(), vertices.end()), vertices.end()); + if (vertices.size() == 0) return; // Get the vertices contributing to the 2D convex hull Vertices extremeVertices = ConvexHull2D::convexHull(vertices); - VertexSet extremeVerticesSet(extremeVertices.cbegin(), extremeVertices.cend()); // Add their extrusions to the final edge collection // We extrude as far as -1.5 as the coordinate space shouldn't ever put any shadow casters further than -1.0 Edges finalEdges; // Add edges towards -Z - for (auto vertex : extremeVertices) - finalEdges.push_back(Edge(vertex, osg::Vec3d(vertex.x(), vertex.y(), -1.5))); + for (const auto& vertex : extremeVertices) + finalEdges.emplace_back(vertex, osg::Vec3d(vertex.x(), vertex.y(), -1.5)); // Add edge loop to 'seal' the hull for (auto itr = extremeVertices.cbegin(); itr != extremeVertices.cend() - 1; ++itr) - finalEdges.push_back(Edge(osg::Vec3d(itr->x(), itr->y(), -1.5), osg::Vec3d((itr + 1)->x(), (itr + 1)->y(), -1.5))); + finalEdges.emplace_back(osg::Vec3d(itr->x(), itr->y(), -1.5), osg::Vec3d((itr + 1)->x(), (itr + 1)->y(), -1.5)); // The convex hull algorithm we are using sometimes places a point at both ends of the vector, so we don't always need to add the last edge separately. if (extremeVertices.front() != extremeVertices.back()) - finalEdges.push_back(Edge(osg::Vec3d(extremeVertices.front().x(), extremeVertices.front().y(), -1.5), osg::Vec3d(extremeVertices.back().x(), extremeVertices.back().y(), -1.5))); + finalEdges.emplace_back(osg::Vec3d(extremeVertices.front().x(), extremeVertices.front().y(), -1.5), osg::Vec3d(extremeVertices.back().x(), extremeVertices.back().y(), -1.5)); // Remove internal edges connected to extreme vertices - for (auto vertex : extremeVertices) + for (const auto& vertex : extremeVertices) { Vertices connectedVertices; - for (Edge edge : _edges) + for (const Edge& edge : _edges) { if (edge.first == vertex) connectedVertices.push_back(edge.second); else if (edge.second == vertex) connectedVertices.push_back(edge.first); } - connectedVertices.push_back(osg::Vec3d(vertex.x(), vertex.y(), -1.5)); + connectedVertices.emplace_back(vertex.x(), vertex.y(), -1.5); Vertices unwantedEdgeEnds = findInternalEdges(vertex, connectedVertices); - for (auto edgeEnd : unwantedEdgeEnds) + for (const auto& edgeEnd : unwantedEdgeEnds) { - for (auto itr = _edges.begin(); itr != _edges.end();) - { - if (*itr == Edge(vertex, edgeEnd)) + const auto edgeA = Edge(vertex, edgeEnd); + const auto edgeB = Edge(edgeEnd, vertex); + _edges.erase(std::remove_if(_edges.begin(), _edges.end(), [&](const auto& elem) { - itr = _edges.erase(itr); - break; - } - else if (*itr == Edge(edgeEnd, vertex)) - { - itr = _edges.erase(itr); - break; - } - else - ++itr; - } + return elem == edgeA || elem == edgeB; + }), _edges.end()); } } // Gather connected vertices - VertexSet unprocessedConnectedVertices(extremeVertices.begin(), extremeVertices.end()); + VertexSet unprocessedConnectedVertices = std::move(extremeVertices); + VertexSet connectedVertices; - while (unprocessedConnectedVertices.size() > 0) + const auto containsVertex = [&](const auto& vert) + { + return std::find(connectedVertices.begin(), connectedVertices.end(), vert) != connectedVertices.end(); + }; + + while (!unprocessedConnectedVertices.empty()) { - osg::Vec3d vertex = *unprocessedConnectedVertices.begin(); - unprocessedConnectedVertices.erase(unprocessedConnectedVertices.begin()); - connectedVertices.insert(vertex); - for (Edge edge : _edges) + osg::Vec3d vertex = unprocessedConnectedVertices.back(); + unprocessedConnectedVertices.pop_back(); + + connectedVertices.emplace_back(vertex); + for (const Edge& edge : _edges) { osg::Vec3d otherEnd; if (edge.first == vertex) otherEnd = edge.second; else if (edge.second == vertex) - otherEnd - edge.first; + otherEnd = edge.first; else continue; - if (connectedVertices.count(otherEnd)) + if (containsVertex(otherEnd)) continue; - unprocessedConnectedVertices.insert(otherEnd); + unprocessedConnectedVertices.emplace_back(otherEnd); } } - for (Edge edge : _edges) + for (const Edge& edge : _edges) { - if (connectedVertices.count(edge.first) || connectedVertices.count(edge.second)) + if (containsVertex(edge.first) || containsVertex(edge.second)) finalEdges.push_back(edge); } - _edges = finalEdges; + _edges = std::move(finalEdges); } void transform(const osg::Matrixd& m) { - for(Edges::iterator itr = _edges.begin(); - itr != _edges.end(); - ++itr) + for (auto& edge : _edges) { - itr->first = itr->first * m; - itr->second = itr->second * m; + edge.first = edge.first * m; + edge.second = edge.second * m; } } @@ -2105,18 +2290,14 @@ struct ConvexHull Vertices intersections; // OSG_NOTICE<<"clip("<=0.0 && d1>=0.0) { @@ -2153,15 +2334,15 @@ struct ConvexHull if (intersections.size() == 2) { - _edges.push_back( Edge(intersections[0], intersections[1]) ); + _edges.emplace_back(intersections[0], intersections[1]); return; } if (intersections.size() == 3) { - _edges.push_back( Edge(intersections[0], intersections[1]) ); - _edges.push_back( Edge(intersections[1], intersections[2]) ); - _edges.push_back( Edge(intersections[2], intersections[0]) ); + _edges.emplace_back(intersections[0], intersections[1]); + _edges.emplace_back(intersections[1], intersections[2]); + _edges.emplace_back(intersections[2], intersections[0]); return; } @@ -2179,11 +2360,9 @@ struct ConvexHull up.normalize(); osg::Vec3d center; - for(Vertices::iterator itr = intersections.begin(); - itr != intersections.end(); - ++itr) + for(auto& vertex : intersections) { - center += *itr; + center += vertex; center.x() = osg::maximum(center.x(), -dbl_max); center.y() = osg::maximum(center.y(), -dbl_max); @@ -2198,11 +2377,9 @@ struct ConvexHull typedef std::map>> VertexMap; VertexMap vertexMap; - for(Vertices::iterator itr = intersections.begin(); - itr != intersections.end(); - ++itr) + for (const auto& vertex : intersections) { - osg::Vec3d dv = (*itr-center); + osg::Vec3d dv = vertex - center; double h = dv * side; double v = dv * up; double angle = atan2(h,v); @@ -2223,20 +2400,18 @@ struct ConvexHull auto listItr = vertexMap[angle].begin(); while (listItr != vertexMap[angle].end() && listItr->second < sortValue) ++listItr; - vertexMap[angle].insert(listItr, std::make_pair(*itr, sortValue)); + vertexMap[angle].emplace(listItr, std::make_pair(vertex, sortValue)); } else - vertexMap[angle].push_back(std::make_pair(*itr, sortValue)); + vertexMap[angle].emplace_back(vertex, sortValue); } osg::Vec3d previous_v = vertexMap.rbegin()->second.back().first; - for(VertexMap::iterator itr = vertexMap.begin(); - itr != vertexMap.end(); - ++itr) + for (auto itr = vertexMap.begin(); itr != vertexMap.end(); ++itr) { - for (auto vertex : itr->second) + for (const auto& vertex : itr->second) { - _edges.push_back(Edge(previous_v, vertex.first)); + _edges.emplace_back(previous_v, vertex.first); previous_v = vertex.first; } } @@ -2247,24 +2422,19 @@ struct ConvexHull void clip(const osg::Polytope& polytope) { const osg::Polytope::PlaneList& planes = polytope.getPlaneList(); - for(osg::Polytope::PlaneList::const_iterator itr = planes.begin(); - itr != planes.end(); - ++itr) + for(const auto& plane : planes) { - clip(*itr); + clip(plane); } } double min(unsigned int index) const { double m = dbl_max; - for(Edges::const_iterator itr = _edges.begin(); - itr != _edges.end(); - ++itr) + for(const auto& edge : _edges) { - const Edge& edge = *itr; - if (edge.first[index]m) m = edge.first[index]; - if (edge.second[index]>m) m = edge.second[index]; + if (edge.first[index] > m) m = edge.first[index]; + if (edge.second[index] > m) m = edge.second[index]; } return m; } @@ -2288,19 +2455,15 @@ struct ConvexHull double m = dbl_max; osg::Vec3d delta; double ratio; - for(Edges::const_iterator itr = _edges.begin(); - itr != _edges.end(); - ++itr) + for (const auto& edge : _edges) { - const Edge& edge = *itr; - - delta = edge.first-eye; - ratio = delta[index]/delta[1]; - if (ratiom) m = ratio; + delta = edge.first - eye; + ratio = delta[index] / delta[1]; + if (ratio > m) m = ratio; - delta = edge.second-eye; - ratio = delta[index]/delta[1]; - if (ratio>m) m = ratio; + delta = edge.second - eye; + ratio = delta[index] / delta[1]; + if (ratio > m) m = ratio; } return m; } void output(std::ostream& out) { - out<<"ConvexHull"< stateset = vdd.getStateSet(traversalNumber); @@ -3210,3 +3366,18 @@ void SceneUtil::MWShadowTechnique::DebugHUD::addAnotherShadowMap() for(auto& uniformVector : mFrustumUniforms) uniformVector.push_back(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "transform")); } + +osg::ref_ptr SceneUtil::MWShadowTechnique::getOrCreateShadowsBinStateSet() +{ + if (_shadowsBinStateSet == nullptr) + { + if (_shadowsBin == nullptr) + { + _shadowsBin = new ShadowsBin(_castingPrograms); + osgUtil::RenderBin::addRenderBinPrototype(_shadowsBinName, _shadowsBin); + } + _shadowsBinStateSet = new osg::StateSet; + _shadowsBinStateSet->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, _shadowsBinName, osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS); + } + return _shadowsBinStateSet; +} diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp index a7208cfa6d..04355b5729 100644 --- a/components/sceneutil/mwshadowtechnique.hpp +++ b/components/sceneutil/mwshadowtechnique.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -31,7 +32,6 @@ #include #include -#include namespace SceneUtil { @@ -67,7 +67,7 @@ namespace SceneUtil { virtual void enableShadows(); - virtual void disableShadows(); + virtual void disableShadows(bool setDummyState = false); virtual void enableDebugHUD(); @@ -90,38 +90,44 @@ namespace SceneUtil { class ComputeLightSpaceBounds : public osg::NodeVisitor, public osg::CullStack { public: - ComputeLightSpaceBounds(osg::Viewport* viewport, const osg::Matrixd& projectionMatrix, osg::Matrixd& viewMatrix); + ComputeLightSpaceBounds(); - void apply(osg::Node& node) override; + void apply(osg::Node& node) override final; + void apply(osg::Group& node) override; - void apply(osg::Drawable& drawable) override; - - void apply(Terrain::QuadTreeWorld& quadTreeWorld); + void apply(osg::Drawable& drawable) override final; + void apply(osg::Geometry& drawable) override; void apply(osg::Billboard&) override; void apply(osg::Projection&) override; - void apply(osg::Transform& transform) override; + void apply(osg::Transform& transform) override final; + void apply(osg::MatrixTransform& transform) override; void apply(osg::Camera&) override; - using osg::NodeVisitor::apply; - void updateBound(const osg::BoundingBox& bb); void update(const osg::Vec3& v); + void reset() override; + osg::BoundingBox _bb; }; struct Frustum { Frustum(osgUtil::CullVisitor* cv, double minZNear, double maxZFar); + void setCustomClipSpace(const osg::BoundingBoxd& clipCornersOverride); + void init(); osg::Matrixd projectionMatrix; osg::Matrixd modelViewMatrix; + bool useCustomClipSpace; + osg::BoundingBoxd customClipSpace; + typedef std::vector Vertices; Vertices corners; @@ -139,6 +145,18 @@ namespace SceneUtil { osg::Vec3d frustumCenterLine; }; + /** Custom frustum callback allowing the application to request shadow maps covering a + * different furstum than the camera normally would cover, by customizing the corners of the clip space. */ + struct CustomFrustumCallback : osg::Referenced + { + /** The callback operator. + * Output the custum frustum to the boundingBox variable. + * If sharedFrustumHint is set to a valid cull visitor, the shadow maps of that cull visitor will be re-used instead of recomputing new shadow maps + * Note that the customClipSpace bounding box will be uninitialized when this operator is called. If it is not initalized, or a valid shared frustum hint set, + * the resulting shadow map may be invalid. */ + virtual void operator()(osgUtil::CullVisitor& cv, osg::BoundingBoxd& customClipSpace, osgUtil::CullVisitor*& sharedFrustumHint) = 0; + }; + // forward declare class ViewDependentData; @@ -196,7 +214,12 @@ namespace SceneUtil { virtual void releaseGLObjects(osg::State* = 0) const; + unsigned int numValidShadows(void) const { return _numValidShadows; } + + void setNumValidShadows(unsigned int numValidShadows) { _numValidShadows = numValidShadows; } + protected: + friend class MWShadowTechnique; virtual ~ViewDependentData() {} MWShadowTechnique* _viewDependentShadowMap; @@ -205,13 +228,19 @@ namespace SceneUtil { LightDataList _lightDataList; ShadowDataList _shadowDataList; + + unsigned int _numValidShadows; }; virtual ViewDependentData* createViewDependentData(osgUtil::CullVisitor* cv); ViewDependentData* getViewDependentData(osgUtil::CullVisitor* cv); + void copyShadowMap(osgUtil::CullVisitor& cv, ViewDependentData* lhs, ViewDependentData* rhs); + void setCustomFrustumCallback(CustomFrustumCallback* cfc); + + void assignTexGenSettings(osgUtil::CullVisitor& cv, ViewDependentData* vdd); virtual void createShaders(); @@ -231,14 +260,22 @@ namespace SceneUtil { virtual void cullShadowCastingScene(osgUtil::CullVisitor* cv, osg::Camera* camera) const; - virtual osg::StateSet* selectStateSetForRenderingShadow(ViewDependentData& vdd, unsigned int traversalNumber) const; + virtual osg::StateSet* prepareStateSetForRenderingShadow(ViewDependentData& vdd, unsigned int traversalNumber) const; + + void setWorldMask(unsigned int worldMask) { _worldMask = worldMask; } + + osg::ref_ptr getOrCreateShadowsBinStateSet(); protected: virtual ~MWShadowTechnique(); + osg::ref_ptr _clsb; + typedef std::map< osgUtil::CullVisitor*, osg::ref_ptr > ViewDependentDataMap; mutable std::mutex _viewDependentDataMapMutex; ViewDependentDataMap _viewDependentDataMap; + osg::ref_ptr _customFrustumCallback; + osg::BoundingBoxd _customClipSpace; osg::ref_ptr _shadowRecievingPlaceholderStateSet; @@ -252,6 +289,7 @@ namespace SceneUtil { osg::ref_ptr _program; bool _enableShadows; + bool mSetDummyStateWhenDisabled; double _splitPointUniformLogRatio = 0.5; double _splitPointDeltaBias = 0.0; @@ -263,6 +301,8 @@ namespace SceneUtil { float _shadowFadeStart = 0.0; + unsigned int _worldMask = ~0u; + class DebugHUD final : public osg::Referenced { public: @@ -287,7 +327,10 @@ namespace SceneUtil { }; osg::ref_ptr _debugHud; - osg::ref_ptr _castingProgram; + std::array, GL_ALWAYS - GL_NEVER + 1> _castingPrograms; + const std::string _shadowsBinName = "ShadowsBin_" + std::to_string(reinterpret_cast(this)); + osg::ref_ptr _shadowsBin; + osg::ref_ptr _shadowsBinStateSet; }; } diff --git a/components/sceneutil/navmesh.cpp b/components/sceneutil/navmesh.cpp index aeb1779bd6..a8a9fbac5f 100644 --- a/components/sceneutil/navmesh.cpp +++ b/components/sceneutil/navmesh.cpp @@ -1,22 +1,299 @@ #include "navmesh.hpp" #include "detourdebugdraw.hpp" +#include "depth.hpp" #include #include #include +#include +#include + +#include + +namespace +{ + // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L26-L38 + float distancePtLine2d(const float* pt, const float* p, const float* q) + { + float pqx = q[0] - p[0]; + float pqz = q[2] - p[2]; + float dx = pt[0] - p[0]; + float dz = pt[2] - p[2]; + float d = pqx*pqx + pqz*pqz; + float t = pqx*dx + pqz*dz; + if (d != 0) t /= d; + dx = p[0] + t*pqx - pt[0]; + dz = p[2] + t*pqz - pt[2]; + return dx*dx + dz*dz; + } + + // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L40-L118 + void drawPolyBoundaries(duDebugDraw* dd, const dtMeshTile* tile, const unsigned int col, + const float linew, bool inner) + { + static const float thr = 0.01f*0.01f; + + dd->begin(DU_DRAW_LINES, linew); + + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + + if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) continue; + + const dtPolyDetail* pd = &tile->detailMeshes[i]; + + for (int j = 0, nj = (int)p->vertCount; j < nj; ++j) + { + unsigned int c = col; + if (inner) + { + if (p->neis[j] == 0) continue; + if (p->neis[j] & DT_EXT_LINK) + { + bool con = false; + for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next) + { + if (tile->links[k].edge == j) + { + con = true; + break; + } + } + if (con) + c = duRGBA(255,255,255,48); + else + c = duRGBA(0,0,0,48); + } + else + c = duRGBA(0,48,64,32); + } + else + { + if (p->neis[j] != 0) continue; + } + + const float* v0 = &tile->verts[p->verts[j]*3]; + const float* v1 = &tile->verts[p->verts[(j+1) % nj]*3]; + + // Draw detail mesh edges which align with the actual poly edge. + // This is really slow. + for (int k = 0; k < pd->triCount; ++k) + { + const unsigned char* t = &tile->detailTris[(pd->triBase+k)*4]; + const float* tv[3]; + for (int m = 0; m < 3; ++m) + { + if (t[m] < p->vertCount) + tv[m] = &tile->verts[p->verts[t[m]]*3]; + else + tv[m] = &tile->detailVerts[(pd->vertBase+(t[m]-p->vertCount))*3]; + } + for (int m = 0, n = 2; m < 3; n=m++) + { + if ((dtGetDetailTriEdgeFlags(t[3], n) & DT_DETAIL_EDGE_BOUNDARY) == 0) + continue; + + if (distancePtLine2d(tv[n],v0,v1) < thr && + distancePtLine2d(tv[m],v0,v1) < thr) + { + dd->vertex(tv[n], c); + dd->vertex(tv[m], c); + } + } + } + } + } + dd->end(); + } + + float getHeat(unsigned salt, unsigned minSalt, unsigned maxSalt) + { + if (salt < minSalt) + return 0; + if (salt > maxSalt) + return 1; + if (maxSalt <= minSalt) + return 0.5; + return static_cast(salt - minSalt) / static_cast(maxSalt - minSalt); + } + + int getRgbaComponent(float v, int base) + { + return static_cast(std::round(v * base)); + } + + unsigned heatToColor(float heat, int alpha) + { + constexpr int min = 100; + constexpr int max = 200; + if (heat < 0.25f) + return duRGBA(min, min + getRgbaComponent(4 * heat, max - min), max, alpha); + if (heat < 0.5f) + return duRGBA(min, max, min + getRgbaComponent(1 - 4 * (heat - 0.5f), max - min), alpha); + if (heat < 0.75f) + return duRGBA(min + getRgbaComponent(4 * (heat - 0.5f), max - min), max, min, alpha); + return duRGBA(max, min + getRgbaComponent(1 - 4 * (heat - 0.75f), max - min), min, alpha); + } + + // Based on https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L120-L235 + void drawMeshTile(duDebugDraw* dd, const dtNavMesh& mesh, const dtNavMeshQuery* query, + const dtMeshTile* tile, unsigned char flags, float heat) + { + using namespace SceneUtil; + + dtPolyRef base = mesh.getPolyRefBase(tile); + + int tileNum = mesh.decodePolyIdTile(base); + const unsigned alpha = tile->header->userId == 0 ? 64 : 128; + const unsigned int tileNumColor = duIntToCol(tileNum, alpha); + + dd->depthMask(false); + + dd->begin(DU_DRAW_TRIS); + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) // Skip off-mesh links. + continue; + + const dtPolyDetail* pd = &tile->detailMeshes[i]; + + unsigned int col; + if (query && query->isInClosedList(base | (dtPolyRef)i)) + col = duRGBA(255, 196, 0, alpha); + else + { + if (flags & NavMeshTileDrawFlagsColorTiles) + col = duTransCol(tileNumColor, alpha); + else if (flags & NavMeshTileDrawFlagsHeat) + col = heatToColor(heat, alpha); + else + col = duTransCol(dd->areaToCol(p->getArea()), alpha); + } + + for (int j = 0; j < pd->triCount; ++j) + { + const unsigned char* t = &tile->detailTris[(pd->triBase+j)*4]; + for (int k = 0; k < 3; ++k) + { + if (t[k] < p->vertCount) + dd->vertex(&tile->verts[p->verts[t[k]]*3], col); + else + dd->vertex(&tile->detailVerts[(pd->vertBase+t[k]-p->vertCount)*3], col); + } + } + } + dd->end(); + + // Draw inter poly boundaries + drawPolyBoundaries(dd, tile, duRGBA(0,48,64,32), 1.5f, true); + + // Draw outer poly boundaries + drawPolyBoundaries(dd, tile, duRGBA(0,48,64,220), 2.5f, false); + + if (flags & NavMeshTileDrawFlagsOffMeshConnections) + { + dd->begin(DU_DRAW_LINES, 2.0f); + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + if (p->getType() != DT_POLYTYPE_OFFMESH_CONNECTION) // Skip regular polys. + continue; + + unsigned int col, col2; + if (query && query->isInClosedList(base | (dtPolyRef)i)) + col = duRGBA(255,196,0,220); + else + col = duDarkenCol(duTransCol(dd->areaToCol(p->getArea()), 220)); + + const dtOffMeshConnection* con = &tile->offMeshCons[i - tile->header->offMeshBase]; + const float* va = &tile->verts[p->verts[0]*3]; + const float* vb = &tile->verts[p->verts[1]*3]; + + // Check to see if start and end end-points have links. + bool startSet = false; + bool endSet = false; + for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next) + { + if (tile->links[k].edge == 0) + startSet = true; + if (tile->links[k].edge == 1) + endSet = true; + } + + // End points and their on-mesh locations. + dd->vertex(va[0],va[1],va[2], col); + dd->vertex(con->pos[0],con->pos[1],con->pos[2], col); + col2 = startSet ? col : duRGBA(220,32,16,196); + duAppendCircle(dd, con->pos[0],con->pos[1]+0.1f,con->pos[2], con->rad, col2); + + dd->vertex(vb[0],vb[1],vb[2], col); + dd->vertex(con->pos[3],con->pos[4],con->pos[5], col); + col2 = endSet ? col : duRGBA(220,32,16,196); + duAppendCircle(dd, con->pos[3],con->pos[4]+0.1f,con->pos[5], con->rad, col2); + + // End point vertices. + dd->vertex(con->pos[0],con->pos[1],con->pos[2], duRGBA(0,48,64,196)); + dd->vertex(con->pos[0],con->pos[1]+0.2f,con->pos[2], duRGBA(0,48,64,196)); + + dd->vertex(con->pos[3],con->pos[4],con->pos[5], duRGBA(0,48,64,196)); + dd->vertex(con->pos[3],con->pos[4]+0.2f,con->pos[5], duRGBA(0,48,64,196)); + + // Connection arc. + duAppendArc(dd, con->pos[0],con->pos[1],con->pos[2], con->pos[3],con->pos[4],con->pos[5], 0.25f, + (con->flags & 1) ? 0.6f : 0, 0.6f, col); + } + dd->end(); + } + + const unsigned int vcol = duRGBA(0,0,0,196); + dd->begin(DU_DRAW_POINTS, 3.0f); + for (int i = 0; i < tile->header->vertCount; ++i) + { + const float* v = &tile->verts[i*3]; + dd->vertex(v[0], v[1], v[2], vcol); + } + dd->end(); + + dd->depthMask(true); + } +} namespace SceneUtil { - osg::ref_ptr createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings) + osg::ref_ptr makeNavMeshTileStateSet() { - const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 10), 1.0f / settings.mRecastScaleFactor); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + + const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + osg::ref_ptr polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttribute(material); + stateSet->setAttributeAndModes(polygonOffset); + return stateSet; + } + + osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, + const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, + const osg::ref_ptr& debugDrawStateSet, unsigned char flags, unsigned minSalt, unsigned maxSalt) + { + if (meshTile.header == nullptr) + return nullptr; + + osg::ref_ptr group(new osg::Group); + group->setStateSet(groupStateSet); + constexpr float shift = 10.0f; + DebugDraw debugDraw(*group, debugDrawStateSet, osg::Vec3f(0, 0, shift), 1.0f / settings.mRecast.mRecastScaleFactor); dtNavMeshQuery navMeshQuery; - navMeshQuery.init(&navMesh, settings.mMaxNavMeshQueryNodes); - duDebugDrawNavMeshWithClosedList(&debugDraw, navMesh, navMeshQuery, - DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST); + navMeshQuery.init(&navMesh, settings.mDetour.mMaxNavMeshQueryNodes); + drawMeshTile(&debugDraw, navMesh, &navMeshQuery, &meshTile, flags, getHeat(meshTile.salt, minSalt, maxSalt)); + return group; } } diff --git a/components/sceneutil/navmesh.hpp b/components/sceneutil/navmesh.hpp index b255b05756..95d79ea4de 100644 --- a/components/sceneutil/navmesh.hpp +++ b/components/sceneutil/navmesh.hpp @@ -4,10 +4,12 @@ #include class dtNavMesh; +struct dtMeshTile; namespace osg { class Group; + class StateSet; } namespace DetourNavigator @@ -17,7 +19,19 @@ namespace DetourNavigator namespace SceneUtil { - osg::ref_ptr createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings); + enum NavMeshTileDrawFlags : unsigned char + { + NavMeshTileDrawFlagsOffMeshConnections = 1, + NavMeshTileDrawFlagsClosedList = 1 << 1, + NavMeshTileDrawFlagsColorTiles = 1 << 2, + NavMeshTileDrawFlagsHeat = 1 << 3, + }; + + osg::ref_ptr makeNavMeshTileStateSet(); + + osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, + const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, + const osg::ref_ptr& debugDrawStateSet, unsigned char flags, unsigned minSalt, unsigned maxSalt); } #endif diff --git a/components/sceneutil/nodecallback.hpp b/components/sceneutil/nodecallback.hpp new file mode 100644 index 0000000000..96e3ae229e --- /dev/null +++ b/components/sceneutil/nodecallback.hpp @@ -0,0 +1,40 @@ +#ifndef SCENEUTIL_NODECALLBACK_H +#define SCENEUTIL_NODECALLBACK_H + +#include + +namespace osg +{ + class Node; + class NodeVisitor; +} + +namespace SceneUtil +{ + +template +class NodeCallback : public osg::Callback +{ +public: + NodeCallback(){} + NodeCallback(const NodeCallback& nc,const osg::CopyOp& copyop): + osg::Callback(nc, copyop) {} + + bool run(osg::Object* object, osg::Object* data) override + { + static_cast(this)->operator()((NodeType)object, (VisitorType)data->asNodeVisitor()); + return true; + } + + template + void traverse(NodeType object, VT data) + { + if (_nestedCallback.valid()) + _nestedCallback->run(object, data); + else + data->traverse(*object); + } +}; + +} +#endif diff --git a/components/sceneutil/optimizer.cpp b/components/sceneutil/optimizer.cpp index b48ceda409..9012349804 100644 --- a/components/sceneutil/optimizer.cpp +++ b/components/sceneutil/optimizer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,8 @@ #include #include +#include + #include #include #include @@ -40,6 +43,8 @@ #include +#include + using namespace osgUtil; namespace SceneUtil @@ -82,6 +87,13 @@ void Optimizer::optimize(osg::Node* node, unsigned int options) cstv.removeTransforms(node); } + if (options & SHARE_DUPLICATE_STATE && _sharedStateManager) + { + if (_sharedStateMutex) _sharedStateMutex->lock(); + _sharedStateManager->share(node); + if (_sharedStateMutex) _sharedStateMutex->unlock(); + } + if (options & REMOVE_REDUNDANT_NODES) { OSG_INFO<<"Optimizer::optimize() doing REMOVE_REDUNDANT_NODES"<(nullptr)); } } @@ -187,15 +199,19 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor // for all current objects associated this transform with them. registerWithCurrentObjects(&transform); } + void apply(osg::MatrixTransform& transform) override + { + // for all current objects associated this transform with them. + registerWithCurrentObjects(&transform); + } - void apply(osg::Geode& geode) override + void apply(osg::Node& node) override { - traverse(geode); + traverse(node); } - void apply(osg::Billboard& geode) override + void apply(osg::Geometry& geometry) override { - traverse(geode); } void collectDataFor(osg::Node* node) @@ -282,7 +298,19 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor ObjectStruct():_canBeApplied(true),_moreThanOneMatrixRequired(false) {} - void add(osg::Transform* transform, bool canOptimize) + inline const osg::Matrix& getMatrix(osg::MatrixTransform* transform) + { + return transform->getMatrix(); + } + osg::Matrix getMatrix(osg::Transform* transform) + { + osg::Matrix matrix; + transform->computeLocalToWorldMatrix(matrix, 0); + return matrix; + } + + template + void add(T* transform, bool canOptimize) { if (transform) { @@ -290,12 +318,10 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor else if (transform->getReferenceFrame()!=osg::Transform::RELATIVE_RF) _moreThanOneMatrixRequired=true; else { - if (_transformSet.empty()) transform->computeLocalToWorldMatrix(_firstMatrix,0); + if (_transformSet.empty()) _firstMatrix = getMatrix(transform); else { - osg::Matrix matrix; - transform->computeLocalToWorldMatrix(matrix,0); - if (_firstMatrix!=matrix) _moreThanOneMatrixRequired=true; + if (_firstMatrix!=getMatrix(transform)) _moreThanOneMatrixRequired=true; } } } @@ -316,8 +342,8 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor TransformSet _transformSet; }; - - void registerWithCurrentObjects(osg::Transform* transform) + template + void registerWithCurrentObjects(T* transform) { for(ObjectList::iterator itr=_currentObjectList.begin(); itr!=_currentObjectList.end(); @@ -622,19 +648,23 @@ osg::Array* cloneArray(osg::Array* array, osg::VertexBufferObject*& vbo, const o return array; } -void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Drawable& drawable) +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Geometry& geometry) { - osg::Geometry *geometry = drawable.asGeometry(); - if((geometry) && (isOperationPermissibleForObject(&drawable))) + if(isOperationPermissibleForObject(&geometry)) { osg::VertexBufferObject* vbo = nullptr; - if(geometry->getVertexArray() && geometry->getVertexArray()->referenceCount() > 1) - geometry->setVertexArray(cloneArray(geometry->getVertexArray(), vbo, geometry)); - if(geometry->getNormalArray() && geometry->getNormalArray()->referenceCount() > 1) - geometry->setNormalArray(cloneArray(geometry->getNormalArray(), vbo, geometry)); - if(geometry->getTexCoordArray(7) && geometry->getTexCoordArray(7)->referenceCount() > 1) // tangents - geometry->setTexCoordArray(7, cloneArray(geometry->getTexCoordArray(7), vbo, geometry)); + if(geometry.getVertexArray() && geometry.getVertexArray()->referenceCount() > 1) + geometry.setVertexArray(cloneArray(geometry.getVertexArray(), vbo, &geometry)); + if(geometry.getNormalArray() && geometry.getNormalArray()->referenceCount() > 1) + geometry.setNormalArray(cloneArray(geometry.getNormalArray(), vbo, &geometry)); + if(geometry.getTexCoordArray(7) && geometry.getTexCoordArray(7)->referenceCount() > 1) // tangents + geometry.setTexCoordArray(7, cloneArray(geometry.getTexCoordArray(7), vbo, &geometry)); } + _drawableSet.insert(&geometry); +} + +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Drawable& drawable) +{ _drawableSet.insert(&drawable); } @@ -662,6 +692,11 @@ void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Transform& transform) _transformStack.pop_back(); } +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::MatrixTransform& transform) +{ + apply(static_cast(transform)); +} + bool Optimizer::FlattenStaticTransformsVisitor::removeTransforms(osg::Node* nodeWeCannotRemove) { CollectLowestTransformsVisitor cltv(_optimizer); @@ -739,7 +774,8 @@ bool Optimizer::CombineStaticTransformsVisitor::removeTransforms(osg::Node* node if (transform->getNumChildren()==1 && transform->getChild(0)->asTransform()!=0 && transform->getChild(0)->asTransform()->asMatrixTransform()!=0 && - transform->getChild(0)->asTransform()->getDataVariance()==osg::Object::STATIC) + (!transform->getChild(0)->getStateSet() || transform->getChild(0)->getStateSet()->referenceCount()==1) && + transform->getChild(0)->getDataVariance()==osg::Object::STATIC) { // now combine with its child. osg::MatrixTransform* child = transform->getChild(0)->asTransform()->asMatrixTransform(); @@ -810,7 +846,7 @@ void Optimizer::RemoveEmptyNodesVisitor::removeEmptyNodes() ++pitr) { osg::Group* parent = *pitr; - if (!parent->asSwitch() && !dynamic_cast(parent)) + if (!parent->asSwitch() && !dynamic_cast(parent) && !dynamic_cast(parent)) { parent->removeChild(nodeToRemove.get()); if (parent->getNumChildren()==0 && isOperationPermissibleForObject(parent)) newEmptyGroups.insert(parent); @@ -852,6 +888,13 @@ void Optimizer::RemoveRedundantNodesVisitor::apply(osg::Switch& switchNode) traverse(*switchNode.getChild(i)); } +void Optimizer::RemoveRedundantNodesVisitor::apply(osg::Sequence& sequenceNode) +{ + // We should keep all sequence child nodes since they reflect different sequence states. + for (unsigned int i=0; igetChildIndex(group); for (unsigned int i=0; igetNumChildren(); ++i) { - osg::Node* child = group->getChild(i); - (*pitr)->insertChild(childIndex++, child); + if (i==0) + (*pitr)->setChild(childIndex, group->getChild(i)); + else + (*pitr)->insertChild(childIndex+i, group->getChild(i)); } - - (*pitr)->removeChild(group); } group->removeChildren(0, group->getNumChildren()); @@ -1100,10 +1143,13 @@ bool isAbleToMerge(const osg::Geometry& g1, const osg::Geometry& g2) } -void Optimizer::MergeGeometryVisitor::pushStateSet(osg::StateSet *stateSet) +bool Optimizer::MergeGeometryVisitor::pushStateSet(osg::StateSet *stateSet) { + if (!stateSet || stateSet->getRenderBinMode() & osg::StateSet::INHERIT_RENDERBIN_DETAILS) + return false; _stateSetStack.push_back(stateSet); checkAlphaBlendingActive(); + return true; } void Optimizer::MergeGeometryVisitor::popStateSet() @@ -1133,15 +1179,14 @@ void Optimizer::MergeGeometryVisitor::checkAlphaBlendingActive() void Optimizer::MergeGeometryVisitor::apply(osg::Group &group) { - if (group.getStateSet()) - pushStateSet(group.getStateSet()); + bool pushed = pushStateSet(group.getStateSet()); if (!_alphaBlendingActive || _mergeAlphaBlending) mergeGroup(group); traverse(group); - if (group.getStateSet()) + if (pushed) popStateSet(); } @@ -1560,8 +1605,8 @@ bool Optimizer::MergeGeometryVisitor::mergeGroup(osg::Group& group) } if (_alphaBlendingActive && _mergeAlphaBlending && !geom->getStateSet()) { - osg::Depth* d = new osg::Depth; - d->setWriteMask(0); + osg::ref_ptr d = new SceneUtil::AutoDepth; + d->setWriteMask(false); geom->getOrCreateStateSet()->setAttribute(d); } } @@ -1570,9 +1615,6 @@ bool Optimizer::MergeGeometryVisitor::mergeGroup(osg::Group& group) } -// geode.dirtyBound(); - - return false; } @@ -1898,8 +1940,8 @@ bool Optimizer::MergeGroupsVisitor::isOperationPermissible(osg::Group& node) return !node.getCullCallback() && !node.getEventCallback() && !node.getUpdateCallback() && - isOperationPermissibleForObject(&node) && - typeid(node)==typeid(osg::Group); + typeid(node)==typeid(osg::Group) && + isOperationPermissibleForObject(&node); } void Optimizer::MergeGroupsVisitor::apply(osg::LOD &lod) @@ -1914,6 +1956,12 @@ void Optimizer::MergeGroupsVisitor::apply(osg::Switch &switchNode) traverse(switchNode); } +void Optimizer::MergeGroupsVisitor::apply(osg::Sequence &sequenceNode) +{ + // We should keep all sequence child nodes since they reflect different sequence states. + traverse(sequenceNode); +} + void Optimizer::MergeGroupsVisitor::apply(osg::Group &group) { if (group.getNumChildren() <= 1) diff --git a/components/sceneutil/optimizer.hpp b/components/sceneutil/optimizer.hpp index 2d6293e231..946f2e1f55 100644 --- a/components/sceneutil/optimizer.hpp +++ b/components/sceneutil/optimizer.hpp @@ -25,6 +25,12 @@ //#include #include +#include + +namespace osgDB +{ + class SharedStateManager; +} //namespace osgUtil { namespace SceneUtil { @@ -65,7 +71,7 @@ class Optimizer public: - Optimizer() : _mergeAlphaBlending(false) {} + Optimizer() : _mergeAlphaBlending(false), _sharedStateManager(nullptr), _sharedStateMutex(nullptr) {} virtual ~Optimizer() {} enum OptimizationOptions @@ -121,6 +127,8 @@ class Optimizer void setMergeAlphaBlending(bool merge) { _mergeAlphaBlending = merge; } void setViewPoint(const osg::Vec3f& viewPoint) { _viewPoint = viewPoint; } + void setSharedStateManager(osgDB::SharedStateManager* sharedStateManager, std::mutex* sharedStateMutex) { _sharedStateMutex = sharedStateMutex; _sharedStateManager = sharedStateManager; } + /** Reset internal data to initial state - the getPermissibleOptionsMap is cleared.*/ void reset(); @@ -258,6 +266,9 @@ class Optimizer osg::Vec3f _viewPoint; bool _mergeAlphaBlending; + osgDB::SharedStateManager* _sharedStateManager; + mutable std::mutex* _sharedStateMutex; + public: /** Flatten Static Transform nodes by applying their transform to the @@ -273,10 +284,12 @@ class Optimizer FlattenStaticTransformsVisitor(Optimizer* optimizer=0): BaseOptimizerVisitor(optimizer, FLATTEN_STATIC_TRANSFORMS) {} - void apply(osg::Node& geode) override; + void apply(osg::Node& node) override; + void apply(osg::Geometry& geometry) override; void apply(osg::Drawable& drawable) override; - void apply(osg::Billboard& geode) override; - void apply(osg::Transform& transform) override; + void apply(osg::Billboard& billboard) override; + void apply(osg::Transform& transform) override final; + void apply(osg::MatrixTransform& transform) override; bool removeTransforms(osg::Node* nodeWeCannotRemove); @@ -305,6 +318,7 @@ class Optimizer BaseOptimizerVisitor(optimizer, FLATTEN_STATIC_TRANSFORMS) {} void apply(osg::MatrixTransform& transform) override; + void apply(osg::Geometry&) override { } bool removeTransforms(osg::Node* nodeWeCannotRemove); @@ -327,6 +341,7 @@ class Optimizer BaseOptimizerVisitor(optimizer, REMOVE_REDUNDANT_NODES) {} void apply(osg::Group& group) override; + void apply(osg::Geometry&) override { } void removeEmptyNodes(); @@ -347,6 +362,8 @@ class Optimizer void apply(osg::Transform& transform) override; void apply(osg::LOD& lod) override; void apply(osg::Switch& switchNode) override; + void apply(osg::Sequence& sequenceNode) override; + void apply(osg::Geometry&) override { } bool isOperationPermissible(osg::Node& node); @@ -365,9 +382,11 @@ class Optimizer bool isOperationPermissible(osg::Group& node); + void apply(osg::Geometry&) override { } void apply(osg::Group& group) override; void apply(osg::LOD& lod) override; void apply(osg::Switch& switchNode) override; + void apply(osg::Sequence& sequenceNode) override; }; class MergeGeometryVisitor : public BaseOptimizerVisitor @@ -398,10 +417,10 @@ class Optimizer return _targetMaximumNumberOfVertices; } - void pushStateSet(osg::StateSet* stateSet); + bool pushStateSet(osg::StateSet* stateSet); void popStateSet(); void checkAlphaBlendingActive(); - + void apply(osg::Geometry&) override { } void apply(osg::Group& group) override; void apply(osg::Billboard&) override { /* don't do anything*/ } diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp new file mode 100644 index 0000000000..0f9cf4ce6e --- /dev/null +++ b/components/sceneutil/osgacontroller.cpp @@ -0,0 +1,171 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace SceneUtil +{ + LinkVisitor::LinkVisitor() : osg::NodeVisitor( TRAVERSE_ALL_CHILDREN ) + { + mAnimation = nullptr; + } + + void LinkVisitor::link(osgAnimation::UpdateMatrixTransform* umt) + { + const osgAnimation::ChannelList& channels = mAnimation->getChannels(); + for (const auto& channel: channels) + { + const std::string& channelName = channel->getName(); + const std::string& channelTargetName = channel->getTargetName(); + + if (channelTargetName != umt->getName()) continue; + + // check if we can link a StackedTransformElement to the current Channel + for (const auto & stackedTransform : umt->getStackedTransforms()) + { + osgAnimation::StackedTransformElement* element = stackedTransform.get(); + if (element && !element->getName().empty() && channelName == element->getName()) + { + osgAnimation::Target* target = element->getOrCreateTarget(); + if (target) + { + channel->setTarget(target); + } + } + } + } + } + + void LinkVisitor::setAnimation(Resource::Animation* animation) + { + mAnimation = animation; + } + + void LinkVisitor::apply(osg::Node& node) + { + osg::Callback* cb = node.getUpdateCallback(); + while (cb) + { + osgAnimation::UpdateMatrixTransform* umt = dynamic_cast(cb); + if (umt) + if (Misc::StringUtils::lowerCase(node.getName()) != "bip01") link(umt); + cb = cb->getNestedCallback(); + } + + if (node.getNumChildrenRequiringUpdateTraversal()) + traverse( node ); + } + + OsgAnimationController::OsgAnimationController(const OsgAnimationController ©, const osg::CopyOp ©op) + : osg::Object(copy, copyop) + , SceneUtil::KeyframeController(copy) + , SceneUtil::NodeCallback(copy, copyop) + , mEmulatedAnimations(copy.mEmulatedAnimations) + { + mLinker = nullptr; + for (const auto& mergedAnimationTrack : copy.mMergedAnimationTracks) + { + Resource::Animation* copiedAnimationTrack = static_cast(mergedAnimationTrack.get()->clone(copyop)); + mMergedAnimationTracks.emplace_back(copiedAnimationTrack); + } + } + + osg::Vec3f OsgAnimationController::getTranslation(float time) const + { + osg::Vec3f translationValue; + std::string animationName; + float newTime = time; + + //Find the correct animation based on time + for (const EmulatedAnimation& emulatedAnimation : mEmulatedAnimations) + { + if (time >= emulatedAnimation.mStartTime && time <= emulatedAnimation.mStopTime) + { + newTime = time - emulatedAnimation.mStartTime; + animationName = emulatedAnimation.mName; + } + } + + //Find the root transform track in animation + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) + { + if (mergedAnimationTrack->getName() != animationName) continue; + + const osgAnimation::ChannelList& channels = mergedAnimationTrack->getChannels(); + + for (const auto& channel: channels) + { + if (channel->getTargetName() != "bip01" || channel->getName() != "transform") continue; + + if ( osgAnimation::MatrixLinearSampler* templateSampler = dynamic_cast (channel->getSampler()) ) + { + osg::Matrixf matrix; + templateSampler->getValueAt(newTime, matrix); + translationValue = matrix.getTrans(); + return osg::Vec3f(translationValue[0], translationValue[1], translationValue[2]); + } + } + } + + return osg::Vec3f(); + } + + void OsgAnimationController::update(float time, const std::string& animationName) + { + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) + { + if (mergedAnimationTrack->getName() == animationName) mergedAnimationTrack->update(time); + } + } + + void OsgAnimationController::operator() (osg::Node* node, osg::NodeVisitor* nv) + { + if (hasInput()) + { + if (mNeedToLink) + { + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) + { + if (!mLinker.valid()) mLinker = new LinkVisitor(); + mLinker->setAnimation(mergedAnimationTrack); + node->accept(*mLinker); + } + mNeedToLink = false; + } + + float time = getInputValue(nv); + + for (const EmulatedAnimation& emulatedAnimation : mEmulatedAnimations) + { + if (time > emulatedAnimation.mStartTime && time < emulatedAnimation.mStopTime) + { + update(time - emulatedAnimation.mStartTime, emulatedAnimation.mName); + } + } + } + + traverse(node, nv); + } + + void OsgAnimationController::setEmulatedAnimations(const std::vector& emulatedAnimations) + { + mEmulatedAnimations = emulatedAnimations; + } + + void OsgAnimationController::addMergedAnimationTrack(osg::ref_ptr animationTrack) + { + mMergedAnimationTracks.emplace_back(animationTrack); + } +} diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp new file mode 100644 index 0000000000..e9ffe2676f --- /dev/null +++ b/components/sceneutil/osgacontroller.hpp @@ -0,0 +1,84 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_OSGACONTROLLER_HPP +#define OPENMW_COMPONENTS_SCENEUTIL_OSGACONTROLLER_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace SceneUtil +{ + struct EmulatedAnimation + { + float mStartTime; + float mStopTime; + std::string mName; + }; + + class LinkVisitor : public osg::NodeVisitor + { + public: + LinkVisitor(); + + virtual void link(osgAnimation::UpdateMatrixTransform* umt); + + virtual void setAnimation(Resource::Animation* animation); + + virtual void apply(osg::Node& node) override; + + protected: + Resource::Animation* mAnimation; + }; + +#ifdef _MSC_VER +#pragma warning( push ) +/* + * Warning C4250: 'SceneUtil::OsgAnimationController': inherits 'osg::Callback::osg::Callback::asCallback' via dominance, + * there is no way to solved this if an object must inherit from both osg::Object and osg::Callback + */ +#pragma warning( disable : 4250 ) +#endif + class OsgAnimationController : public SceneUtil::KeyframeController, public SceneUtil::NodeCallback + { + public: + /// @brief Handles the animation for osgAnimation formats + OsgAnimationController() {}; + + OsgAnimationController(const OsgAnimationController& copy, const osg::CopyOp& copyop); + + META_Object(SceneUtil, OsgAnimationController) + + osg::Callback* getAsCallback() override { return this; } + + /// @brief Handles the location of the instance + osg::Vec3f getTranslation(float time) const override; + + /// @brief Calls animation track update() + void update(float time, const std::string& animationName); + + /// @brief Called every frame for osgAnimation + void operator() (osg::Node*, osg::NodeVisitor*); + + /// @brief Sets details of the animations + void setEmulatedAnimations(const std::vector& emulatedAnimations); + + /// @brief Adds an animation track to a model + void addMergedAnimationTrack(osg::ref_ptr animationTrack); + + private: + bool mNeedToLink = true; + osg::ref_ptr mLinker; + std::vector> mMergedAnimationTracks; // Used only by osgAnimation-based formats (e.g. dae) + std::vector mEmulatedAnimations; + }; +#ifdef _MSC_VER +#pragma warning( pop ) +#endif +} + +#endif diff --git a/components/sceneutil/pathgridutil.cpp b/components/sceneutil/pathgridutil.cpp index ed6894dfc9..6bfc724d7b 100644 --- a/components/sceneutil/pathgridutil.cpp +++ b/components/sceneutil/pathgridutil.cpp @@ -1,8 +1,9 @@ #include "pathgridutil.hpp" #include +#include -#include +#include namespace SceneUtil { @@ -142,7 +143,7 @@ namespace SceneUtil osg::Vec3f dir = toPos - fromPos; dir.normalize(); - osg::Quat rot = osg::Quat(-osg::PI / 2, osg::Vec3(0, 0, 1)); + osg::Quat rot(static_cast(-osg::PI_2), osg::Vec3f(0, 0, 1)); dir = rot * dir; unsigned short diamondIndex = 0; @@ -174,6 +175,11 @@ namespace SceneUtil gridGeometry->addPrimitiveSet(lineIndices); gridGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); } + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + gridGeometry->getOrCreateStateSet()->setAttribute(material); + return gridGeometry; } diff --git a/components/sceneutil/recastmesh.cpp b/components/sceneutil/recastmesh.cpp index 2716f46832..d320624682 100644 --- a/components/sceneutil/recastmesh.cpp +++ b/components/sceneutil/recastmesh.cpp @@ -1,12 +1,19 @@ -#include "navmesh.hpp" +#include "recastmesh.hpp" #include "detourdebugdraw.hpp" +#include "depth.hpp" #include #include +#include #include #include +#include +#include + +#include +#include namespace { @@ -35,14 +42,44 @@ namespace namespace SceneUtil { osg::ref_ptr createRecastMeshGroup(const DetourNavigator::RecastMesh& recastMesh, - const DetourNavigator::Settings& settings) + const DetourNavigator::RecastSettings& settings) { + using namespace DetourNavigator; + const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1.0f / settings.mRecastScaleFactor); - const auto normals = calculateNormals(recastMesh.getVertices(), recastMesh.getIndices()); + DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1.0f); + const DetourNavigator::Mesh& mesh = recastMesh.getMesh(); + std::vector indices = mesh.getIndices(); + std::vector vertices = mesh.getVertices(); + + for (const Heightfield& heightfield : recastMesh.getHeightfields()) + { + const Mesh heightfieldMesh = makeMesh(heightfield); + const int indexShift = static_cast(vertices.size() / 3); + std::copy(heightfieldMesh.getVertices().begin(), heightfieldMesh.getVertices().end(), std::back_inserter(vertices)); + std::transform(heightfieldMesh.getIndices().begin(), heightfieldMesh.getIndices().end(), std::back_inserter(indices), + [&] (int index) { return index + indexShift; }); + } + + for (std::size_t i = 0; i < vertices.size(); i += 3) + std::swap(vertices[i + 1], vertices[i + 2]); + + const auto normals = calculateNormals(vertices, indices); const auto texScale = 1.0f / (settings.mCellSize * 10.0f); - duDebugDrawTriMesh(&debugDraw, recastMesh.getVertices().data(), recastMesh.getVerticesCount(), - recastMesh.getIndices().data(), normals.data(), recastMesh.getTrianglesCount(), nullptr, texScale); + duDebugDrawTriMeshSlope(&debugDraw, vertices.data(), static_cast(vertices.size() / 3), + indices.data(), normals.data(), static_cast(indices.size() / 3), settings.mMaxSlope, texScale); + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + + const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + osg::ref_ptr polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits); + + osg::ref_ptr stateSet = group->getOrCreateStateSet(); + stateSet->setAttribute(material); + stateSet->setAttributeAndModes(polygonOffset); + return group; } } diff --git a/components/sceneutil/recastmesh.hpp b/components/sceneutil/recastmesh.hpp index ee5d9865e5..674b5b1d2a 100644 --- a/components/sceneutil/recastmesh.hpp +++ b/components/sceneutil/recastmesh.hpp @@ -11,13 +11,13 @@ namespace osg namespace DetourNavigator { class RecastMesh; - struct Settings; + struct RecastSettings; } namespace SceneUtil { osg::ref_ptr createRecastMeshGroup(const DetourNavigator::RecastMesh& recastMesh, - const DetourNavigator::Settings& settings); + const DetourNavigator::RecastSettings& settings); } #endif diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index b9201fdf66..84b31f4afc 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "skeleton.hpp" #include "util.hpp" @@ -59,12 +61,22 @@ RigGeometry::RigGeometry(const RigGeometry ©, const osg::CopyOp ©op) void RigGeometry::setSourceGeometry(osg::ref_ptr sourceGeometry) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeometry; for (unsigned int i=0; i<2; ++i) { const osg::Geometry& from = *sourceGeometry; + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by mSourceGeometry) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(from, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); + osg::Geometry& to = *mGeometry[i]; to.setSupportsDisplayList(false); to.setUseVertexBufferObjects(true); @@ -114,9 +126,11 @@ osg::ref_ptr RigGeometry::getSourceGeometry() const bool RigGeometry::initFromParentSkeleton(osg::NodeVisitor* nv) { const osg::NodePath& path = nv->getNodePath(); - for (osg::NodePath::const_reverse_iterator it = path.rbegin(); it != path.rend(); ++it) + for (osg::NodePath::const_reverse_iterator it = path.rbegin()+1; it != path.rend(); ++it) { osg::Node* node = *it; + if (node->asTransform()) + continue; if (Skeleton* skel = dynamic_cast(node)) { mSkeleton = skel; @@ -246,8 +260,8 @@ void RigGeometry::cull(osg::NodeVisitor* nv) if (tangentDst) tangentDst->dirty(); -#if OSG_MIN_VERSION_REQUIRED(3, 5, 6) - geom.dirtyGLObjects(); +#if OSG_MIN_VERSION_REQUIRED(3, 5, 10) + geom.osg::Drawable::dirtyGLObjects(); #endif nv->pushOntoNodePath(&geom); @@ -310,8 +324,10 @@ void RigGeometry::updateBounds(osg::NodeVisitor *nv) void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath) { bool foundSkel = false; - osg::ref_ptr geomToSkelMatrix; - for (osg::NodePath::const_iterator it = nodePath.begin(); it != nodePath.end(); ++it) + osg::RefMatrix* geomToSkelMatrix = mGeomToSkelMatrix; + if (geomToSkelMatrix) + geomToSkelMatrix->makeIdentity(); + for (osg::NodePath::const_iterator it = nodePath.begin(); it != nodePath.end()-1; ++it) { osg::Node* node = *it; if (!foundSkel) @@ -323,14 +339,15 @@ void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath) { if (osg::Transform* trans = node->asTransform()) { + osg::MatrixTransform* matrixTrans = trans->asMatrixTransform(); + if (matrixTrans && matrixTrans->getMatrix().isIdentity()) + continue; if (!geomToSkelMatrix) - geomToSkelMatrix = new osg::RefMatrix; + geomToSkelMatrix = mGeomToSkelMatrix = new osg::RefMatrix; trans->computeWorldToLocalMatrix(*geomToSkelMatrix, nullptr); } } } - if (geomToSkelMatrix && !geomToSkelMatrix->isIdentity()) - mGeomToSkelMatrix = geomToSkelMatrix; } void RigGeometry::setInfluenceMap(osg::ref_ptr influenceMap) diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index e01583399e..25ae5a3243 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -9,6 +9,13 @@ namespace SceneUtil class Skeleton; class Bone; + // TODO: This class has a lot of issues. + // - We require too many workarounds to ensure safety. + // - mSourceGeometry should be const, but can not be const because of a use case in shadervisitor.cpp. + // - We create useless mGeometry clones in template RigGeometries. + // - We do not support compileGLObjects. + // - We duplicate some code in MorphGeometry. + /// @brief Mesh skinning implementation. /// @note A RigGeometry may be attached directly to a Skeleton, or somewhere below a Skeleton. /// Note though that the RigGeometry ignores any transforms below the Skeleton, so the attachment point is not that important. diff --git a/components/sceneutil/riggeometryosgaextension.cpp b/components/sceneutil/riggeometryosgaextension.cpp new file mode 100644 index 0000000000..75af4c4b82 --- /dev/null +++ b/components/sceneutil/riggeometryosgaextension.cpp @@ -0,0 +1,281 @@ +#include "riggeometryosgaextension.hpp" + +#include + +#include +#include +#include + +#include +#include + +namespace SceneUtil +{ + +OsgaRigGeometry::OsgaRigGeometry() : osgAnimation::RigGeometry() +{ + setDataVariance(osg::Object::STATIC); +} + +OsgaRigGeometry::OsgaRigGeometry(const osgAnimation::RigGeometry& copy, const osg::CopyOp& copyop) : osgAnimation::RigGeometry(copy, copyop) +{ + setDataVariance(osg::Object::STATIC); +} + +OsgaRigGeometry::OsgaRigGeometry(const OsgaRigGeometry& copy, const osg::CopyOp& copyop) : + osgAnimation::RigGeometry(copy, copyop) +{ + setDataVariance(osg::Object::STATIC); +} + +void OsgaRigGeometry::computeMatrixFromRootSkeleton(osg::MatrixList mtxList) +{ + if (!_root.valid()) + { + Log(Debug::Warning) << "Warning " << className() <<"::computeMatrixFromRootSkeleton if you have this message it means you miss to call buildTransformer(Skeleton* root), or your RigGeometry (" << getName() <<") is not attached to a Skeleton subgraph"; + return; + } + osg::Matrix notRoot = _root->getMatrix(); + _matrixFromSkeletonToGeometry = mtxList[0] * osg::Matrix::inverse(notRoot); + _invMatrixFromSkeletonToGeometry = osg::Matrix::inverse(_matrixFromSkeletonToGeometry); + _needToComputeMatrix = false; +} + +RigGeometryHolder::RigGeometryHolder() : + mBackToOrigin(nullptr), + mLastFrameNumber(0), + mIsBodyPart(false) +{ +} + +RigGeometryHolder::RigGeometryHolder(const RigGeometryHolder& copy, const osg::CopyOp& copyop) : + Drawable(copy, copyop), + mBackToOrigin(copy.mBackToOrigin), + mLastFrameNumber(0), + mIsBodyPart(copy.mIsBodyPart) +{ + setUseVertexBufferObjects(true); + + if (!copy.getSourceRigGeometry()) + { + Log(Debug::Error) << "copy constructor of RigGeometryHolder partially failed (no source RigGeometry)"; + return; + } + + osg::ref_ptr rigGeometry = new OsgaRigGeometry(*copy.getSourceRigGeometry(), copyop); + setSourceRigGeometry(rigGeometry); +} + +RigGeometryHolder::RigGeometryHolder(const osgAnimation::RigGeometry& copy, const osg::CopyOp& copyop) : + mBackToOrigin(nullptr), + mLastFrameNumber(0), + mIsBodyPart(false) +{ + setUseVertexBufferObjects(true); + + osg::ref_ptr rigGeometry = new OsgaRigGeometry(copy, copyop); + setSourceRigGeometry(rigGeometry); +} + +void RigGeometryHolder::setSourceRigGeometry(osg::ref_ptr sourceRigGeometry) +{ + for (unsigned int i=0; i<2; ++i) + mGeometry.at(i) = nullptr; + + mSourceRigGeometry = sourceRigGeometry; + + _boundingBox = mSourceRigGeometry->getComputeBoundingBoxCallback()->computeBound(*mSourceRigGeometry); + _boundingSphere = osg::BoundingSphere(_boundingBox); + + for (unsigned int i=0; i<2; ++i) + { + const OsgaRigGeometry& from = *sourceRigGeometry; + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by mSourceRigGeometry, possibly also RigGeometry._geometry) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. + mGeometry.at(i) = new OsgaRigGeometry(from, osg::CopyOp::SHALLOW_COPY); + mGeometry.at(i)->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceRigGeometry)); + + OsgaRigGeometry& to = *mGeometry.at(i); + to.setSupportsDisplayList(false); + to.setUseVertexBufferObjects(true); + to.setCullingActive(false); // make sure to disable culling since that's handled by this class + + to.setDataVariance(osg::Object::STATIC); + to.setNeedToComputeMatrix(true); + + // vertices and normals are modified every frame, so we need to deep copy them. + // assign a dedicated VBO to make sure that modifications don't interfere with source geometry's VBO. + osg::ref_ptr vbo (new osg::VertexBufferObject); + vbo->setUsage(GL_DYNAMIC_DRAW_ARB); + + osg::ref_ptr vertexArray = static_cast(from.getVertexArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + if (vertexArray) + { + vertexArray->setVertexBufferObject(vbo); + to.setVertexArray(vertexArray); + } + + if (const osg::Array* normals = from.getNormalArray()) + { + osg::ref_ptr normalArray = static_cast(normals->clone(osg::CopyOp::DEEP_COPY_ALL)); + if (normalArray) + { + normalArray->setVertexBufferObject(vbo); + to.setNormalArray(normalArray, osg::Array::BIND_PER_VERTEX); + } + } + + if (const osg::Vec4Array* tangents = dynamic_cast(from.getTexCoordArray(7))) + { + osg::ref_ptr tangentArray = static_cast(tangents->clone(osg::CopyOp::DEEP_COPY_ALL)); + tangentArray->setVertexBufferObject(vbo); + to.setTexCoordArray(7, tangentArray, osg::Array::BIND_PER_VERTEX); + } + } + +} + +osg::ref_ptr RigGeometryHolder::getSourceRigGeometry() const +{ + return mSourceRigGeometry; +} + +void RigGeometryHolder::updateRigGeometry(OsgaRigGeometry* geom, osg::NodeVisitor *nv) +{ + if(!geom) + return; + + if(!geom->getSkeleton() && !this->getParents().empty()) + { + osgAnimation::RigGeometry::FindNearestParentSkeleton finder; + if(this->getParents().size() > 1) + Log(Debug::Warning) << "A RigGeometry should not have multi parent ( " << geom->getName() << " )"; + this->getParents()[0]->accept(finder); + + if(!finder._root.valid()) + { + Log(Debug::Warning) << "A RigGeometry did not find a parent skeleton for RigGeometry ( " << geom->getName() << " )"; + return; + } + geom->getRigTransformImplementation()->prepareData(*geom); + geom->setSkeleton(finder._root.get()); + } + + if(!geom->getSkeleton()) + return; + + if(geom->getNeedToComputeMatrix()) + { + osgAnimation::Skeleton* root = geom->getSkeleton(); + if (!root) + { + Log(Debug::Warning) << "Warning: if you have this message it means you miss to call buildTransformer(Skeleton* root), or your RigGeometry is not attached to a Skeleton subgraph"; + return; + } + osg::MatrixList mtxList = root->getWorldMatrices(root); //We always assume that RigGeometries have origin at their root + geom->computeMatrixFromRootSkeleton(mtxList); + + if (mIsBodyPart && mBackToOrigin) updateBackToOriginTransform(geom); + } + + if(geom->getSourceGeometry()) + { + osg::Drawable::UpdateCallback * up = dynamic_cast(geom->getSourceGeometry()->getUpdateCallback()); + if(up) + { + up->update(nv, geom->getSourceGeometry()); + } + } + + geom->update(); +} + +OsgaRigGeometry* RigGeometryHolder::getGeometry(int geometry) +{ + return mGeometry.at(geometry).get(); +} + + +void RigGeometryHolder::updateBackToOriginTransform(OsgaRigGeometry* geometry) +{ + osgAnimation::Skeleton* skeleton = geometry->getSkeleton(); + if (skeleton) + { + osg::MatrixList mtxList = mBackToOrigin->getParents()[0]->getWorldMatrices(skeleton); + osg::Matrix skeletonMatrix = skeleton->getMatrix(); + osg::Matrixf matrixFromSkeletonToGeometry = mtxList[0] * osg::Matrix::inverse(skeletonMatrix); + osg::Matrixf invMatrixFromSkeletonToGeometry = osg::Matrix::inverse(matrixFromSkeletonToGeometry); + mBackToOrigin->setMatrix(invMatrixFromSkeletonToGeometry); + } +} + +void RigGeometryHolder::accept(osg::NodeVisitor &nv) +{ + if (!nv.validNodeMask(*this)) + return; + + nv.pushOntoNodePath(this); + + if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR && mSourceRigGeometry.get()) + { + unsigned int traversalNumber = nv.getTraversalNumber(); + if (mLastFrameNumber == traversalNumber) + { + OsgaRigGeometry& geom = *getRigGeometryPerFrame(mLastFrameNumber); + + nv.pushOntoNodePath(&geom); + nv.apply(geom); + nv.popFromNodePath(); + } + else + { + mLastFrameNumber = traversalNumber; + + OsgaRigGeometry& geom = *getRigGeometryPerFrame(mLastFrameNumber); + + if (mIsBodyPart) + { + if (mBackToOrigin) updateBackToOriginTransform(&geom); + else + { + osg::MatrixTransform* matrixTransform = dynamic_cast (this->getParents()[0]); + if (matrixTransform) + { + mBackToOrigin = matrixTransform; + updateBackToOriginTransform(&geom); + } + } + } + + updateRigGeometry(&geom, &nv); + + nv.pushOntoNodePath(&geom); + nv.apply(geom); + nv.popFromNodePath(); + } + } + else if (nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) + { + } + else + nv.apply(*this); + + nv.popFromNodePath(); +} + + +void RigGeometryHolder::accept(osg::PrimitiveFunctor& func) const +{ + getRigGeometryPerFrame(mLastFrameNumber)->accept(func); +} + +OsgaRigGeometry* RigGeometryHolder::getRigGeometryPerFrame(unsigned int frame) const +{ + return mGeometry.at(frame%2).get(); +} + + +} diff --git a/components/sceneutil/riggeometryosgaextension.hpp b/components/sceneutil/riggeometryosgaextension.hpp new file mode 100644 index 0000000000..6098bd9608 --- /dev/null +++ b/components/sceneutil/riggeometryosgaextension.hpp @@ -0,0 +1,72 @@ +#ifndef OPENMW_COMPONENTS_OSGAEXTENSION_RIGGEOMETRY_H +#define OPENMW_COMPONENTS_OSGAEXTENSION_RIGGEOMETRY_H + +#include + +#include +#include + +#include + +namespace SceneUtil +{ + /// @brief Custom RigGeometry-class for osgAnimation-formats (collada) + class OsgaRigGeometry : public osgAnimation::RigGeometry + { + public: + + OsgaRigGeometry(); + + OsgaRigGeometry(const osgAnimation::RigGeometry& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY); + + OsgaRigGeometry(const OsgaRigGeometry& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY); + + META_Object(SceneUtil, OsgaRigGeometry); + + void computeMatrixFromRootSkeleton(osg::MatrixList mtxList); + }; + + /// @brief OpenMW-compatible double buffered static datavariance version of osgAnimation::RigGeometry + /// This class is based on osgAnimation::RigGeometry and SceneUtil::RigGeometry + class RigGeometryHolder : public osg::Drawable + { + public: + RigGeometryHolder(); + + RigGeometryHolder(const RigGeometryHolder& copy, const osg::CopyOp& copyop); + + RigGeometryHolder(const osgAnimation::RigGeometry& copy, const osg::CopyOp& copyop); + + META_Object(SceneUtil, RigGeometryHolder); + + void setSourceRigGeometry(osg::ref_ptr sourceRigGeometry); + osg::ref_ptr getSourceRigGeometry() const; + + /// @brief Modified rig update, code based on osgAnimation::UpdateRigGeometry : public osg::Drawable::UpdateCallback + void updateRigGeometry(OsgaRigGeometry* geom, osg::NodeVisitor *nv); + + OsgaRigGeometry* getGeometry(int geometry); + + void accept(osg::NodeVisitor &nv) override; + void accept(osg::PrimitiveFunctor&) const override; + bool supports(const osg::PrimitiveFunctor&) const override{ return true; } + + void setBackToOrigin(osg::MatrixTransform* backToOrigin) {mBackToOrigin = backToOrigin;} + void setBodyPart(bool isBodyPart) {mIsBodyPart = isBodyPart;} + + private: + std::array, 2> mGeometry; + osg::ref_ptr mSourceRigGeometry; + osg::MatrixTransform* mBackToOrigin; //This is used to move riggeometries from their slot locations to skeleton origin in order to get correct deformations for bodyparts + + unsigned int mLastFrameNumber; + bool mIsBodyPart; + + void updateBackToOriginTransform(OsgaRigGeometry* geometry); + + OsgaRigGeometry* getRigGeometryPerFrame(unsigned int frame) const; + }; + +} + +#endif diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp new file mode 100644 index 0000000000..19daf2b560 --- /dev/null +++ b/components/sceneutil/rtt.cpp @@ -0,0 +1,256 @@ +#include "rtt.hpp" +#include "util.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace SceneUtil +{ + class CullCallback : public SceneUtil::NodeCallback + { + public: + + void operator()(RTTNode* node, osgUtil::CullVisitor* cv) + { + node->cull(cv); + } + }; + + RTTNode::RTTNode(uint32_t textureWidth, uint32_t textureHeight, uint32_t samples, bool generateMipmaps, int renderOrderNum, StereoAwareness stereoAwareness) + : mTextureWidth(textureWidth) + , mTextureHeight(textureHeight) + , mSamples(samples) + , mGenerateMipmaps(generateMipmaps) + , mColorBufferInternalFormat(Color::colorInternalFormat()) + , mDepthBufferInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()) + , mRenderOrderNum(renderOrderNum) + , mStereoAwareness(stereoAwareness) + { + addCullCallback(new CullCallback); + setCullingActive(false); + } + + RTTNode::~RTTNode() + { + for (auto& vdd : mViewDependentDataMap) + { + auto* camera = vdd.second->mCamera.get(); + if (camera) + { + camera->removeChildren(0, camera->getNumChildren()); + } + } + mViewDependentDataMap.clear(); + } + + void RTTNode::cull(osgUtil::CullVisitor* cv) + { + auto frameNumber = cv->getFrameStamp()->getFrameNumber(); + auto* vdd = getViewDependentData(cv); + if (frameNumber > vdd->mFrameNumber) + { + apply(vdd->mCamera); + auto& sm = Stereo::Manager::instance(); + if (sm.getEye(cv) == Stereo::Eye::Left) + applyLeft(vdd->mCamera); + if (sm.getEye(cv) == Stereo::Eye::Right) + applyRight(vdd->mCamera); + vdd->mCamera->accept(*cv); + } + vdd->mFrameNumber = frameNumber; + } + + void RTTNode::setColorBufferInternalFormat(GLint internalFormat) + { + mColorBufferInternalFormat = internalFormat; + } + + void RTTNode::setDepthBufferInternalFormat(GLint internalFormat) + { + mDepthBufferInternalFormat = internalFormat; + } + + bool RTTNode::shouldDoPerViewMapping() + { + if(mStereoAwareness != StereoAwareness::Aware) + return false; + if (!Stereo::getMultiview()) + return true; + return false; + } + + bool RTTNode::shouldDoTextureArray() + { + if (mStereoAwareness == StereoAwareness::Unaware) + return false; + if (Stereo::getMultiview()) + return true; + return false; + } + + bool RTTNode::shouldDoTextureView() + { + if (mStereoAwareness != StereoAwareness::Unaware_MultiViewShaders) + return false; + if (Stereo::getMultiview()) + return true; + return false; + } + + osg::Texture2DArray* RTTNode::createTextureArray(GLint internalFormat) + { + osg::Texture2DArray* textureArray = new osg::Texture2DArray; + textureArray->setTextureSize(mTextureWidth, mTextureHeight, 2); + textureArray->setInternalFormat(internalFormat); + GLenum sourceFormat = 0; + GLenum sourceType = 0; + if (SceneUtil::isDepthFormat(internalFormat)) + { + SceneUtil::getDepthFormatSourceFormatAndType(internalFormat, sourceFormat, sourceType); + } + else + { + SceneUtil::getColorFormatSourceFormatAndType(internalFormat, sourceFormat, sourceType); + } + textureArray->setSourceFormat(sourceFormat); + textureArray->setSourceType(sourceType); + textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + return textureArray; + } + + osg::Texture2D* RTTNode::createTexture(GLint internalFormat) + { + osg::Texture2D* texture = new osg::Texture2D; + texture->setTextureSize(mTextureWidth, mTextureHeight); + texture->setInternalFormat(internalFormat); + GLenum sourceFormat = 0; + GLenum sourceType = 0; + if (SceneUtil::isDepthFormat(internalFormat)) + { + SceneUtil::getDepthFormatSourceFormatAndType(internalFormat, sourceFormat, sourceType); + } + else + { + SceneUtil::getColorFormatSourceFormatAndType(internalFormat, sourceFormat, sourceType); + } + texture->setSourceFormat(sourceFormat); + texture->setSourceType(sourceType); + 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); + texture->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + return texture; + } + + osg::Texture* RTTNode::getColorTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mColorTexture; + } + + osg::Texture* RTTNode::getDepthTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mDepthTexture; + } + + osg::Camera* RTTNode::getCamera(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mCamera; + } + + RTTNode::ViewDependentData* RTTNode::getViewDependentData(osgUtil::CullVisitor* cv) + { + if (!shouldDoPerViewMapping()) + // Always setting it to null is an easy way to disable per-view mapping when mDoPerViewMapping is false. + // This is safe since the visitor is never dereferenced. + cv = nullptr; + + if (mViewDependentDataMap.count(cv) == 0) + { + auto camera = new osg::Camera(); + auto vdd = std::make_shared(); + mViewDependentDataMap[cv] = vdd; + mViewDependentDataMap[cv]->mCamera = camera; + + camera->setRenderOrder(osg::Camera::PRE_RENDER, mRenderOrderNum); + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + camera->setViewport(0, 0, mTextureWidth, mTextureHeight); + SceneUtil::setCameraClearDepth(camera); + + setDefaults(camera); + + if (camera->getBufferAttachmentMap().count(osg::Camera::COLOR_BUFFER)) + vdd->mColorTexture = camera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._texture; + if (camera->getBufferAttachmentMap().count(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER)) + vdd->mDepthTexture = camera->getBufferAttachmentMap()[osg::Camera::PACKED_DEPTH_STENCIL_BUFFER]._texture; + + if (shouldDoTextureArray()) + { + // Create any buffer attachments not added in setDefaults + if (camera->getBufferAttachmentMap().count(osg::Camera::COLOR_BUFFER) == 0) + { + vdd->mColorTexture = createTextureArray(mColorBufferInternalFormat); + camera->attach(osg::Camera::COLOR_BUFFER, vdd->mColorTexture, 0, Stereo::osgFaceControlledByMultiviewShader(), mGenerateMipmaps, mSamples); + SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, vdd->mColorTexture, 0, Stereo::osgFaceControlledByMultiviewShader(), mGenerateMipmaps); + } + + if (camera->getBufferAttachmentMap().count(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER) == 0) + { + vdd->mDepthTexture = createTextureArray(mDepthBufferInternalFormat); + camera->attach(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, vdd->mDepthTexture, 0, Stereo::osgFaceControlledByMultiviewShader(), false, mSamples); + } + + if (shouldDoTextureView()) + { + // In this case, shaders being set to multiview forces us to render to a multiview framebuffer even though we don't need that. + // This forces us to make Texture2DArray. To make this possible to sample as a Texture2D, make a Texture2D view into the texture array. + vdd->mColorTexture = Stereo::createTextureView_Texture2DFromTexture2DArray(static_cast(vdd->mColorTexture.get()), 0); + vdd->mDepthTexture = Stereo::createTextureView_Texture2DFromTexture2DArray(static_cast(vdd->mDepthTexture.get()), 0); + } + } + else + { + // Create any buffer attachments not added in setDefaults + if (camera->getBufferAttachmentMap().count(osg::Camera::COLOR_BUFFER) == 0) + { + vdd->mColorTexture = createTexture(mColorBufferInternalFormat); + camera->attach(osg::Camera::COLOR_BUFFER, vdd->mColorTexture, 0, 0, mGenerateMipmaps, mSamples); + SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, vdd->mColorTexture, 0, 0, mGenerateMipmaps); + } + + if (camera->getBufferAttachmentMap().count(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER) == 0) + { + vdd->mDepthTexture = createTexture(mDepthBufferInternalFormat); + camera->attach(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, vdd->mDepthTexture, 0, 0, false, mSamples); + } + } + + // OSG appears not to properly initialize this metadata. So when multisampling is enabled, OSG will use incorrect formats for the resolve buffers. + if (mSamples > 1) + { + camera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._internalFormat = mColorBufferInternalFormat; + camera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._mipMapGeneration = mGenerateMipmaps; + camera->getBufferAttachmentMap()[osg::Camera::PACKED_DEPTH_STENCIL_BUFFER]._internalFormat = mDepthBufferInternalFormat; + camera->getBufferAttachmentMap()[osg::Camera::PACKED_DEPTH_STENCIL_BUFFER]._mipMapGeneration = mGenerateMipmaps; + } + } + + return mViewDependentDataMap[cv].get(); + } +} diff --git a/components/sceneutil/rtt.hpp b/components/sceneutil/rtt.hpp new file mode 100644 index 0000000000..ec8243792f --- /dev/null +++ b/components/sceneutil/rtt.hpp @@ -0,0 +1,107 @@ +#ifndef OPENMW_RTT_H +#define OPENMW_RTT_H + +#include + +#include +#include + +namespace osg +{ + class Texture2D; + class Texture2DArray; + class Camera; +} + +namespace osgUtil +{ + class CullVisitor; +} + +namespace SceneUtil +{ + class CreateTextureViewsCallback; + + /// @brief Implements per-view RTT operations. + /// @par With a naive RTT implementation, subsequent views of multiple views will overwrite the results of the previous views, leading to + /// the results of the last view being broadcast to all views. An error in all cases where the RTT result depends on the view. + /// @par If using an RTTNode this is solved by mapping RTT operations to CullVisitors, which will be unique per view. This requires + /// instancing one camera per view, and traversing only the camera mapped to that CV during cull traversals. + /// @par Camera settings should be effectuated by overriding the setDefaults() and apply() methods, following a pattern similar to SceneUtil::StateSetUpdater + /// @par When using the RTT texture in your statesets, it is recommended to use SceneUtil::StateSetUpdater as a cull callback to handle this as the appropriate + /// textures can be retrieved during SceneUtil::StateSetUpdater::Apply() + /// @par For any of COLOR_BUFFER or PACKED_DEPTH_STENCIL_BUFFER not added during setDefaults(), RTTNode will attach a default buffer. The default color buffer has an internal format of GL_RGB. + /// The default depth buffer has internal format GL_DEPTH_COMPONENT24, source format GL_DEPTH_COMPONENT, and source type GL_UNSIGNED_INT. Default wrap is CLAMP_TO_EDGE and filter LINEAR. + class RTTNode : public osg::Node + { + public: + enum class StereoAwareness + { + Unaware, //! RTT does not vary by view. A single RTT context is created + Aware, //! RTT varies by view. One RTT context per view is created. Textures are automatically created as arrays if multiview is enabled. + Unaware_MultiViewShaders, //! RTT does not vary by view, but renders with multiview shaders and needs to create texture arrays if multiview is enabled. + }; + + RTTNode(uint32_t textureWidth, uint32_t textureHeight, uint32_t samples, bool generateMipmaps, int renderOrderNum, StereoAwareness stereoAwareness); + ~RTTNode(); + + osg::Texture* getColorTexture(osgUtil::CullVisitor* cv); + + osg::Texture* getDepthTexture(osgUtil::CullVisitor* cv); + + osg::Camera* getCamera(osgUtil::CullVisitor* cv); + + /// Set default settings - optionally override in derived classes + virtual void setDefaults(osg::Camera* camera) {}; + + /// Apply state - to override in derived classes + /// @note Due to the view mapping approach you *have* to apply all camera settings, even if they have not changed since the last frame. + virtual void apply(osg::Camera* camera) {}; + + /// Apply any state specific to the Left view. Default implementation does nothing. Called after apply() + virtual void applyLeft(osg::Camera* camera) {} + /// Apply any state specific to the Right view. Default implementation does nothing. Called after apply() + virtual void applyRight(osg::Camera* camera) {} + + void cull(osgUtil::CullVisitor* cv); + + uint32_t width() const { return mTextureWidth; } + uint32_t height() const { return mTextureHeight; } + uint32_t samples() const { return mSamples; } + bool generatesMipmaps() const { return mGenerateMipmaps; } + + void setColorBufferInternalFormat(GLint internalFormat); + void setDepthBufferInternalFormat(GLint internalFormat); + + protected: + bool shouldDoPerViewMapping(); + bool shouldDoTextureArray(); + bool shouldDoTextureView(); + osg::Texture2DArray* createTextureArray(GLint internalFormat); + osg::Texture2D* createTexture(GLint internalFormat); + + private: + friend class CreateTextureViewsCallback; + struct ViewDependentData + { + osg::ref_ptr mCamera; + osg::ref_ptr mColorTexture; + osg::ref_ptr mDepthTexture; + unsigned int mFrameNumber = 0; + }; + + ViewDependentData* getViewDependentData(osgUtil::CullVisitor* cv); + + typedef std::map< osgUtil::CullVisitor*, std::shared_ptr > ViewDependentDataMap; + ViewDependentDataMap mViewDependentDataMap; + uint32_t mTextureWidth; + uint32_t mTextureHeight; + uint32_t mSamples; + bool mGenerateMipmaps; + GLint mColorBufferInternalFormat; + GLint mDepthBufferInternalFormat; + int mRenderOrderNum; + StereoAwareness mStereoAwareness; + }; +} +#endif diff --git a/components/sceneutil/screencapture.cpp b/components/sceneutil/screencapture.cpp new file mode 100644 index 0000000000..97a22e94aa --- /dev/null +++ b/components/sceneutil/screencapture.cpp @@ -0,0 +1,166 @@ +#include "screencapture.hpp" + +#include +#include + +#include +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + class ScreenCaptureWorkItem : public SceneUtil::WorkItem + { + public: + ScreenCaptureWorkItem(const osg::ref_ptr& impl, + const osg::Image& image, unsigned int contextId) + : mImpl(impl), + mImage(new osg::Image(image)), + mContextId(contextId) + { + assert(mImpl != nullptr); + } + + void doWork() override + { + if (mAborted) + return; + + try + { + (*mImpl)(*mImage, mContextId); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "ScreenCaptureWorkItem exception: " << e.what(); + } + } + + void abort() override + { + mAborted = true; + } + + private: + const osg::ref_ptr mImpl; + const osg::ref_ptr mImage; + const unsigned int mContextId; + std::atomic_bool mAborted {false}; + }; +} + +namespace SceneUtil +{ + std::string writeScreenshotToFile(const std::string& screenshotPath, const std::string& screenshotFormat, + const osg::Image& image) + { + // Count screenshots. + int shotCount = 0; + + // Find the first unused filename with a do-while + std::ostringstream stream; + std::string lastFileName; + std::string lastFilePath; + do + { + // Reset the stream + stream.str(""); + stream.clear(); + + stream << "screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << screenshotFormat; + + lastFileName = stream.str(); + lastFilePath = screenshotPath + "/" + lastFileName; + + } while (std::filesystem::exists(lastFilePath)); + + std::ofstream outStream; + outStream.open(std::filesystem::path(std::move(lastFilePath)), std::ios::binary); + + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension(screenshotFormat); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Can't write screenshot, no '" << screenshotFormat << "' readerwriter found"; + return std::string(); + } + + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(image, outStream); + if (!result.success()) + { + Log(Debug::Error) << "Error: Can't write screenshot: " << result.message() << " code " << result.status(); + return std::string(); + } + + return lastFileName; + } + + WriteScreenshotToFileOperation::WriteScreenshotToFileOperation(const std::string& screenshotPath, + const std::string& screenshotFormat, + std::function callback) + : mScreenshotPath(screenshotPath) + , mScreenshotFormat(screenshotFormat) + , mCallback(callback) + { + } + + void WriteScreenshotToFileOperation::operator()(const osg::Image& image, const unsigned int /*context_id*/) + { + std::string fileName; + try + { + fileName = writeScreenshotToFile(mScreenshotPath, mScreenshotFormat, image); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to write screenshot to file with path=\"" << mScreenshotPath + << "\", format=\"" << mScreenshotFormat << "\": " << e.what(); + } + if (fileName.empty()) + mCallback("Failed to save screenshot"); + else + mCallback(fileName + " has been saved"); + } + + AsyncScreenCaptureOperation::AsyncScreenCaptureOperation(osg::ref_ptr queue, + osg::ref_ptr impl) + : mQueue(std::move(queue)), + mImpl(std::move(impl)) + { + assert(mQueue != nullptr); + assert(mImpl != nullptr); + } + + AsyncScreenCaptureOperation::~AsyncScreenCaptureOperation() + { + stop(); + } + + void AsyncScreenCaptureOperation::stop() + { + for (const osg::ref_ptr& item : *mWorkItems.lockConst()) + item->abort(); + + for (const osg::ref_ptr& item : *mWorkItems.lockConst()) + item->waitTillDone(); + } + + void AsyncScreenCaptureOperation::operator()(const osg::Image& image, const unsigned int context_id) + { + osg::ref_ptr item(new ScreenCaptureWorkItem(mImpl, image, context_id)); + mQueue->addWorkItem(item); + const auto isDone = [] (const osg::ref_ptr& v) { return v->isDone(); }; + const auto workItems = mWorkItems.lock(); + workItems->erase(std::remove_if(workItems->begin(), workItems->end(), isDone), workItems->end()); + workItems->emplace_back(std::move(item)); + } +} diff --git a/components/sceneutil/screencapture.hpp b/components/sceneutil/screencapture.hpp new file mode 100644 index 0000000000..87e396b020 --- /dev/null +++ b/components/sceneutil/screencapture.hpp @@ -0,0 +1,58 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_SCREENCAPTURE_H +#define OPENMW_COMPONENTS_SCENEUTIL_SCREENCAPTURE_H + +#include + +#include +#include + +#include +#include + +namespace osg +{ + class Image; +} + +namespace SceneUtil +{ + class WorkQueue; + class WorkItem; + + std::string writeScreenshotToFile(const std::string& screenshotPath, const std::string& screenshotFormat, + const osg::Image& image); + + class WriteScreenshotToFileOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation + { + public: + WriteScreenshotToFileOperation(const std::string& screenshotPath, const std::string& screenshotFormat, + std::function callback); + + void operator()(const osg::Image& image, const unsigned int context_id) override; + + private: + const std::string mScreenshotPath; + const std::string mScreenshotFormat; + const std::function mCallback; + }; + + class AsyncScreenCaptureOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation + { + public: + AsyncScreenCaptureOperation(osg::ref_ptr queue, + osg::ref_ptr impl); + + ~AsyncScreenCaptureOperation(); + + void stop(); + + void operator()(const osg::Image& image, const unsigned int context_id) override; + + private: + const osg::ref_ptr mQueue; + const osg::ref_ptr mImpl; + Misc::ScopeGuarded>> mWorkItems; + }; +} + +#endif diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index d03612fc1d..748b3a708d 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace SceneUtil @@ -49,6 +50,24 @@ public: } }; +class RigGeometryHolderSerializer : public osgDB::ObjectWrapper +{ +public: + RigGeometryHolderSerializer() + : osgDB::ObjectWrapper(createInstanceFunc, "SceneUtil::RigGeometryHolder", "osg::Object osg::Node osg::Drawable SceneUtil::RigGeometryHolder") + { + } +}; + +class OsgaRigGeometrySerializer : public osgDB::ObjectWrapper +{ +public: + OsgaRigGeometrySerializer() + : osgDB::ObjectWrapper(createInstanceFunc, "SceneUtil::OsgaRigGeometry", "osg::Object osg::Node osg::Geometry osgAnimation::RigGeometry SceneUtil::OsgaRigGeometry") + { + } +}; + class MorphGeometrySerializer : public osgDB::ObjectWrapper { public: @@ -80,7 +99,7 @@ class MatrixTransformSerializer : public osgDB::ObjectWrapper { public: MatrixTransformSerializer() - : osgDB::ObjectWrapper(createInstanceFunc, "NifOsg::MatrixTransform", "osg::Object osg::Node osg::Transform osg::MatrixTransform NifOsg::MatrixTransform") + : osgDB::ObjectWrapper(createInstanceFunc, "NifOsg::MatrixTransform", "osg::Object osg::Node osg::Group osg::Transform osg::MatrixTransform NifOsg::MatrixTransform") { } }; @@ -108,6 +127,8 @@ void registerSerializers() mgr->addWrapper(new PositionAttitudeTransformSerializer); mgr->addWrapper(new SkeletonSerializer); mgr->addWrapper(new RigGeometrySerializer); + mgr->addWrapper(new RigGeometryHolderSerializer); + mgr->addWrapper(new OsgaRigGeometrySerializer); mgr->addWrapper(new MorphGeometrySerializer); mgr->addWrapper(new LightManagerSerializer); mgr->addWrapper(new CameraRelativeTransformSerializer); @@ -121,18 +142,22 @@ void registerSerializers() const char* ignore[] = { "MWRender::PtrHolder", "Resource::TemplateRef", + "Resource::TemplateMultiRef", "SceneUtil::CompositeStateSetUpdater", + "SceneUtil::UBOManager", "SceneUtil::LightListCallback", "SceneUtil::LightManagerUpdateCallback", + "SceneUtil::FFPLightStateAttribute", "SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource", - "SceneUtil::StateSetUpdater", "SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", + "SceneUtil::TextKeyMapHolder", + "Shader::AddedState", + "Shader::RemovedAlphaFunc", "NifOsg::FlipController", "NifOsg::KeyframeController", - "NifOsg::TextKeyMapHolder", "NifOsg::Emitter", "NifOsg::ParticleColorAffector", "NifOsg::ParticleSystem", @@ -146,6 +171,7 @@ void registerSerializers() "NifOsg::VisController", "osgMyGUI::Drawable", "osg::DrawCallback", + "osg::UniformBufferObject", "osgOQ::ClearQueriesCallback", "osgOQ::RetrieveQueriesCallback", "osg::DummyObject" diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 1e14fbbb13..5220fce78a 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -1,9 +1,13 @@ #include "shadow.hpp" #include +#include #include #include +#include + +#include "mwshadowtechnique.hpp" namespace SceneUtil { @@ -24,8 +28,7 @@ namespace SceneUtil mShadowSettings->setLightNum(0); mShadowSettings->setReceivesShadowTraversalMask(~0u); - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight); mShadowSettings->setBaseShadowTextureUnit(8 - numberOfShadowMapsPerLight); @@ -33,7 +36,7 @@ namespace SceneUtil const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows"); if (maximumShadowMapDistance > 0) { - const float shadowFadeStart = std::min(std::max(0.f, Settings::Manager::getFloat("shadow fade start", "Shadows")), 1.f); + const float shadowFadeStart = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f); mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance); mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart); } @@ -72,8 +75,10 @@ namespace SceneUtil void ShadowManager::disableShadowsForStateSet(osg::ref_ptr stateset) { - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + if (!Settings::Manager::getBool("enable shadows", "Shadows")) + return; + + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight; @@ -91,12 +96,13 @@ namespace SceneUtil } } - ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager) : mShadowedScene(new osgShadow::ShadowedScene), + ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, Shader::ShaderManager &shaderManager) : mShadowedScene(new osgShadow::ShadowedScene), mShadowTechnique(new MWShadowTechnique), mOutdoorShadowCastingMask(outdoorShadowCastingMask), mIndoorShadowCastingMask(indoorShadowCastingMask) { mShadowedScene->setShadowTechnique(mShadowTechnique); + Stereo::Manager::instance().setShadowTechnique(mShadowTechnique); mShadowedScene->addChild(sceneRoot); rootNode->addChild(mShadowedScene); @@ -106,10 +112,16 @@ namespace SceneUtil setupShadowSettings(); mShadowTechnique->setupCastingShader(shaderManager); + mShadowTechnique->setWorldMask(worldMask); enableOutdoorMode(); } + ShadowManager::~ShadowManager() + { + Stereo::Manager::instance().setShadowTechnique(nullptr); + } + Shader::ShaderManager::DefineMap ShadowManager::getShadowDefines() { if (!mEnableShadows) @@ -168,7 +180,7 @@ namespace SceneUtil if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) mShadowSettings->setCastsShadowTraversalMask(mIndoorShadowCastingMask); else - mShadowTechnique->disableShadows(); + mShadowTechnique->disableShadows(true); } void ShadowManager::enableOutdoorMode() diff --git a/components/sceneutil/shadow.hpp b/components/sceneutil/shadow.hpp index c823ecf860..4c34944f4a 100644 --- a/components/sceneutil/shadow.hpp +++ b/components/sceneutil/shadow.hpp @@ -1,15 +1,22 @@ #ifndef COMPONENTS_SCENEUTIL_SHADOW_H #define COMPONENTS_SCENEUTIL_SHADOW_H -#include -#include - #include -#include "mwshadowtechnique.hpp" +namespace osg +{ + class StateSet; + class Group; +} +namespace osgShadow +{ + class ShadowSettings; + class ShadowedScene; +} namespace SceneUtil { + class MWShadowTechnique; class ShadowManager { public: @@ -17,7 +24,8 @@ namespace SceneUtil static Shader::ShaderManager::DefineMap getShadowsDisabledDefines(); - ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager); + ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, Shader::ShaderManager &shaderManager); + ~ShadowManager(); void setupShadowSettings(); diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp index 520ad0362f..3e933cbb98 100644 --- a/components/sceneutil/shadowsbin.cpp +++ b/components/sceneutil/shadowsbin.cpp @@ -1,7 +1,9 @@ #include "shadowsbin.hpp" #include #include +#include #include +#include #include using namespace osgUtil; @@ -25,9 +27,9 @@ namespace osg::StateSet::ModeList::const_iterator mf = l.find(mode); if (mf == l.end()) return; - int flags = mf->second; + unsigned int flags = mf->second; bool newValue = flags & osg::StateAttribute::ON; - accumulateState(currentValue, newValue, isOverride, ss->getMode(mode)); + accumulateState(currentValue, newValue, isOverride, flags); } inline bool materialNeedShadows(osg::Material* m) @@ -40,7 +42,7 @@ namespace namespace SceneUtil { -ShadowsBin::ShadowsBin() +ShadowsBin::ShadowsBin(const CastingPrograms& castingPrograms) { mNoTestStateSet = new osg::StateSet; mNoTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); @@ -48,10 +50,17 @@ ShadowsBin::ShadowsBin() mShaderAlphaTestStateSet = new osg::StateSet; mShaderAlphaTestStateSet->addUniform(new osg::Uniform("alphaTestShadows", true)); - mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + mShaderAlphaTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); + mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); + + for (size_t i = 0; i < castingPrograms.size(); ++i) + { + mAlphaFuncShaders[i] = new osg::StateSet; + mAlphaFuncShaders[i]->setAttribute(castingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); + } } -StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::unordered_set& uninterestingCache) +StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::unordered_set& uninterestingCache, bool cullFaceOverridden) { std::vector return_path; State state; @@ -71,7 +80,6 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un continue; accumulateModeState(ss, state.mAlphaBlend, state.mAlphaBlendOverride, GL_BLEND); - accumulateModeState(ss, state.mAlphaTest, state.mAlphaTestOverride, GL_ALPHA_TEST); const osg::StateSet::AttributeList& attributes = ss->getAttributeList(); osg::StateSet::AttributeList::const_iterator found = attributes.find(std::make_pair(osg::StateAttribute::MATERIAL, 0)); @@ -83,10 +91,21 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un state.mMaterial = nullptr; } - // osg::FrontFace specifies triangle winding, not front-face culling. We can't safely reparent anything under it. - found = attributes.find(std::make_pair(osg::StateAttribute::FRONTFACE, 0)); + found = attributes.find(std::make_pair(osg::StateAttribute::ALPHAFUNC, 0)); if (found != attributes.end()) - state.mImportantState = true; + { + // As force shaders is on, we know this is really a RemovedAlphaFunc + const osg::StateSet::RefAttributePair& rap = found->second; + accumulateState(state.mAlphaFunc, static_cast(rap.first.get()), state.mAlphaFuncOverride, rap.second); + } + + if (!cullFaceOverridden) + { + // osg::FrontFace specifies triangle winding, not front-face culling. We can't safely reparent anything under it unless GL_CULL_FACE is off or we flip face culling. + found = attributes.find(std::make_pair(osg::StateAttribute::FRONTFACE, 0)); + if (found != attributes.end()) + state.mImportantState = true; + } if ((*itr) != sg && !state.interesting()) uninterestingCache.insert(*itr); @@ -108,21 +127,38 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un if (state.mAlphaBlend) { sg_new = sg->find_or_insert(mShaderAlphaTestStateSet); - for (RenderLeaf* leaf : sg->_leaves) - { + sg_new->_leaves = std::move(sg->_leaves); + for (RenderLeaf* leaf : sg_new->_leaves) leaf->_parent = sg_new; - sg_new->_leaves.push_back(leaf); - } - return sg_new; + sg = sg_new; + } + + // GL_ALWAYS is set by default by mwshadowtechnique + if (state.mAlphaFunc && state.mAlphaFunc->getFunction() != GL_ALWAYS) + { + sg_new = sg->find_or_insert(mAlphaFuncShaders[state.mAlphaFunc->getFunction() - GL_NEVER]); + sg_new->_leaves = std::move(sg->_leaves); + for (RenderLeaf* leaf : sg_new->_leaves) + leaf->_parent = sg_new; + sg = sg_new; } + return sg; } +inline bool ShadowsBin::State::needTexture() const +{ + return mAlphaBlend || (mAlphaFunc && mAlphaFunc->getFunction() != GL_ALWAYS); +} + bool ShadowsBin::State::needShadows() const { - if (!mMaterial) - return true; - return materialNeedShadows(mMaterial); + if (mAlphaFunc && mAlphaFunc->getFunction() == GL_NEVER) + return false; + // other alpha func + material combinations might be skippable + if (mAlphaBlend && mMaterial) + return materialNeedShadows(mMaterial); + return true; } void ShadowsBin::sortImplementation() @@ -139,13 +175,28 @@ void ShadowsBin::sortImplementation() root = root->_parent; const osg::StateSet* ss = root->getStateSet(); if (ss->getMode(GL_NORMALIZE) & osg::StateAttribute::ON // that is root stategraph of renderingmanager cpp - || ss->getAttribute(osg::StateAttribute::VIEWPORT)) // fallback to rendertargets sg just in case + || ss->getAttribute(osg::StateAttribute::VIEWPORT)) // fallback to rendertarget's sg just in case break; if (!root->_parent) return; } StateGraph* noTestRoot = root->find_or_insert(mNoTestStateSet.get()); - // root is now a stategraph with useDiffuseMapForShadowAlpha disabled but minimal other state + // noTestRoot is now a stategraph with useDiffuseMapForShadowAlpha disabled but minimal other state + + bool cullFaceOverridden = false; + while (root->_parent) + { + root = root->_parent; + if (!root->getStateSet()) + continue; + unsigned int cullFaceFlags = root->getStateSet()->getMode(GL_CULL_FACE); + if (cullFaceFlags & osg::StateAttribute::OVERRIDE && !(cullFaceFlags & osg::StateAttribute::ON)) + { + cullFaceOverridden = true; + break; + } + } + noTestRoot->_leaves.reserve(_stateGraphList.size()); StateGraphList newList; std::unordered_set uninterestingCache; @@ -154,13 +205,13 @@ void ShadowsBin::sortImplementation() // Render leaves which shouldn't use the diffuse map for shadow alpha but do cast shadows become children of root, so graph is now empty. Don't add to newList. // Graphs containing just render leaves which don't cast shadows are discarded. Don't add to newList. // Graphs containing other leaves need to be in newList. - StateGraph* graphToAdd = cullStateGraph(graph, noTestRoot, uninterestingCache); + StateGraph* graphToAdd = cullStateGraph(graph, noTestRoot, uninterestingCache, cullFaceOverridden); if (graphToAdd) newList.push_back(graphToAdd); } if (!noTestRoot->_leaves.empty()) newList.push_back(noTestRoot); - _stateGraphList = newList; + _stateGraphList = std::move(newList); } } diff --git a/components/sceneutil/shadowsbin.hpp b/components/sceneutil/shadowsbin.hpp index cc6fd3525c..2c838d509e 100644 --- a/components/sceneutil/shadowsbin.hpp +++ b/components/sceneutil/shadowsbin.hpp @@ -1,29 +1,33 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_SHADOWBIN_H #define OPENMW_COMPONENTS_SCENEUTIL_SHADOWBIN_H +#include #include #include namespace osg { class Material; + class AlphaFunc; } namespace SceneUtil { - /// renderbin which culls redundant state for shadow map rendering class ShadowsBin : public osgUtil::RenderBin { - private: - osg::ref_ptr mNoTestStateSet; - osg::ref_ptr mShaderAlphaTestStateSet; public: + template + using Array = std::array; + + using CastingPrograms = Array>; + META_Object(SceneUtil, ShadowsBin) - ShadowsBin(); + ShadowsBin(const CastingPrograms& castingPrograms); ShadowsBin(const ShadowsBin& rhs, const osg::CopyOp& copyop) : osgUtil::RenderBin(rhs, copyop) , mNoTestStateSet(rhs.mNoTestStateSet) , mShaderAlphaTestStateSet(rhs.mShaderAlphaTestStateSet) + , mAlphaFuncShaders(rhs.mAlphaFuncShaders) {} void sortImplementation() override; @@ -33,8 +37,8 @@ namespace SceneUtil State() : mAlphaBlend(false) , mAlphaBlendOverride(false) - , mAlphaTest(false) - , mAlphaTestOverride(false) + , mAlphaFunc(nullptr) + , mAlphaFuncOverride(false) , mMaterial(nullptr) , mMaterialOverride(false) , mImportantState(false) @@ -42,35 +46,30 @@ namespace SceneUtil bool mAlphaBlend; bool mAlphaBlendOverride; - bool mAlphaTest; - bool mAlphaTestOverride; + osg::AlphaFunc* mAlphaFunc; + bool mAlphaFuncOverride; osg::Material* mMaterial; bool mMaterialOverride; bool mImportantState; - bool needTexture() const { return mAlphaBlend || mAlphaTest; } + bool needTexture() const; bool needShadows() const; // A state is interesting if there's anything about it that might affect whether we can optimise child state bool interesting() const { - return !needShadows() || needTexture() || mAlphaBlendOverride || mAlphaTestOverride || mMaterialOverride || mImportantState; + return !needShadows() || needTexture() || mAlphaBlendOverride || mAlphaFuncOverride || mMaterialOverride || mImportantState; } }; - osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set& uninteresting); + osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set& uninteresting, bool cullFaceOverridden); - static void addPrototype(const std::string& name) - { - osg::ref_ptr bin (new ShadowsBin); - osgUtil::RenderBin::addRenderBinPrototype(name, bin); - } - }; + private: + ShadowsBin() {} - class ShadowsBinAdder - { - public: - ShadowsBinAdder(const std::string& name){ ShadowsBin::addPrototype(name); } - }; + osg::ref_ptr mNoTestStateSet; + osg::ref_ptr mShaderAlphaTestStateSet; + Array> mAlphaFuncShaders; + }; } #endif diff --git a/components/sceneutil/skeleton.cpp b/components/sceneutil/skeleton.cpp index 40f524e0a2..5c8449a50d 100644 --- a/components/sceneutil/skeleton.cpp +++ b/components/sceneutil/skeleton.cpp @@ -1,35 +1,36 @@ #include "skeleton.hpp" -#include #include #include #include +#include + namespace SceneUtil { class InitBoneCacheVisitor : public osg::NodeVisitor { public: - InitBoneCacheVisitor(std::map >& cache) + typedef std::vector TransformPath; + InitBoneCacheVisitor(std::unordered_map& cache) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCache(cache) { } - void apply(osg::Transform &node) override + void apply(osg::MatrixTransform &node) override { - osg::MatrixTransform* bone = node.asMatrixTransform(); - if (!bone) - return; - - mCache[Misc::StringUtils::lowerCase(bone->getName())] = std::make_pair(getNodePath(), bone); - + mPath.push_back(&node); + mCache[Misc::StringUtils::lowerCase(node.getName())] = mPath; traverse(node); + mPath.pop_back(); } + private: - std::map >& mCache; + TransformPath mPath; + std::unordered_map& mCache; }; Skeleton::Skeleton() @@ -70,34 +71,22 @@ Bone* Skeleton::getBone(const std::string &name) if (!mRootBone.get()) { - mRootBone.reset(new Bone); + mRootBone = std::make_unique(); } - const osg::NodePath& path = found->second.first; Bone* bone = mRootBone.get(); - for (osg::NodePath::const_iterator it = path.begin(); it != path.end(); ++it) + for (osg::MatrixTransform* matrixTransform : found->second) { - osg::MatrixTransform* matrixTransform = dynamic_cast(*it); - if (!matrixTransform) - continue; + const auto it = std::find_if(bone->mChildren.begin(), bone->mChildren.end(), + [&] (const auto& v) { return v->mNode == matrixTransform; }); - Bone* child = nullptr; - for (unsigned int i=0; imChildren.size(); ++i) + if (it == bone->mChildren.end()) { - if (bone->mChildren[i]->mNode == *it) - { - child = bone->mChildren[i]; - break; - } - } - - if (!child) - { - child = new Bone; - bone->mChildren.push_back(child); + bone = bone->mChildren.emplace_back(std::make_unique()).get(); mNeedToUpdateBoneMatrices = true; } - bone = child; + else + bone = it->get(); bone->mNode = matrixTransform; } @@ -116,8 +105,8 @@ void Skeleton::updateBoneMatrices(unsigned int traversalNumber) { if (mRootBone.get()) { - for (unsigned int i=0; imChildren.size(); ++i) - mRootBone->mChildren[i]->update(nullptr); + for (const auto& child : mRootBone->mChildren) + child->update(nullptr); } mNeedToUpdateBoneMatrices = false; @@ -171,13 +160,6 @@ Bone::Bone() { } -Bone::~Bone() -{ - for (unsigned int i=0; igetMatrix(); - for (unsigned int i=0; iupdate(&mMatrixInSkeletonSpace); - } + for (const auto& child : mChildren) + child->update(&mMatrixInSkeletonSpace); } } diff --git a/components/sceneutil/skeleton.hpp b/components/sceneutil/skeleton.hpp index 22988dfd5f..7ca4887999 100644 --- a/components/sceneutil/skeleton.hpp +++ b/components/sceneutil/skeleton.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace SceneUtil { @@ -14,20 +15,15 @@ namespace SceneUtil { public: Bone(); - ~Bone(); osg::Matrixf mMatrixInSkeletonSpace; osg::MatrixTransform* mNode; - std::vector mChildren; + std::vector> mChildren; /// Update the skeleton-space matrix of this bone and all its children. void update(const osg::Matrixf* parentMatrixInSkeletonSpace); - - private: - Bone(const Bone&); - void operator=(const Bone&); }; /// @brief Handles the bone matrices for any number of child RigGeometries. @@ -72,7 +68,7 @@ namespace SceneUtil // As far as the scene graph goes we support multiple root bones. std::unique_ptr mRootBone; - typedef std::map > BoneCache; + typedef std::unordered_map > BoneCache; BoneCache mBoneCache; bool mBoneCacheInit; diff --git a/components/sceneutil/statesetupdater.cpp b/components/sceneutil/statesetupdater.cpp index 5d7dbd7559..9778cf4852 100644 --- a/components/sceneutil/statesetupdater.cpp +++ b/components/sceneutil/statesetupdater.cpp @@ -1,5 +1,7 @@ #include "statesetupdater.hpp" +#include + #include #include #include @@ -10,36 +12,63 @@ namespace SceneUtil void StateSetUpdater::operator()(osg::Node* node, osg::NodeVisitor* nv) { bool isCullVisitor = nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR; - if (!mStateSets[0]) + + if (isCullVisitor) + return applyCull(node, static_cast(nv)); + else + return applyUpdate(node, nv); + } + + void StateSetUpdater::applyUpdate(osg::Node* node, osg::NodeVisitor* nv) + { + if (!mStateSetsUpdate[0]) { - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - if (!isCullVisitor) - mStateSets[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults - else - mStateSets[i] = new osg::StateSet; - setDefaults(mStateSets[i]); + mStateSetsUpdate[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults + setDefaults(mStateSetsUpdate[i]); } } - osg::ref_ptr stateset = mStateSets[nv->getTraversalNumber()%2]; + osg::ref_ptr stateset = mStateSetsUpdate[nv->getTraversalNumber() % 2]; apply(stateset, nv); - - if (!isCullVisitor) - node->setStateSet(stateset); - else - static_cast(nv)->pushStateSet(stateset); - + node->setStateSet(stateset); traverse(node, nv); + } + + void StateSetUpdater::applyCull(osg::Node* node, osgUtil::CullVisitor* cv) + { + auto stateset = getCvDependentStateset(cv); + apply(stateset, cv); + auto& sm = Stereo::Manager::instance(); + if (sm.getEye(cv) == Stereo::Eye::Left) + applyLeft(stateset, cv); + if (sm.getEye(cv) == Stereo::Eye::Right) + applyRight(stateset, cv); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } - if (isCullVisitor) - static_cast(nv)->popStateSet(); + osg::StateSet* StateSetUpdater::getCvDependentStateset(osgUtil::CullVisitor* cv) + { + auto it = mStateSetsCull.find(cv); + if (it == mStateSetsCull.end()) + { + osg::ref_ptr stateset = new osg::StateSet; + mStateSetsCull.emplace(cv, stateset); + setDefaults(stateset); + return stateset; + } + return it->second; } void StateSetUpdater::reset() { - mStateSets[0] = nullptr; - mStateSets[1] = nullptr; + mStateSetsUpdate[0] = nullptr; + mStateSetsUpdate[1] = nullptr; + mStateSetsCull.clear(); } StateSetUpdater::StateSetUpdater() @@ -47,7 +76,7 @@ namespace SceneUtil } StateSetUpdater::StateSetUpdater(const StateSetUpdater ©, const osg::CopyOp ©op) - : osg::NodeCallback(copy, copyop) + : SceneUtil::NodeCallback(copy, copyop) { } diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 25e50acfd2..fb8a9c2fc9 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -1,7 +1,15 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_STATESETCONTROLLER_H #define OPENMW_COMPONENTS_SCENEUTIL_STATESETCONTROLLER_H -#include +#include + +#include +#include + +namespace osgUtil +{ + class CullVisitor; +} namespace SceneUtil { @@ -11,36 +19,47 @@ namespace SceneUtil /// queues up a StateSet that we want to modify for the next frame. To solve this we could set the StateSet to /// DYNAMIC data variance but that would undo all the benefits of the threading model - having the cull and draw /// traversals run in parallel can yield up to 200% framerates. - /// @par Race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, + /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, + /// otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as an UpdateCallback, race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, /// one StateSet we can write to, the second one is currently in use by the draw traversal of the last frame. - /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as a CullCallback, race conditions are prevented by mapping statesets to cull visitors - OSG has two cull visitors that take turns, + /// allowing the updater to automatically scale for the number of views. + /// @note When used as a CullCallback, StateSetUpdater will have no effect on leaf nodes such as osg::Geometry and must be used on branch nodes only. /// @note Do not add the same StateSetUpdater to multiple nodes. - /// @note Do not add multiple StateSetControllers on the same Node as they will conflict - instead use the CompositeStateSetUpdater. - class StateSetUpdater : public osg::NodeCallback + /// @note Do not add multiple StateSetUpdaters on the same Node as they will conflict - instead use the CompositeStateSetUpdater. + class StateSetUpdater : public SceneUtil::NodeCallback { public: StateSetUpdater(); StateSetUpdater(const StateSetUpdater& copy, const osg::CopyOp& copyop); - META_Object(SceneUtil, StateSetUpdater) - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + void operator()(osg::Node* node, osg::NodeVisitor* nv); /// Apply state - to override in derived classes /// @note Due to the double buffering approach you *have* to apply all state /// even if it has not changed since the last frame. virtual void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) {} + /// Apply any state specific to the Left view. Default implementation does nothing. Called after apply() \note requires the updater be a cull callback + virtual void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* nv) {} + /// Apply any state specific to the Right view. Default implementation does nothing. Called after apply() \note requires the updater be a cull callback + virtual void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* nv) {} + /// Set default state - optionally override in derived classes /// @par May be used e.g. to allocate StateAttributes. virtual void setDefaults(osg::StateSet* stateset) {} - - protected: + /// Reset mStateSets, forcing a setDefaults() on the next frame. Can be used to change the defaults if needed. void reset(); private: - osg::ref_ptr mStateSets[2]; + void applyCull(osg::Node* node, osgUtil::CullVisitor* cv); + void applyUpdate(osg::Node* node, osg::NodeVisitor* nv); + osg::StateSet* getCvDependentStateset(osgUtil::CullVisitor* cv); + + std::array, 2> mStateSetsUpdate; + std::map> mStateSetsCull; }; /// @brief A variant of the StateSetController that can be made up of multiple controllers all controlling the same target. diff --git a/components/nifosg/textkeymap.hpp b/components/sceneutil/textkeymap.hpp similarity index 94% rename from components/nifosg/textkeymap.hpp rename to components/sceneutil/textkeymap.hpp index 49e1e461e4..ee58bc72a7 100644 --- a/components/nifosg/textkeymap.hpp +++ b/components/sceneutil/textkeymap.hpp @@ -1,12 +1,12 @@ -#ifndef OPENMW_COMPONENTS_NIFOSG_TEXTKEYMAP -#define OPENMW_COMPONENTS_NIFOSG_TEXTKEYMAP +#ifndef OPENMW_COMPONENTS_SCENEUTIL_TEXTKEYMAP +#define OPENMW_COMPONENTS_SCENEUTIL_TEXTKEYMAP #include #include #include #include -namespace NifOsg +namespace SceneUtil { class TextKeyMap { diff --git a/components/sceneutil/unrefqueue.cpp b/components/sceneutil/unrefqueue.cpp deleted file mode 100644 index 50b23cae92..0000000000 --- a/components/sceneutil/unrefqueue.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "unrefqueue.hpp" - -//#include - -//#include - -namespace SceneUtil -{ - void UnrefWorkItem::doWork() - { - mObjects.clear(); - } - - UnrefQueue::UnrefQueue() - { - mWorkItem = new UnrefWorkItem; - } - - void UnrefQueue::push(const osg::Referenced *obj) - { - mWorkItem->mObjects.emplace_back(obj); - } - - void UnrefQueue::flush(SceneUtil::WorkQueue *workQueue) - { - if (mWorkItem->mObjects.empty()) - return; - - workQueue->addWorkItem(mWorkItem, true); - - mWorkItem = new UnrefWorkItem; - } - - unsigned int UnrefQueue::getNumItems() const - { - return mWorkItem->mObjects.size(); - } - -} diff --git a/components/sceneutil/unrefqueue.hpp b/components/sceneutil/unrefqueue.hpp deleted file mode 100644 index 84372d28c0..0000000000 --- a/components/sceneutil/unrefqueue.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef OPENMW_COMPONENTS_UNREFQUEUE_H -#define OPENMW_COMPONENTS_UNREFQUEUE_H - -#include - -#include -#include - -#include - -namespace SceneUtil -{ - class WorkQueue; - - class UnrefWorkItem : public SceneUtil::WorkItem - { - public: - std::deque > mObjects; - void doWork() override; - }; - - /// @brief Handles unreferencing of objects through the WorkQueue. Typical use scenario - /// would be the main thread pushing objects that are no longer needed, and the background thread deleting them. - class UnrefQueue : public osg::Referenced - { - public: - UnrefQueue(); - - /// Adds an object to the list of objects to be unreferenced. Call from the main thread. - void push(const osg::Referenced* obj); - - /// Adds a WorkItem to the given WorkQueue that will clear the list of objects in a worker thread, thus unreferencing them. - /// Call from the main thread. - void flush(SceneUtil::WorkQueue* workQueue); - - unsigned int getNumItems() const; - - private: - osg::ref_ptr mWorkItem; - }; - -} - -#endif diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index a23f3f1090..52b7be1798 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -8,9 +8,13 @@ #include #include #include +#include +#include +#include #include #include +#include namespace SceneUtil { @@ -112,6 +116,8 @@ void GlowUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) removeTexture(stateset); this->reset(); mDone = true; + // normally done in StateSetUpdater::operator(), but needs doing here so the shader visitor sees the right StateSet + mNode->setStateSet(stateset); mResourceSystem->getSceneManager()->recreateShaders(mNode); } if (mOriginalDuration < 0) // if this glowupdater was originally a permanent glow @@ -146,37 +152,6 @@ void GlowUpdater::setDuration(float duration) mDuration = duration; } -void transformBoundingSphere (const osg::Matrixf& matrix, osg::BoundingSphere& bsphere) -{ - osg::BoundingSphere::vec_type xdash = bsphere._center; - xdash.x() += bsphere._radius; - xdash = xdash*matrix; - - osg::BoundingSphere::vec_type ydash = bsphere._center; - ydash.y() += bsphere._radius; - ydash = ydash*matrix; - - osg::BoundingSphere::vec_type zdash = bsphere._center; - zdash.z() += bsphere._radius; - zdash = zdash*matrix; - - bsphere._center = bsphere._center*matrix; - - xdash -= bsphere._center; - osg::BoundingSphere::value_type sqrlen_xdash = xdash.length2(); - - ydash -= bsphere._center; - osg::BoundingSphere::value_type sqrlen_ydash = ydash.length2(); - - zdash -= bsphere._center; - osg::BoundingSphere::value_type sqrlen_zdash = zdash.length2(); - - bsphere._radius = sqrlen_xdash; - if (bsphere._radius> 0) & 0xFF) / 255.0f, @@ -196,7 +171,7 @@ float makeOsgColorComponent(unsigned int value, unsigned int shift) return float((value >> shift) & 0xFFu) / 255.0f; } -bool hasUserDescription(const osg::Node* node, const std::string pattern) +bool hasUserDescription(const osg::Node* node, const std::string& pattern) { if (node == nullptr) return false; @@ -214,7 +189,7 @@ bool hasUserDescription(const osg::Node* node, const std::string pattern) return false; } -osg::ref_ptr addEnchantedGlow(osg::ref_ptr node, Resource::ResourceSystem* resourceSystem, osg::Vec4f glowColor, float glowDuration) +osg::ref_ptr addEnchantedGlow(osg::ref_ptr node, Resource::ResourceSystem* resourceSystem, const osg::Vec4f& glowColor, float glowDuration) { std::vector > textures; for (int i=0; i<32; ++i) @@ -258,4 +233,37 @@ osg::ref_ptr addEnchantedGlow(osg::ref_ptr node, Resourc return glowUpdater; } +bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture * texture, unsigned int level, unsigned int face, bool mipMapGeneration) +{ + unsigned int samples = 0; + unsigned int colourSamples = 0; + bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1; + if (addMSAAIntermediateTarget) + { + // Alpha-to-coverage requires a multisampled framebuffer. + // OSG will set that up automatically and resolve it to the specified single-sample texture for us. + // For some reason, two samples are needed, at least with some drivers. + samples = 2; + colourSamples = 1; + } + camera->attach(buffer, texture, level, face, mipMapGeneration, samples, colourSamples); + return addMSAAIntermediateTarget; +} + +OperationSequence::OperationSequence(bool keep) + : Operation("OperationSequence", keep) + , mOperationQueue(new osg::OperationQueue()) +{ +} + +void OperationSequence::operator()(osg::Object* object) +{ + mOperationQueue->runOperations(object); +} + +void OperationSequence::add(osg::Operation* operation) +{ + mOperationQueue->add(operation); +} + } diff --git a/components/sceneutil/util.hpp b/components/sceneutil/util.hpp index 303d609f57..18bf8c7728 100644 --- a/components/sceneutil/util.hpp +++ b/components/sceneutil/util.hpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include @@ -49,7 +49,37 @@ namespace SceneUtil // Transform a bounding sphere by a matrix // based off private code in osg::Transform // TODO: patch osg to make public - void transformBoundingSphere (const osg::Matrixf& matrix, osg::BoundingSphere& bsphere); + template + inline void transformBoundingSphere (const osg::Matrixf& matrix, osg::BoundingSphereImpl& bsphere) + { + VT xdash = bsphere._center; + xdash.x() += bsphere._radius; + xdash = xdash*matrix; + + VT ydash = bsphere._center; + ydash.y() += bsphere._radius; + ydash = ydash*matrix; + + VT zdash = bsphere._center; + zdash.z() += bsphere._radius; + zdash = zdash*matrix; + + bsphere._center = bsphere._center*matrix; + + xdash -= bsphere._center; + typename VT::value_type sqrlen_xdash = xdash.length2(); + + ydash -= bsphere._center; + typename VT::value_type sqrlen_ydash = ydash.length2(); + + zdash -= bsphere._center; + typename VT::value_type sqrlen_zdash = zdash.length2(); + + bsphere._radius = sqrlen_xdash; + if (bsphere._radius addEnchantedGlow(osg::ref_ptr node, Resource::ResourceSystem* resourceSystem, osg::Vec4f glowColor, float glowDuration=-1); + osg::ref_ptr addEnchantedGlow(osg::ref_ptr node, Resource::ResourceSystem* resourceSystem, const osg::Vec4f& glowColor, float glowDuration=-1); + + // Alpha-to-coverage requires a multisampled framebuffer, so we need to set that up for RTTs + bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture* texture, unsigned int level = 0, unsigned int face = 0, bool mipMapGeneration = false); + + class OperationSequence : public osg::Operation + { + public: + OperationSequence(bool keep); + + void operator()(osg::Object* object) override; + + void add(osg::Operation* operation); + protected: + osg::ref_ptr mOperationQueue; + }; } #endif diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index 00d18ffcb8..ea09445678 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -9,6 +9,9 @@ #include +#include +#include + namespace SceneUtil { @@ -24,7 +27,7 @@ namespace SceneUtil void FindByClassVisitor::apply(osg::Node &node) { - if (Misc::StringUtils::ciEqual(node.className(), mNameToFind)) + if (Misc::StringUtils::ciEqual(std::string_view(node.className()), mNameToFind)) mFoundNodes.push_back(&node); traverse(node); @@ -32,13 +35,13 @@ namespace SceneUtil void FindByNameVisitor::apply(osg::Group &group) { - if (!checkGroup(group)) + if (!mFoundNode && !checkGroup(group)) traverse(group); } void FindByNameVisitor::apply(osg::MatrixTransform &node) { - if (!checkGroup(node)) + if (!mFoundNode && !checkGroup(node)) traverse(node); } @@ -46,22 +49,19 @@ namespace SceneUtil { } - void DisableFreezeOnCullVisitor::apply(osg::MatrixTransform &node) - { - traverse(node); - } - - void DisableFreezeOnCullVisitor::apply(osg::Drawable& drw) - { - if (osgParticle::ParticleSystem* partsys = dynamic_cast(&drw)) - partsys->setFreezeOnCull(false); - } - void NodeMapVisitor::apply(osg::MatrixTransform& trans) { - // Take transformation for first found node in file - const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName()); - mMap.emplace(nodeName, &trans); + // Choose first found node in file + + if (trans.libraryName() == std::string("osgAnimation")) + { + std::string nodeName = trans.getName(); + // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses whitespace-separated names) + std::replace(nodeName.begin(), nodeName.end(), '_', ' '); + mMap.emplace(nodeName, &trans); + } + else + mMap.emplace(trans.getName(), &trans); traverse(trans); } diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index 5e041dc454..45aa408b9e 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -4,6 +4,10 @@ #include #include +#include + +#include + // Commonly used scene graph visitors namespace SceneUtil { @@ -45,25 +49,11 @@ namespace SceneUtil std::vector mFoundNodes; }; - // Disable freezeOnCull for all visited particlesystems - class DisableFreezeOnCullVisitor : public osg::NodeVisitor - { - public: - DisableFreezeOnCullVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - - void apply(osg::MatrixTransform& node) override; - - void apply(osg::Drawable& drw) override; - }; - /// Maps names to nodes class NodeMapVisitor : public osg::NodeVisitor { public: - typedef std::map > NodeMap; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; NodeMapVisitor(NodeMap& map) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) diff --git a/components/sceneutil/waterutil.cpp b/components/sceneutil/waterutil.cpp index 8a434105c8..10a0d4e885 100644 --- a/components/sceneutil/waterutil.cpp +++ b/components/sceneutil/waterutil.cpp @@ -5,6 +5,8 @@ #include #include +#include "depth.hpp" + namespace SceneUtil { // disable nonsense test against a worldsize bb what will always pass @@ -76,15 +78,12 @@ namespace SceneUtil stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - osg::ref_ptr depth (new osg::Depth); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); stateset->setRenderBinDetails(renderBin, "RenderBin"); - // Let the shader know we're dealing with simple water here. - stateset->addUniform(new osg::Uniform("simpleWater", true)); - return stateset; } } diff --git a/components/sceneutil/workqueue.cpp b/components/sceneutil/workqueue.cpp index 3f8ed8aaf2..eb7a7b2cae 100644 --- a/components/sceneutil/workqueue.cpp +++ b/components/sceneutil/workqueue.cpp @@ -33,14 +33,28 @@ bool WorkItem::isDone() const return mDone; } -WorkQueue::WorkQueue(int workerThreads) +WorkQueue::WorkQueue(std::size_t workerThreads) : mIsReleased(false) { - for (int i=0; i(*this)); + start(workerThreads); } WorkQueue::~WorkQueue() +{ + stop(); +} + +void WorkQueue::start(std::size_t workerThreads) +{ + { + const std::lock_guard lock(mMutex); + mIsReleased = false; + } + while (mThreads.size() < workerThreads) + mThreads.emplace_back(std::make_unique(*this)); +} + +void WorkQueue::stop() { { std::unique_lock lock(mMutex); @@ -63,9 +77,9 @@ void WorkQueue::addWorkItem(osg::ref_ptr item, bool front) std::unique_lock lock(mMutex); if (front) - mQueue.push_front(item); + mQueue.push_front(std::move(item)); else - mQueue.push_back(item); + mQueue.push_back(std::move(item)); mCondition.notify_one(); } @@ -78,12 +92,11 @@ osg::ref_ptr WorkQueue::removeWorkItem() } if (!mQueue.empty()) { - osg::ref_ptr item = mQueue.front(); + osg::ref_ptr item = std::move(mQueue.front()); mQueue.pop_front(); return item; } - else - return nullptr; + return nullptr; } unsigned int WorkQueue::getNumItems() const diff --git a/components/sceneutil/workqueue.hpp b/components/sceneutil/workqueue.hpp index 5b51c59e59..6eb6a36a3e 100644 --- a/components/sceneutil/workqueue.hpp +++ b/components/sceneutil/workqueue.hpp @@ -44,9 +44,13 @@ namespace SceneUtil class WorkQueue : public osg::Referenced { public: - WorkQueue(int numWorkerThreads=1); + WorkQueue(std::size_t workerThreads); ~WorkQueue(); + void start(std::size_t workerThreads); + + void stop(); + /// Add a new work item to the back of the queue. /// @par The work item's waitTillDone() method may be used by the caller to wait until the work is complete. /// @param front If true, add item to the front of the queue. If false (default), add to the back. diff --git a/components/sceneutil/writescene.cpp b/components/sceneutil/writescene.cpp index 6be963ef2e..d243f521c0 100644 --- a/components/sceneutil/writescene.cpp +++ b/components/sceneutil/writescene.cpp @@ -1,10 +1,10 @@ #include "writescene.hpp" #include +#include #include -#include #include "serialize.hpp" @@ -16,7 +16,7 @@ void SceneUtil::writeScene(osg::Node *node, const std::string& filename, const s if (!rw) throw std::runtime_error("can not find readerwriter for " + format); - boost::filesystem::ofstream stream; + std::ofstream stream; stream.open(filename); osg::ref_ptr options = new osgDB::Options; diff --git a/components/sdlutil/events.hpp b/components/sdlutil/events.hpp index a0dd11acec..ce71f04457 100644 --- a/components/sdlutil/events.hpp +++ b/components/sdlutil/events.hpp @@ -1,6 +1,7 @@ #ifndef _SFO_EVENTS_H #define _SFO_EVENTS_H +#include #include #include @@ -18,6 +19,24 @@ struct MouseMotionEvent : SDL_MouseMotionEvent { Sint32 z; }; +struct TouchEvent { + int mDevice; + int mFinger; + float mX; + float mY; + float mPressure; + + #if SDL_VERSION_ATLEAST(2, 0, 14) + explicit TouchEvent(const SDL_ControllerTouchpadEvent& arg) + : mDevice(arg.touchpad) + , mFinger(arg.finger) + , mX(arg.x) + , mY(arg.y) + , mPressure(arg.pressure) + {} + #endif +}; + /////////////// // Listeners // @@ -50,25 +69,24 @@ public: virtual void keyReleased(const SDL_KeyboardEvent &arg) = 0; }; + class ControllerListener { public: virtual ~ControllerListener() {} - /** @remarks Joystick button down event */ - virtual void buttonPressed(int deviceID, const SDL_ControllerButtonEvent &evt) = 0; - /** @remarks Joystick button up event */ + virtual void buttonPressed(int deviceID, const SDL_ControllerButtonEvent &evt) = 0; virtual void buttonReleased(int deviceID, const SDL_ControllerButtonEvent &evt) = 0; - /** @remarks Joystick axis moved event */ virtual void axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) = 0; - /** @remarks Joystick Added **/ virtual void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) = 0; - - /** @remarks Joystick Removed **/ virtual void controllerRemoved(const SDL_ControllerDeviceEvent &arg) = 0; + virtual void touchpadMoved(int deviceId, const TouchEvent& arg) = 0; + virtual void touchpadPressed(int deviceId, const TouchEvent& arg) = 0; + virtual void touchpadReleased(int deviceId, const TouchEvent& arg) = 0; + }; class WindowListener diff --git a/components/sdlutil/gl4es_init.cpp b/components/sdlutil/gl4es_init.cpp new file mode 100644 index 0000000000..bf9e007b65 --- /dev/null +++ b/components/sdlutil/gl4es_init.cpp @@ -0,0 +1,36 @@ +// EGL does not work reliably for feature detection. +// Instead, we initialize gl4es manually. +#ifdef OPENMW_GL4ES_MANUAL_INIT +#include "gl4es_init.h" + +// For glHint +#include + +extern "C" { + +#include +#include + +static SDL_Window *gWindow; + +void openmw_gl4es_GetMainFBSize(int *width, int *height) +{ + SDL_GetWindowSize(gWindow, width, height); +} + +void openmw_gl4es_init(SDL_Window *window) +{ + gWindow = window; + set_getprocaddress(SDL_GL_GetProcAddress); + set_getmainfbsize(openmw_gl4es_GetMainFBSize); + initialize_gl4es(); + + // merge glBegin/glEnd in beams and console + glHint(GL_BEGINEND_HINT_GL4ES, 1); + // dxt unpacked to 16-bit looks ugly + glHint(GL_AVOID16BITS_HINT_GL4ES, 1); +} + +} // extern "C" + +#endif // OPENMW_GL4ES_MANUAL_INIT diff --git a/components/sdlutil/gl4es_init.h b/components/sdlutil/gl4es_init.h new file mode 100644 index 0000000000..11371e7aef --- /dev/null +++ b/components/sdlutil/gl4es_init.h @@ -0,0 +1,13 @@ +#ifndef OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H +#define OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H +#ifdef OPENMW_GL4ES_MANUAL_INIT +#include + +// Must be called once SDL video mode has been set, +// which creates a context. +// +// GL4ES can then query the context for features and extensions. +extern "C" void openmw_gl4es_init(SDL_Window *window); + +#endif // OPENMW_GL4ES_MANUAL_INIT +#endif // OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index 82854cb2f8..626930291d 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -20,7 +20,7 @@ #include "imagetosurface.hpp" -#if defined(OSG_LIBRARY_STATIC) && !defined(ANDROID) +#if defined(OSG_LIBRARY_STATIC) && (!defined(ANDROID) || OSG_VERSION_GREATER_THAN(3, 6, 5)) // Sets the default windowing system interface according to the OS. // Necessary for OpenSceneGraph to do some things, like decompression. USE_GRAPHICSWINDOW() @@ -50,7 +50,7 @@ namespace CursorDecompression traits->alpha = 8; traits->windowDecoration = false; traits->doubleBuffer = false; - traits->sharedContext = 0; + traits->sharedContext = nullptr; traits->pbuffer = true; osg::GraphicsContext::ScreenIdentifier si; @@ -267,7 +267,7 @@ namespace SDLUtil if (mCursorMap.find(name) != mCursorMap.end()) return; - static bool forceSoftwareDecompression = (getenv("OPENMW_DECOMPRESS_TEXTURES") != 0); + static bool forceSoftwareDecompression = (getenv("OPENMW_DECOMPRESS_TEXTURES") != nullptr); SurfaceUniquePtr (*decompressionFunction)(osg::ref_ptr, float); if (forceSoftwareDecompression || CursorDecompression::DXTCSupported) { diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index 0a19517001..43284c216d 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -2,6 +2,10 @@ #include +#ifdef OPENMW_GL4ES_MANUAL_INIT +#include "gl4es_init.h" +#endif + namespace SDLUtil { @@ -11,8 +15,8 @@ GraphicsWindowSDL2::~GraphicsWindowSDL2() } GraphicsWindowSDL2::GraphicsWindowSDL2(osg::GraphicsContext::Traits *traits) - : mWindow(0) - , mContext(0) + : mWindow(nullptr) + , mContext(nullptr) , mValid(false) , mRealized(false) , mOwnsWindow(false) @@ -79,7 +83,7 @@ void GraphicsWindowSDL2::init() WindowData *inheritedWindowData = dynamic_cast(_traits->inheritedWindowData.get()); mWindow = inheritedWindowData ? inheritedWindowData->mWindow : nullptr; - mOwnsWindow = (mWindow == 0); + mOwnsWindow = (mWindow == nullptr); if(mOwnsWindow) { OSG_FATAL<<"Error: No SDL window provided."<vsync); // Update traits with what we've actually been given diff --git a/components/sdlutil/sdlgraphicswindow.hpp b/components/sdlutil/sdlgraphicswindow.hpp index 01a97e7545..559deda675 100644 --- a/components/sdlutil/sdlgraphicswindow.hpp +++ b/components/sdlutil/sdlgraphicswindow.hpp @@ -24,7 +24,7 @@ class GraphicsWindowSDL2 : public osgViewer::GraphicsWindow public: GraphicsWindowSDL2(osg::GraphicsContext::Traits *traits); - bool isSameKindAs(const Object* object) const override { return dynamic_cast(object)!=0; } + bool isSameKindAs(const Object* object) const override { return dynamic_cast(object)!=nullptr; } const char* libraryName() const override { return "osgViewer"; } const char* className() const override { return "GraphicsWindowSDL2"; } diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 8d6a124e2c..3bd74f569a 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -52,9 +52,14 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v if (windowEventsOnly) { - // During loading, just handle window events, and keep others for later + // During loading, handle window events, discard button presses and keep others for later while (SDL_PeepEvents(&evt, 1, SDL_GETEVENT, SDL_WINDOWEVENT, SDL_WINDOWEVENT)) handleWindowEvent(evt); + + SDL_FlushEvent(SDL_KEYDOWN); + SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN); + SDL_FlushEvent(SDL_MOUSEBUTTONDOWN); + return; } @@ -141,6 +146,20 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v if(mConListener) mConListener->axisMoved(1, evt.caxis); break; + #if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + // controller sensor data is received on demand + break; + case SDL_CONTROLLERTOUCHPADDOWN: + mConListener->touchpadPressed(1, TouchEvent(evt.ctouchpad)); + break; + case SDL_CONTROLLERTOUCHPADMOTION: + mConListener->touchpadMoved(1, TouchEvent(evt.ctouchpad)); + break; + case SDL_CONTROLLERTOUCHPADUP: + mConListener->touchpadReleased(1, TouchEvent(evt.ctouchpad)); + break; + #endif case SDL_WINDOWEVENT: handleWindowEvent(evt); break; @@ -361,13 +380,10 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v /// \brief Package mouse and mousewheel motions into a single event MouseMotionEvent InputWrapper::_packageMouseMotion(const SDL_Event &evt) { - MouseMotionEvent pack_evt; + MouseMotionEvent pack_evt = {}; pack_evt.x = mMouseX; - pack_evt.xrel = 0; pack_evt.y = mMouseY; - pack_evt.yrel = 0; pack_evt.z = mMouseZ; - pack_evt.zrel = 0; if(evt.type == SDL_MOUSEMOTION) { @@ -375,6 +391,7 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v pack_evt.y = mMouseY = evt.motion.y; pack_evt.xrel = evt.motion.xrel; pack_evt.yrel = evt.motion.yrel; + pack_evt.type = SDL_MOUSEMOTION; if (mFirstMouseMove) { // first event should be treated as non-relative, since there's no point of reference @@ -387,6 +404,7 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v { mMouseZ += pack_evt.zrel = (evt.wheel.y * 120); pack_evt.z = mMouseZ; + pack_evt.type = SDL_MOUSEWHEEL; } else { diff --git a/components/sdlutil/sdlmappings.cpp b/components/sdlutil/sdlmappings.cpp new file mode 100644 index 0000000000..8306909ee5 --- /dev/null +++ b/components/sdlutil/sdlmappings.cpp @@ -0,0 +1,251 @@ +#include "sdlmappings.hpp" + +#include + +#include + +#include +#include + +namespace SDLUtil +{ + std::string sdlControllerButtonToString(int button) + { + switch(button) + { + case SDL_CONTROLLER_BUTTON_A: + return "A Button"; + case SDL_CONTROLLER_BUTTON_B: + return "B Button"; + case SDL_CONTROLLER_BUTTON_BACK: + return "Back Button"; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return "DPad Down"; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return "DPad Left"; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return "DPad Right"; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + return "DPad Up"; + case SDL_CONTROLLER_BUTTON_GUIDE: + return "Guide Button"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return "Left Shoulder"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return "Left Stick Button"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return "Right Shoulder"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return "Right Stick Button"; + case SDL_CONTROLLER_BUTTON_START: + return "Start Button"; + case SDL_CONTROLLER_BUTTON_X: + return "X Button"; + case SDL_CONTROLLER_BUTTON_Y: + return "Y Button"; + default: + return "Button " + std::to_string(button); + } + } + + std::string sdlControllerAxisToString(int axis) + { + switch(axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + return "Left Stick X"; + case SDL_CONTROLLER_AXIS_LEFTY: + return "Left Stick Y"; + case SDL_CONTROLLER_AXIS_RIGHTX: + return "Right Stick X"; + case SDL_CONTROLLER_AXIS_RIGHTY: + return "Right Stick Y"; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return "Left Trigger"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return "Right Trigger"; + default: + return "Axis " + std::to_string(axis); + } + } + + MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button) + { + //The right button is the second button, according to MyGUI + if(button == SDL_BUTTON_RIGHT) + button = SDL_BUTTON_MIDDLE; + else if(button == SDL_BUTTON_MIDDLE) + button = SDL_BUTTON_RIGHT; + + //MyGUI's buttons are 0 indexed + return MyGUI::MouseButton::Enum(button - 1); + } + + Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button) + { + Uint8 value = button.getValue() + 1; + if (value == SDL_BUTTON_RIGHT) + value = SDL_BUTTON_MIDDLE; + else if (value == SDL_BUTTON_MIDDLE) + value = SDL_BUTTON_RIGHT; + return value; + } + + namespace + { + std::map initKeyMap() + { + std::map keyMap; + keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None; + keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape; + keyMap[SDLK_1] = MyGUI::KeyCode::One; + keyMap[SDLK_2] = MyGUI::KeyCode::Two; + keyMap[SDLK_3] = MyGUI::KeyCode::Three; + keyMap[SDLK_4] = MyGUI::KeyCode::Four; + keyMap[SDLK_5] = MyGUI::KeyCode::Five; + keyMap[SDLK_6] = MyGUI::KeyCode::Six; + keyMap[SDLK_7] = MyGUI::KeyCode::Seven; + keyMap[SDLK_8] = MyGUI::KeyCode::Eight; + keyMap[SDLK_9] = MyGUI::KeyCode::Nine; + keyMap[SDLK_0] = MyGUI::KeyCode::Zero; + keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus; + keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals; + keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace; + keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab; + keyMap[SDLK_q] = MyGUI::KeyCode::Q; + keyMap[SDLK_w] = MyGUI::KeyCode::W; + keyMap[SDLK_e] = MyGUI::KeyCode::E; + keyMap[SDLK_r] = MyGUI::KeyCode::R; + keyMap[SDLK_t] = MyGUI::KeyCode::T; + keyMap[SDLK_y] = MyGUI::KeyCode::Y; + keyMap[SDLK_u] = MyGUI::KeyCode::U; + keyMap[SDLK_i] = MyGUI::KeyCode::I; + keyMap[SDLK_o] = MyGUI::KeyCode::O; + keyMap[SDLK_p] = MyGUI::KeyCode::P; + keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return; + keyMap[SDLK_a] = MyGUI::KeyCode::A; + keyMap[SDLK_s] = MyGUI::KeyCode::S; + keyMap[SDLK_d] = MyGUI::KeyCode::D; + keyMap[SDLK_f] = MyGUI::KeyCode::F; + keyMap[SDLK_g] = MyGUI::KeyCode::G; + keyMap[SDLK_h] = MyGUI::KeyCode::H; + keyMap[SDLK_j] = MyGUI::KeyCode::J; + keyMap[SDLK_k] = MyGUI::KeyCode::K; + keyMap[SDLK_l] = MyGUI::KeyCode::L; + keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon; + keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe; + keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave; + keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift; + keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash; + keyMap[SDLK_z] = MyGUI::KeyCode::Z; + keyMap[SDLK_x] = MyGUI::KeyCode::X; + keyMap[SDLK_c] = MyGUI::KeyCode::C; + keyMap[SDLK_v] = MyGUI::KeyCode::V; + keyMap[SDLK_b] = MyGUI::KeyCode::B; + keyMap[SDLK_n] = MyGUI::KeyCode::N; + keyMap[SDLK_m] = MyGUI::KeyCode::M; + keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma; + keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period; + keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash; + keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift; + keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply; + keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt; + keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space; + keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital; + keyMap[SDLK_F1] = MyGUI::KeyCode::F1; + keyMap[SDLK_F2] = MyGUI::KeyCode::F2; + keyMap[SDLK_F3] = MyGUI::KeyCode::F3; + keyMap[SDLK_F4] = MyGUI::KeyCode::F4; + keyMap[SDLK_F5] = MyGUI::KeyCode::F5; + keyMap[SDLK_F6] = MyGUI::KeyCode::F6; + keyMap[SDLK_F7] = MyGUI::KeyCode::F7; + keyMap[SDLK_F8] = MyGUI::KeyCode::F8; + keyMap[SDLK_F9] = MyGUI::KeyCode::F9; + keyMap[SDLK_F10] = MyGUI::KeyCode::F10; + keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock; + keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock; + keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7; + keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8; + keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9; + keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract; + keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4; + keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5; + keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6; + keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add; + keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1; + keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2; + keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3; + keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0; + keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal; + keyMap[SDLK_F11] = MyGUI::KeyCode::F11; + keyMap[SDLK_F12] = MyGUI::KeyCode::F12; + keyMap[SDLK_F13] = MyGUI::KeyCode::F13; + keyMap[SDLK_F14] = MyGUI::KeyCode::F14; + keyMap[SDLK_F15] = MyGUI::KeyCode::F15; + keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals; + keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon; + keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter; + keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide; + keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq; + keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt; + keyMap[SDLK_HOME] = MyGUI::KeyCode::Home; + keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp; + keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp; + keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft; + keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight; + keyMap[SDLK_END] = MyGUI::KeyCode::End; + keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown; + keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown; + keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert; + keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete; + keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu; + + //The function of the Ctrl and Meta keys are switched on macOS compared to other platforms. + //For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard + #if defined(__APPLE__) + keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl; + keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl; + keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows; + keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows; + #else + keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows; + keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows; + keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl; + keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl; + #endif + return keyMap; + } + + std::map reverseKeyMap(const std::map& map) + { + std::map result; + for (auto [sdl, mygui] : map) + result[mygui] = sdl; + return result; + } + } + + MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code) + { + static std::map keyMap = initKeyMap(); + + MyGUI::KeyCode kc = MyGUI::KeyCode::None; + auto foundKey = keyMap.find(code); + if (foundKey != keyMap.end()) + kc = foundKey->second; + + return kc; + } + + SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button) + { + static auto keyMap = reverseKeyMap(initKeyMap()); + + SDL_Keycode kc = 0; + auto foundKey = keyMap.find(button); + if (foundKey != keyMap.end()) + kc = foundKey->second; + + return kc; + } +} diff --git a/apps/openmw/mwinput/sdlmappings.hpp b/components/sdlutil/sdlmappings.hpp similarity index 50% rename from apps/openmw/mwinput/sdlmappings.hpp rename to components/sdlutil/sdlmappings.hpp index 0cdd4694f5..3625075009 100644 --- a/apps/openmw/mwinput/sdlmappings.hpp +++ b/components/sdlutil/sdlmappings.hpp @@ -1,5 +1,5 @@ -#ifndef MWINPUT_SDLMAPPINGS_H -#define MWINPUT_SDLMAPPINGS_H +#ifndef SDLUTIL_SDLMAPPINGS +#define SDLUTIL_SDLMAPPINGS #include @@ -12,14 +12,16 @@ namespace MyGUI struct MouseButton; } -namespace MWInput +namespace SDLUtil { std::string sdlControllerButtonToString(int button); std::string sdlControllerAxisToString(int axis); - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button); + MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button); + Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button); MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code); + SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button); } -#endif +#endif // !SDLUTIL_SDLMAPPINGS diff --git a/components/sdlutil/sdlvideowrapper.cpp b/components/sdlutil/sdlvideowrapper.cpp index b3ba98ee3c..7d62979e7f 100644 --- a/components/sdlutil/sdlvideowrapper.cpp +++ b/components/sdlutil/sdlvideowrapper.cpp @@ -1,6 +1,7 @@ #include "sdlvideowrapper.hpp" #include +#include #include @@ -68,21 +69,21 @@ namespace SDLUtil Log(Debug::Warning) << "Couldn't set gamma: " << SDL_GetError(); } - void VideoWrapper::setVideoMode(int width, int height, bool fullscreen, bool windowBorder) + void VideoWrapper::setVideoMode(int width, int height, Settings::WindowMode windowMode, bool windowBorder) { SDL_SetWindowFullscreen(mWindow, 0); if (SDL_GetWindowFlags(mWindow) & SDL_WINDOW_MAXIMIZED) SDL_RestoreWindow(mWindow); - if (fullscreen) + if (windowMode == Settings::WindowMode::Fullscreen || windowMode == Settings::WindowMode::WindowedFullscreen) { SDL_DisplayMode mode; SDL_GetWindowDisplayMode(mWindow, &mode); mode.w = width; mode.h = height; SDL_SetWindowDisplayMode(mWindow, &mode); - SDL_SetWindowFullscreen(mWindow, fullscreen); + SDL_SetWindowFullscreen(mWindow, windowMode == Settings::WindowMode::Fullscreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP); } else { diff --git a/components/sdlutil/sdlvideowrapper.hpp b/components/sdlutil/sdlvideowrapper.hpp index 3866c3ec31..b9bcd66b5c 100644 --- a/components/sdlutil/sdlvideowrapper.hpp +++ b/components/sdlutil/sdlvideowrapper.hpp @@ -12,6 +12,11 @@ namespace osgViewer class Viewer; } +namespace Settings +{ + enum class WindowMode; +} + namespace SDLUtil { @@ -25,7 +30,7 @@ namespace SDLUtil void setGammaContrast(float gamma, float contrast); - void setVideoMode(int width, int height, bool fullscreen, bool windowBorder); + void setVideoMode(int width, int height, Settings::WindowMode windowMode, bool windowBorder); void centerWindow(); diff --git a/components/serialization/binaryreader.hpp b/components/serialization/binaryreader.hpp new file mode 100644 index 0000000000..79776a26a7 --- /dev/null +++ b/components/serialization/binaryreader.hpp @@ -0,0 +1,79 @@ +#ifndef OPENMW_COMPONENTS_SERIALIZATION_BINARYREADER_H +#define OPENMW_COMPONENTS_SERIALIZATION_BINARYREADER_H + +#include + +#include +#include +#include +#include +#include +#include + +namespace Serialization +{ + struct NotEnoughData : std::runtime_error + { + NotEnoughData() : std::runtime_error("Not enough data") {} + }; + + class BinaryReader + { + public: + explicit BinaryReader(const std::byte* pos, const std::byte* end) + : mPos(pos), mEnd(end) + { + assert(mPos <= mEnd); + } + + BinaryReader(const BinaryReader&) = delete; + + template + void operator()(Format&& format, T& value) + { + if constexpr (std::is_enum_v) + (*this)(std::forward(format), static_cast&>(value)); + else if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(sizeof(T))) + throw NotEnoughData(); + std::memcpy(&value, mPos, sizeof(T)); + mPos += sizeof(T); + value = Misc::toLittleEndian(value); + } + else if constexpr (std::is_pointer_v) + value = reinterpret_cast(mPos); + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, T* data, std::size_t count) + { + if constexpr (std::is_enum_v) + (*this)(std::forward(format), reinterpret_cast*>(data), count); + else if constexpr (std::is_arithmetic_v) + { + const std::size_t size = sizeof(T) * count; + if (mEnd - mPos < static_cast(size)) + throw NotEnoughData(); + std::memcpy(data, mPos, size); + mPos += size; + if constexpr (!Misc::IS_LITTLE_ENDIAN) + std::for_each(data, data + count, [&] (T& v) { v = Misc::fromLittleEndian(v); }); + } + else + { + format(*this, data, count); + } + } + + private: + const std::byte* mPos; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/serialization/binarywriter.hpp b/components/serialization/binarywriter.hpp new file mode 100644 index 0000000000..64ec00d5bf --- /dev/null +++ b/components/serialization/binarywriter.hpp @@ -0,0 +1,86 @@ +#ifndef OPENMW_COMPONENTS_SERIALIZATION_BINARYWRITER_H +#define OPENMW_COMPONENTS_SERIALIZATION_BINARYWRITER_H + +#include + +#include +#include +#include +#include +#include +#include + +namespace Serialization +{ + struct NotEnoughSpace : std::runtime_error + { + NotEnoughSpace() : std::runtime_error("Not enough space") {} + }; + + struct BinaryWriter + { + public: + explicit BinaryWriter(std::byte* dest, const std::byte* end) + : mDest(dest), mEnd(end) + { + assert(mDest <= mEnd); + } + + BinaryWriter(const BinaryWriter&) = delete; + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_enum_v) + (*this)(std::forward(format), static_cast>(value)); + else if constexpr (std::is_arithmetic_v) + { + if (mEnd - mDest < static_cast(sizeof(T))) + throw NotEnoughSpace(); + writeValue(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_enum_v) + (*this)(std::forward(format), reinterpret_cast*>(data), count); + else if constexpr (std::is_arithmetic_v) + { + const std::size_t size = sizeof(T) * count; + if (mEnd - mDest < static_cast(size)) + throw NotEnoughSpace(); + if constexpr (Misc::IS_LITTLE_ENDIAN) + { + std::memcpy(mDest, data, size); + mDest += size; + } + else + std::for_each(data, data + count, [&] (const T& v) { writeValue(v); }); + } + else + { + format(*this, data, count); + } + } + + private: + std::byte* mDest; + const std::byte* const mEnd; + + template + void writeValue(const T& value) noexcept + { + T coverted = Misc::toLittleEndian(value); + std::memcpy(mDest, &coverted, sizeof(T)); + mDest += sizeof(T); + } + }; +} + +#endif diff --git a/components/serialization/format.hpp b/components/serialization/format.hpp new file mode 100644 index 0000000000..29fc0ec42d --- /dev/null +++ b/components/serialization/format.hpp @@ -0,0 +1,70 @@ +#ifndef OPENMW_COMPONENTS_SERIALIZATION_FORMAT_H +#define OPENMW_COMPONENTS_SERIALIZATION_FORMAT_H + +#include +#include +#include +#include +#include +#include +#include + +namespace Serialization +{ + enum class Mode + { + Read, + Write, + }; + + template + struct IsContiguousContainer : std::false_type {}; + + template + struct IsContiguousContainer> : std::true_type {}; + + template + struct IsContiguousContainer> : std::true_type {}; + + template + inline constexpr bool isContiguousContainer = IsContiguousContainer>::value; + + template + struct Format + { + template + void operator()(Visitor&& visitor, T* data, std::size_t size) const + { + if constexpr (std::is_arithmetic_v || std::is_enum_v) + visitor(self(), data, size); + else + std::for_each(data, data + size, [&] (auto& v) { visitor(self(), v); }); + } + + template + void operator()(Visitor&& visitor, T(& data)[size]) const + { + self()(std::forward(visitor), data, size); + } + + template + auto operator()(Visitor&& visitor, T&& value) const + -> std::enable_if_t> + { + if constexpr (mode == Mode::Write) + visitor(self(), static_cast(value.size())); + else + { + static_assert(mode == Mode::Read); + std::uint64_t size = 0; + visitor(self(), size); + value.resize(static_cast(size)); + } + self()(std::forward(visitor), value.data(), value.size()); + } + + const Derived& self() const { return static_cast(*this); } + }; +} + +#endif diff --git a/components/serialization/osgyaml.hpp b/components/serialization/osgyaml.hpp new file mode 100644 index 0000000000..866d1db2fe --- /dev/null +++ b/components/serialization/osgyaml.hpp @@ -0,0 +1,64 @@ +#ifndef OPENMW_COMPONENTS_SERIALIZATION_OSGYAML_H +#define OPENMW_COMPONENTS_SERIALIZATION_OSGYAML_H + +#include + +#include +#include +#include + +namespace Serialization +{ + template + YAML::Node encodeOSGVec(const OSGVec& rhs) + { + YAML::Node node; + for (int i = 0; i < OSGVec::num_components; ++i) + node.push_back(rhs[i]); + + return node; + } + + template + bool decodeOSGVec(const YAML::Node& node, OSGVec& rhs) + { + if (!node.IsSequence() || node.size() != OSGVec::num_components) + return false; + + for (int i = 0; i < OSGVec::num_components; ++i) + rhs[i] = node[i].as(); + + return true; + } +} + +namespace YAML +{ + + template<> + struct convert + { + static Node encode(const osg::Vec2f& rhs) { return Serialization::encodeOSGVec(rhs); } + + static bool decode(const Node& node, osg::Vec2f& rhs) { return Serialization::decodeOSGVec(node, rhs); } + }; + + template<> + struct convert + { + static Node encode(const osg::Vec3f& rhs) { return Serialization::encodeOSGVec(rhs); } + + static bool decode(const Node& node, osg::Vec3f& rhs) { return Serialization::decodeOSGVec(node, rhs); } + }; + + template<> + struct convert + { + static Node encode(const osg::Vec4f& rhs) { return Serialization::encodeOSGVec(rhs); } + + static bool decode(const Node& node, osg::Vec4f& rhs) { return Serialization::decodeOSGVec(node, rhs); } + }; + +} + +#endif \ No newline at end of file diff --git a/components/serialization/sizeaccumulator.hpp b/components/serialization/sizeaccumulator.hpp new file mode 100644 index 0000000000..4dcc004f73 --- /dev/null +++ b/components/serialization/sizeaccumulator.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_SERIALIZATION_SIZEACCUMULATOR_H +#define OPENMW_COMPONENTS_SERIALIZATION_SIZEACCUMULATOR_H + +#include +#include + +namespace Serialization +{ + class SizeAccumulator + { + public: + SizeAccumulator() = default; + + SizeAccumulator(const SizeAccumulator&) = delete; + + std::size_t value() const { return mValue; } + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v || std::is_enum_v) + mValue += sizeof(T); + else + format(*this, value); + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v || std::is_enum_v) + mValue += count * sizeof(T); + else + format(*this, data, count); + } + + private: + std::size_t mValue = 0; + }; +} + +#endif diff --git a/components/settings/categories.hpp b/components/settings/categories.hpp index d6cd042f61..246d6c2a44 100644 --- a/components/settings/categories.hpp +++ b/components/settings/categories.hpp @@ -5,12 +5,24 @@ #include #include #include +#include namespace Settings { + struct Less + { + using is_transparent = void; + + bool operator()(const std::pair& l, + const std::pair& r) const + { + return l < r; + } + }; + using CategorySetting = std::pair; - using CategorySettingVector = std::set>; - using CategorySettingValueMap = std::map; + using CategorySettingVector = std::set; + using CategorySettingValueMap = std::map; } #endif // COMPONENTS_SETTINGS_CATEGORIES_H diff --git a/components/settings/parser.cpp b/components/settings/parser.cpp index 9693bf5117..778888d2d2 100644 --- a/components/settings/parser.cpp +++ b/components/settings/parser.cpp @@ -5,21 +5,40 @@ #include #include -#include +#include +#include -void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings) +#include + +void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, + bool base64Encoded, bool overrideExisting) { mFile = file; - boost::filesystem::ifstream stream; - stream.open(boost::filesystem::path(file)); + std::ifstream fstream; + fstream.open(std::filesystem::path(file)); + auto stream = std::ref(fstream); + + std::istringstream decodedStream; + if (base64Encoded) + { + std::string base64String(std::istreambuf_iterator(fstream), {}); + std::string decodedString; + auto result = Base64::Base64::Decode(base64String, decodedString); + if (!result.empty()) + fail("Could not decode Base64 file: " + result); + // Move won't do anything until C++20, but won't hurt to do it anyway. + decodedStream.str(std::move(decodedString)); + stream = std::ref(decodedStream); + } + Log(Debug::Info) << "Loading settings file: " << file; std::string currentCategory; mLine = 0; - while (!stream.eof() && !stream.fail()) + while (!stream.get().eof() && !stream.get().fail()) { ++mLine; std::string line; - std::getline( stream, line ); + std::getline( stream.get(), line ); size_t i = 0; if (!skipWhiteSpace(i, line)) @@ -56,7 +75,9 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat std::string value = line.substr(valueBegin); Misc::StringUtils::trim(value); - if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) + if (overrideExisting) + settings[std::make_pair(currentCategory, setting)] = value; + else if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); } } @@ -86,8 +107,8 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con // Open the existing settings.cfg file to copy comments. This might not be the same file // as the output file if we're copying the setting from the default settings.cfg for the // first time. A minor change in API to pass the source file might be in order here. - boost::filesystem::ifstream istream; - boost::filesystem::path ipath(file); + std::ifstream istream; + std::filesystem::path ipath(file); istream.open(ipath); // Create a new string stream to write the current settings to. It's likely that the @@ -255,7 +276,7 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con ostream << "# This is the OpenMW user 'settings.cfg' file. This file only contains" << std::endl; ostream << "# explicitly changed settings. If you would like to revert a setting" << std::endl; ostream << "# to its default, simply remove it from this file. For available" << std::endl; - ostream << "# settings, see the file 'settings-default.cfg' or the documentation at:" << std::endl; + ostream << "# settings, see the file 'files/settings-default.cfg' in our source repo or the documentation at:" << std::endl; ostream << "#" << std::endl; ostream << "# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html" << std::endl; } @@ -285,7 +306,7 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con // Now install the newly written file in the requested place. if (changed) { Log(Debug::Info) << "Updating settings file: " << ipath; - boost::filesystem::ofstream ofstream; + std::ofstream ofstream; ofstream.open(ipath); ofstream << ostream.rdbuf(); ofstream.close(); @@ -301,7 +322,7 @@ bool Settings::SettingsFileParser::skipWhiteSpace(size_t& i, std::string& str) return i < str.size(); } -void Settings::SettingsFileParser::fail(const std::string& message) +[[noreturn]] void Settings::SettingsFileParser::fail(const std::string& message) { std::stringstream error; error << "Error on line " << mLine << " in " << mFile << ":\n" << message; diff --git a/components/settings/parser.hpp b/components/settings/parser.hpp index 449e542235..c934fbea07 100644 --- a/components/settings/parser.hpp +++ b/components/settings/parser.hpp @@ -10,7 +10,8 @@ namespace Settings class SettingsFileParser { public: - void loadSettingsFile(const std::string& file, CategorySettingValueMap& settings); + void loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, + bool base64encoded = false, bool overrideExisting = false); void saveSettingsFile(const std::string& file, const CategorySettingValueMap& settings); @@ -20,7 +21,7 @@ namespace Settings /// @return false if we have reached the end of the string bool skipWhiteSpace(size_t& i, std::string& str); - void fail(const std::string& message); + [[noreturn]] void fail(const std::string& message); std::string mFile; int mLine = 0; diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index b29dadcdc8..ff4a40d707 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -1,8 +1,10 @@ #include "settings.hpp" #include "parser.hpp" +#include #include +#include #include namespace Settings @@ -19,16 +21,48 @@ void Manager::clear() mChangedSettings.clear(); } -void Manager::loadDefault(const std::string &file) +std::string Manager::load(const Files::ConfigurationManager& cfgMgr, bool loadEditorSettings) { SettingsFileParser parser; - parser.loadSettingsFile(file, mDefaultSettings); -} + const std::vector& paths = cfgMgr.getActiveConfigPaths(); + if (paths.empty()) + throw std::runtime_error("No config dirs! ConfigurationManager::readConfiguration must be called first."); -void Manager::loadUser(const std::string &file) -{ - SettingsFileParser parser; - parser.loadSettingsFile(file, mUserSettings); + // Create file name strings for either the engine or the editor. + std::string defaultSettingsFile; + std::string userSettingsFile; + + if (!loadEditorSettings) + { + defaultSettingsFile = "defaults.bin"; + userSettingsFile = "settings.cfg"; + } + else + { + defaultSettingsFile = "defaults-cs.bin"; + userSettingsFile = "openmw-cs.cfg"; + } + + // Create the settings manager and load default settings file. + const std::string defaultsBin = (paths.front() / defaultSettingsFile).string(); + if (!std::filesystem::exists(defaultsBin)) + throw std::runtime_error ("No default settings file found! Make sure the file \"" + defaultSettingsFile + "\" was properly installed."); + parser.loadSettingsFile(defaultsBin, mDefaultSettings, true, false); + + // Load "settings.cfg" or "openmw-cs.cfg" from every config dir except the last one as additional default settings. + for (int i = 0; i < static_cast(paths.size()) - 1; ++i) + { + const std::string additionalDefaults = (paths[i] / userSettingsFile).string(); + if (std::filesystem::exists(additionalDefaults)) + parser.loadSettingsFile(additionalDefaults, mDefaultSettings, false, true); + } + + // Load "settings.cfg" or "openmw-cs.cfg" from the last config dir as user settings. This path will be used to save modified settings. + std::string settingspath = (paths.back() / userSettingsFile).string(); + if (std::filesystem::exists(settingspath)) + parser.loadSettingsFile(settingspath, mUserSettings, false, false); + + return settingspath; } void Manager::saveUser(const std::string &file) @@ -37,9 +71,9 @@ void Manager::saveUser(const std::string &file) parser.saveSettingsFile(file, mUserSettings); } -std::string Manager::getString(const std::string &setting, const std::string &category) +std::string Manager::getString(std::string_view setting, std::string_view category) { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); + const auto key = std::make_pair(category, setting); CategorySettingValueMap::iterator it = mUserSettings.find(key); if (it != mUserSettings.end()) return it->second; @@ -48,11 +82,13 @@ std::string Manager::getString(const std::string &setting, const std::string &ca if (it != mDefaultSettings.end()) return it->second; - throw std::runtime_error(std::string("Trying to retrieve a non-existing setting: ") + setting - + ".\nMake sure the settings-default.cfg file was properly installed."); + std::string error("Trying to retrieve a non-existing setting: "); + error += setting; + error += ".\nMake sure the defaults.bin file was properly installed."; + throw std::runtime_error(error); } -float Manager::getFloat (const std::string& setting, const std::string& category) +float Manager::getFloat(std::string_view setting, std::string_view category) { const std::string& value = getString(setting, category); std::stringstream stream(value); @@ -61,7 +97,16 @@ float Manager::getFloat (const std::string& setting, const std::string& category return number; } -int Manager::getInt (const std::string& setting, const std::string& category) +double Manager::getDouble(std::string_view setting, std::string_view category) +{ + const std::string& value = getString(setting, category); + std::stringstream stream(value); + double number = 0.0; + stream >> number; + return number; +} + +int Manager::getInt(std::string_view setting, std::string_view category) { const std::string& value = getString(setting, category); std::stringstream stream(value); @@ -70,13 +115,22 @@ int Manager::getInt (const std::string& setting, const std::string& category) return number; } -bool Manager::getBool (const std::string& setting, const std::string& category) +std::int64_t Manager::getInt64(std::string_view setting, std::string_view category) +{ + const std::string& value = getString(setting, category); + std::stringstream stream(value); + std::int64_t number = 0; + stream >> number; + return number; +} + +bool Manager::getBool(std::string_view setting, std::string_view category) { const std::string& string = getString(setting, category); return Misc::StringUtils::ciEqual(string, "true"); } -osg::Vec2f Manager::getVector2 (const std::string& setting, const std::string& category) +osg::Vec2f Manager::getVector2(std::string_view setting, std::string_view category) { const std::string& value = getString(setting, category); std::stringstream stream(value); @@ -84,10 +138,10 @@ osg::Vec2f Manager::getVector2 (const std::string& setting, const std::string& c stream >> x >> y; if (stream.fail()) throw std::runtime_error(std::string("Can't parse 2d vector: " + value)); - return osg::Vec2f(x, y); + return {x, y}; } -osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& category) +osg::Vec3f Manager::getVector3(std::string_view setting, std::string_view category) { const std::string& value = getString(setting, category); std::stringstream stream(value); @@ -95,67 +149,84 @@ osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& c stream >> x >> y >> z; if (stream.fail()) throw std::runtime_error(std::string("Can't parse 3d vector: " + value)); - return osg::Vec3f(x, y, z); + return {x, y, z}; } -void Manager::setString(const std::string &setting, const std::string &category, const std::string &value) +void Manager::setString(std::string_view setting, std::string_view category, const std::string &value) { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); - - CategorySettingValueMap::iterator found = mUserSettings.find(key); + auto found = mUserSettings.find(std::make_pair(category, setting)); if (found != mUserSettings.end()) { if (found->second == value) return; } + CategorySettingValueMap::key_type key(category, setting); + mUserSettings[key] = value; - mChangedSettings.insert(key); + mChangedSettings.insert(std::move(key)); +} + +void Manager::setInt(std::string_view setting, std::string_view category, const int value) +{ + std::ostringstream stream; + stream << value; + setString(setting, category, stream.str()); +} + +void Manager::setInt64(std::string_view setting, std::string_view category, const std::int64_t value) +{ + std::ostringstream stream; + stream << value; + setString(setting, category, stream.str()); } -void Manager::setInt (const std::string& setting, const std::string& category, const int value) +void Manager::setFloat (std::string_view setting, std::string_view category, const float value) { std::ostringstream stream; stream << value; setString(setting, category, stream.str()); } -void Manager::setFloat (const std::string &setting, const std::string &category, const float value) +void Manager::setDouble (std::string_view setting, std::string_view category, const double value) { std::ostringstream stream; stream << value; setString(setting, category, stream.str()); } -void Manager::setBool(const std::string &setting, const std::string &category, const bool value) +void Manager::setBool(std::string_view setting, std::string_view category, const bool value) { setString(setting, category, value ? "true" : "false"); } -void Manager::setVector2 (const std::string &setting, const std::string &category, const osg::Vec2f value) +void Manager::setVector2 (std::string_view setting, std::string_view category, const osg::Vec2f value) { std::ostringstream stream; stream << value.x() << " " << value.y(); setString(setting, category, stream.str()); } -void Manager::setVector3 (const std::string &setting, const std::string &category, const osg::Vec3f value) +void Manager::setVector3 (std::string_view setting, std::string_view category, const osg::Vec3f value) { std::ostringstream stream; stream << value.x() << ' ' << value.y() << ' ' << value.z(); setString(setting, category, stream.str()); } -void Manager::resetPendingChange(const std::string &setting, const std::string &category) +CategorySettingVector Manager::getPendingChanges() { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); - mChangedSettings.erase(key); + return mChangedSettings; } -const CategorySettingVector Manager::getPendingChanges() +CategorySettingVector Manager::getPendingChanges(const CategorySettingVector& filter) { - return mChangedSettings; + CategorySettingVector intersection; + std::set_intersection(mChangedSettings.begin(), mChangedSettings.end(), + filter.begin(), filter.end(), + std::inserter(intersection, intersection.begin())); + return intersection; } void Manager::resetPendingChanges() @@ -163,4 +234,12 @@ void Manager::resetPendingChanges() mChangedSettings.clear(); } +void Manager::resetPendingChanges(const CategorySettingVector& filter) +{ + for (const auto& key : filter) + { + mChangedSettings.erase(key); + } +} + } diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index ecc5aa5fd3..32157bd3d2 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -6,11 +6,25 @@ #include #include #include +#include + #include #include +namespace Files +{ + struct ConfigurationManager; +} + namespace Settings { + enum class WindowMode + { + Fullscreen = 0, + WindowedFullscreen, + Windowed + }; + /// /// \brief Settings management (can change during runtime) /// @@ -23,40 +37,46 @@ namespace Settings static CategorySettingVector mChangedSettings; ///< tracks all the settings that were changed since the last apply() call - void clear(); + static void clear(); ///< clears all settings and default settings - void loadDefault (const std::string& file); - ///< load file as the default settings (can be overridden by user settings) + static std::string load(const Files::ConfigurationManager& cfgMgr, bool loadEditorSettings = false); + ///< load settings from all active config dirs. Returns the path of the last loaded file. - void loadUser (const std::string& file); - ///< load file as user settings - - void saveUser (const std::string& file); + static void saveUser (const std::string& file); ///< save user settings to file - static void resetPendingChange(const std::string &setting, const std::string &category); - static void resetPendingChanges(); + ///< resets the list of all pending changes + + static void resetPendingChanges(const CategorySettingVector& filter); + ///< resets only the pending changes listed in the filter + + static CategorySettingVector getPendingChanges(); + ///< returns the list of changed settings + + static CategorySettingVector getPendingChanges(const CategorySettingVector& filter); + ///< returns the list of changed settings intersecting with the filter + + static int getInt(std::string_view setting, std::string_view category); + static std::int64_t getInt64(std::string_view setting, std::string_view category); + static float getFloat(std::string_view setting, std::string_view category); + static double getDouble(std::string_view setting, std::string_view category); + static std::string getString(std::string_view setting, std::string_view category); + static bool getBool(std::string_view setting, std::string_view category); + static osg::Vec2f getVector2(std::string_view setting, std::string_view category); + static osg::Vec3f getVector3(std::string_view setting, std::string_view category); - static const CategorySettingVector getPendingChanges(); - ///< returns the list of changed settings and then clears it - - static int getInt (const std::string& setting, const std::string& category); - static float getFloat (const std::string& setting, const std::string& category); - static std::string getString (const std::string& setting, const std::string& category); - static bool getBool (const std::string& setting, const std::string& category); - static osg::Vec2f getVector2 (const std::string& setting, const std::string& category); - static osg::Vec3f getVector3 (const std::string& setting, const std::string& category); - - static void setInt (const std::string& setting, const std::string& category, const int value); - static void setFloat (const std::string& setting, const std::string& category, const float value); - static void setString (const std::string& setting, const std::string& category, const std::string& value); - static void setBool (const std::string& setting, const std::string& category, const bool value); - static void setVector2 (const std::string& setting, const std::string& category, const osg::Vec2f value); - static void setVector3 (const std::string& setting, const std::string& category, const osg::Vec3f value); + static void setInt(std::string_view setting, std::string_view category, int value); + static void setInt64(std::string_view setting, std::string_view category, std::int64_t value); + static void setFloat(std::string_view setting, std::string_view category, float value); + static void setDouble(std::string_view setting, std::string_view category, double value); + static void setString(std::string_view setting, std::string_view category, const std::string& value); + static void setBool(std::string_view setting, std::string_view category, bool value); + static void setVector2(std::string_view setting, std::string_view category, osg::Vec2f value); + static void setVector3(std::string_view setting, std::string_view category, osg::Vec3f value); }; } -#endif // _COMPONENTS_SETTINGS_H +#endif // COMPONENTS_SETTINGS_H diff --git a/components/settings/shadermanager.hpp b/components/settings/shadermanager.hpp new file mode 100644 index 0000000000..eaf10a3a34 --- /dev/null +++ b/components/settings/shadermanager.hpp @@ -0,0 +1,175 @@ +#ifndef OPENMW_COMPONENTS_SETTINGS_SHADERMANAGER_H +#define OPENMW_COMPONENTS_SETTINGS_SHADERMANAGER_H + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace Settings +{ + /* + * Manages the shader.yaml file which is auto-generated and lives next to settings.cfg. + * This YAML file is simply a mapping of technique name to a list of uniforms and their values. + * Currently only vec2f, vec3f, vec4f, int, and float uniforms are supported. + * + * config: + * TECHNIQUE: + * MY_FLOAT: 10.34 + * MY_VEC2: [0.23, 0.34] + * TECHNIQUE2: + * MY_VEC3: [0.22, 0.33, 0.20] + */ + class ShaderManager + { + public: + + enum class Mode + { + Normal, + Debug + }; + + ShaderManager() = default; + ShaderManager(ShaderManager const&) = delete; + void operator=(ShaderManager const&) = delete; + + static ShaderManager& get() + { + static ShaderManager instance; + return instance; + } + + Mode getMode() + { + return mMode; + } + + void setMode(Mode mode) + { + mMode = mode; + } + + const YAML::Node& getRoot() + { + return mData; + } + + template + bool setValue(const std::string& tname, const std::string& uname, const T& value) + { + if (mData.IsNull()) + { + Log(Debug::Warning) << "Failed setting " << tname << ", " << uname << " : shader settings failed to load"; + return false; + } + + mData["config"][tname][uname] = value; + return true; + } + + template + std::optional getValue(const std::string& tname, const std::string& uname) + { + if (mData.IsNull()) + { + Log(Debug::Warning) << "Failed getting " << tname << ", " << uname << " : shader settings failed to load"; + return std::nullopt; + } + + try + { + auto value = mData["config"][tname][uname]; + + if (!value) + return std::nullopt; + + return value.as(); + } + catch(const YAML::BadConversion&) + { + Log(Debug::Warning) << "Failed retrieving " << tname << ", " << uname << " : mismatched types in config file."; + } + + return std::nullopt; + } + + bool load(const std::string& path) + { + mData = YAML::Null; + mPath = std::filesystem::path(path); + + Log(Debug::Info) << "Loading shader settings file: " << mPath; + + if (!std::filesystem::exists(mPath)) + { + std::ofstream fout(mPath); + if (!fout) + { + Log(Debug::Error) << "Failed creating shader settings file: " << mPath; + return false; + } + } + + try + { + mData = YAML::LoadFile(mPath.string()); + mData.SetStyle(YAML::EmitterStyle::Block); + + if (!mData["config"]) + mData["config"] = YAML::Node(); + + return true; + } + catch(const YAML::Exception& e) + { + Log(Debug::Error) << "Shader settings failed to load, " << e.msg; + } + + return false; + } + + bool save() + { + if (mData.IsNull()) + { + Log(Debug::Error) << "Shader settings failed to load, settings will not be saved: " << mPath; + return false; + } + + Log(Debug::Info) << "Saving shader settings file: " << mPath; + + YAML::Emitter out; + out.SetMapFormat(YAML::Block); + out << mData; + + std::ofstream fout(mPath.string()); + fout << out.c_str(); + + if (!fout) + { + Log(Debug::Error) << "Failed saving shader settings file: " << mPath; + return false; + } + + return true; + } + + private: + std::filesystem::path mPath; + YAML::Node mData; + Mode mMode = Mode::Normal; + }; +} + +#endif diff --git a/components/shader/removedalphafunc.cpp b/components/shader/removedalphafunc.cpp new file mode 100644 index 0000000000..6b701b8900 --- /dev/null +++ b/components/shader/removedalphafunc.cpp @@ -0,0 +1,20 @@ +#include "removedalphafunc.hpp" + +#include + +#include + +namespace Shader +{ + std::array, GL_ALWAYS - GL_NEVER + 1> RemovedAlphaFunc::sInstances{ + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr + }; + + osg::ref_ptr RemovedAlphaFunc::getInstance(GLenum func) + { + assert(func >= GL_NEVER && func <= GL_ALWAYS); + if (!sInstances[func - GL_NEVER]) + sInstances[func - GL_NEVER] = new RemovedAlphaFunc(static_cast(func), 1.0); + return sInstances[func - GL_NEVER]; + } +} diff --git a/components/shader/removedalphafunc.hpp b/components/shader/removedalphafunc.hpp new file mode 100644 index 0000000000..c744391e56 --- /dev/null +++ b/components/shader/removedalphafunc.hpp @@ -0,0 +1,40 @@ +#ifndef OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H +#define OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H + +#include + +#include + +namespace Shader +{ + // State attribute used when shader visitor replaces the deprecated alpha function with a shader + // Prevents redundant glAlphaFunc calls and lets the shadowsbin know the stateset had alpha testing + class RemovedAlphaFunc : public osg::AlphaFunc + { + public: + // Get a singleton-like instance with the right func (but a default threshold) + static osg::ref_ptr getInstance(GLenum func); + + RemovedAlphaFunc() + : osg::AlphaFunc() + {} + + RemovedAlphaFunc(ComparisonFunction func, float ref) + : osg::AlphaFunc(func, ref) + {} + + RemovedAlphaFunc(const RemovedAlphaFunc& raf, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) + : osg::AlphaFunc(raf, copyop) + {} + + META_StateAttribute(Shader, RemovedAlphaFunc, ALPHAFUNC); + + void apply(osg::State& state) const override {} + + protected: + virtual ~RemovedAlphaFunc() = default; + + static std::array, GL_ALWAYS - GL_NEVER + 1> sInstances; + }; +} +#endif //OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 788a8720bc..8136bab447 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -3,18 +3,22 @@ #include #include #include +#include +#include #include -#include -#include - #include #include +#include namespace Shader { + ShaderManager::ShaderManager() + { + } + void ShaderManager::setShaderPath(const std::string &path) { mPath = path; @@ -64,7 +68,7 @@ namespace Shader // Recursively replaces include statements with the actual source of the included files. // Adjusts #line statements accordingly and detects cyclic includes. // includingFiles is the set of files that include this file directly or indirectly, and is intentionally not a reference to allow automatic cleanup. - static bool parseIncludes(boost::filesystem::path shaderPath, std::string& source, const std::string& fileName, int& fileNumber, std::set includingFiles) + static bool parseIncludes(const std::filesystem::path& shaderPath, std::string& source, const std::string& fileName, int& fileNumber, std::set includingFiles) { // An include is cyclic if it is being included by itself if (includingFiles.insert(shaderPath/fileName).second == false) @@ -91,7 +95,7 @@ namespace Shader return false; } std::string includeFilename = source.substr(start + 1, end - (start + 1)); - boost::filesystem::path includePath = shaderPath / includeFilename; + std::filesystem::path includePath = shaderPath / includeFilename; // Determine the line number that will be used for the #line directive following the included source size_t lineDirectivePosition = source.rfind("#line", foundPos); @@ -111,7 +115,7 @@ namespace Shader lineNumber += std::count(source.begin() + lineDirectivePosition, source.begin() + foundPos, '\n'); // Include the file recursively - boost::filesystem::ifstream includeFstream; + std::ifstream includeFstream; includeFstream.open(includePath); if (includeFstream.fail()) { @@ -138,11 +142,123 @@ namespace Shader return true; } - bool parseFors(std::string& source, const std::string& templateName) + bool parseForeachDirective(std::string& source, const std::string& templateName, size_t foundPos) + { + size_t iterNameStart = foundPos + strlen("$foreach") + 1; + size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart); + if (iterNameEnd == std::string::npos) + { + Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; + return false; + } + std::string iteratorName = "$" + source.substr(iterNameStart, iterNameEnd - iterNameStart); + + size_t listStart = iterNameEnd + 1; + size_t listEnd = source.find_first_of("\n\r", listStart); + if (listEnd == std::string::npos) + { + Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; + return false; + } + std::string list = source.substr(listStart, listEnd - listStart); + std::vector listElements; + if (list != "") + Misc::StringUtils::split(list, listElements, ","); + + size_t contentStart = source.find_first_not_of("\n\r", listEnd); + size_t contentEnd = source.find("$endforeach", contentStart); + if (contentEnd == std::string::npos) + { + Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; + return false; + } + std::string content = source.substr(contentStart, contentEnd - contentStart); + + size_t overallEnd = contentEnd + std::string("$endforeach").length(); + + size_t lineDirectivePosition = source.rfind("#line", overallEnd); + int lineNumber; + if (lineDirectivePosition != std::string::npos) + { + size_t lineNumberStart = lineDirectivePosition + std::string("#line ").length(); + size_t lineNumberEnd = source.find_first_not_of("0123456789", lineNumberStart); + std::string lineNumberString = source.substr(lineNumberStart, lineNumberEnd - lineNumberStart); + lineNumber = std::stoi(lineNumberString); + } + else + { + lineDirectivePosition = 0; + lineNumber = 2; + } + lineNumber += std::count(source.begin() + lineDirectivePosition, source.begin() + overallEnd, '\n'); + + std::string replacement; + for (std::vector::const_iterator element = listElements.cbegin(); element != listElements.cend(); element++) + { + std::string contentInstance = content; + size_t foundIterator; + while ((foundIterator = contentInstance.find(iteratorName)) != std::string::npos) + contentInstance.replace(foundIterator, iteratorName.length(), *element); + replacement += contentInstance; + } + replacement += "\n#line " + std::to_string(lineNumber); + source.replace(foundPos, overallEnd - foundPos, replacement); + return true; + } + + bool parseLinkDirective(std::string& source, std::string& linkTarget, const std::string& templateName, size_t foundPos) + { + size_t endPos = foundPos + 5; + size_t lineEnd = source.find_first_of('\n', endPos); + // If lineEnd = npos, this is the last line, so no need to check + std::string linkStatement = source.substr(endPos, lineEnd - endPos); + std::regex linkRegex( + R"r(\s*"([^"]+)"\s*)r" // Find any quoted string as the link name -> match[1] + R"r((if\s+)r" // Begin optional condition -> match[2] + R"r((!)?\s*)r" // Optional ! -> match[3] + R"r(([_a-zA-Z0-9]+)?)r" // The condition -> match[4] + R"r()?\s*)r" // End optional condition -> match[2] + ); + std::smatch linkMatch; + bool hasCondition = false; + std::string linkConditionExpression; + if (std::regex_match(linkStatement, linkMatch, linkRegex)) + { + linkTarget = linkMatch[1].str(); + hasCondition = !linkMatch[2].str().empty(); + linkConditionExpression = linkMatch[4].str(); + } + else + { + Log(Debug::Error) << "Shader " << templateName << " error: Expected a shader filename to link"; + return false; + } + if (linkTarget.empty()) + { + Log(Debug::Error) << "Shader " << templateName << " error: Empty link name"; + return false; + } + + if (hasCondition) + { + bool condition = !(linkConditionExpression.empty() || linkConditionExpression == "0"); + if (linkMatch[3].str() == "!") + condition = !condition; + + if (!condition) + linkTarget.clear(); + } + + source.replace(foundPos, lineEnd - foundPos, ""); + return true; + } + + bool parseDirectives(std::string& source, std::vector& linkedShaderTemplateNames, const ShaderManager::DefineMap& defines, const ShaderManager::DefineMap& globalDefines, const std::string& templateName) { const char escapeCharacter = '$'; size_t foundPos = 0; - while ((foundPos = source.find(escapeCharacter)) != std::string::npos) + + while ((foundPos = source.find(escapeCharacter, foundPos)) != std::string::npos) { size_t endPos = source.find_first_of(" \n\r()[].;,", foundPos); if (endPos == std::string::npos) @@ -150,72 +266,25 @@ namespace Shader Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; return false; } - std::string command = source.substr(foundPos + 1, endPos - (foundPos + 1)); - if (command != "foreach") - { - Log(Debug::Error) << "Shader " << templateName << " error: Unknown shader directive: $" << command; - return false; - } - - size_t iterNameStart = endPos + 1; - size_t iterNameEnd = source.find_first_of(" \n\r()[].;,", iterNameStart); - if (iterNameEnd == std::string::npos) + std::string directive = source.substr(foundPos + 1, endPos - (foundPos + 1)); + if (directive == "foreach") { - Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; - return false; - } - std::string iteratorName = "$" + source.substr(iterNameStart, iterNameEnd - iterNameStart); - - size_t listStart = iterNameEnd + 1; - size_t listEnd = source.find_first_of("\n\r", listStart); - if (listEnd == std::string::npos) - { - Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; - return false; - } - std::string list = source.substr(listStart, listEnd - listStart); - std::vector listElements; - if (list != "") - Misc::StringUtils::split (list, listElements, ","); - - size_t contentStart = source.find_first_not_of("\n\r", listEnd); - size_t contentEnd = source.find("$endforeach", contentStart); - if (contentEnd == std::string::npos) - { - Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; - return false; + if (!parseForeachDirective(source, templateName, foundPos)) + return false; } - std::string content = source.substr(contentStart, contentEnd - contentStart); - - size_t overallEnd = contentEnd + std::string("$endforeach").length(); - - size_t lineDirectivePosition = source.rfind("#line", overallEnd); - int lineNumber; - if (lineDirectivePosition != std::string::npos) + else if (directive == "link") { - size_t lineNumberStart = lineDirectivePosition + std::string("#line ").length(); - size_t lineNumberEnd = source.find_first_not_of("0123456789", lineNumberStart); - std::string lineNumberString = source.substr(lineNumberStart, lineNumberEnd - lineNumberStart); - lineNumber = std::stoi(lineNumberString); + std::string linkTarget; + if (!parseLinkDirective(source, linkTarget, templateName, foundPos)) + return false; + if (!linkTarget.empty() && linkTarget != templateName) + linkedShaderTemplateNames.push_back(linkTarget); } else { - lineDirectivePosition = 0; - lineNumber = 2; - } - lineNumber += std::count(source.begin() + lineDirectivePosition, source.begin() + overallEnd, '\n'); - - std::string replacement = ""; - for (std::vector::const_iterator element = listElements.cbegin(); element != listElements.cend(); element++) - { - std::string contentInstance = content; - size_t foundIterator; - while ((foundIterator = contentInstance.find(iteratorName)) != std::string::npos) - contentInstance.replace(foundIterator, iteratorName.length(), *element); - replacement += contentInstance; + Log(Debug::Error) << "Shader " << templateName << " error: Unknown shader directive: $" << directive; + return false; } - replacement += "\n#line " + std::to_string(lineNumber); - source.replace(foundPos, overallEnd - foundPos, replacement); } return true; @@ -261,6 +330,10 @@ namespace Shader else forIterators.pop_back(); } + else if (define == "link") + { + source.replace(foundPos, 1, "$"); + } else if (std::find(forIterators.begin(), forIterators.end(), define) != forIterators.end()) { source.replace(foundPos, 1, "$"); @@ -284,14 +357,14 @@ namespace Shader osg::ref_ptr ShaderManager::getShader(const std::string &templateName, const ShaderManager::DefineMap &defines, osg::Shader::Type shaderType) { - std::lock_guard lock(mMutex); + std::unique_lock lock(mMutex); // read the template if we haven't already TemplateMap::iterator templateIt = mShaderTemplates.find(templateName); if (templateIt == mShaderTemplates.end()) { - boost::filesystem::path path = (boost::filesystem::path(mPath) / templateName); - boost::filesystem::ifstream stream; + std::filesystem::path path = (std::filesystem::path(mPath) / templateName); + std::ifstream stream; stream.open(path); if (stream.fail()) { @@ -305,7 +378,7 @@ namespace Shader int fileNumber = 1; std::string source = buffer.str(); if (!addLineDirectivesAfterConditionalBlocks(source) - || !parseIncludes(boost::filesystem::path(mPath), source, templateName, fileNumber, {})) + || !parseIncludes(std::filesystem::path(mPath), source, templateName, fileNumber, {})) return nullptr; templateIt = mShaderTemplates.insert(std::make_pair(templateName, source)).first; @@ -315,7 +388,8 @@ namespace Shader if (shaderIt == mShaders.end()) { std::string shaderSource = templateIt->second; - if (!parseDefines(shaderSource, defines, mGlobalDefines, templateName) || !parseFors(shaderSource, templateName)) + std::vector linkedShaderNames; + if (!createSourceFromTemplate(shaderSource, linkedShaderNames, templateName, defines)) { // Add to the cache anyway to avoid logging the same error over and over. mShaders.insert(std::make_pair(std::make_pair(templateName, defines), nullptr)); @@ -324,29 +398,46 @@ namespace Shader osg::ref_ptr shader (new osg::Shader(shaderType)); shader->setShaderSource(shaderSource); - // Assign a unique name to allow the SharedStateManager to compare shaders efficiently + // Assign a unique prefix to allow the SharedStateManager to compare shaders efficiently. + // Append shader source filename for debugging. static unsigned int counter = 0; - shader->setName(std::to_string(counter++)); + shader->setName(Misc::StringUtils::format("%u %s", counter++, templateName)); + + lock.unlock(); + getLinkedShaders(shader, linkedShaderNames, defines); + lock.lock(); shaderIt = mShaders.insert(std::make_pair(std::make_pair(templateName, defines), shader)).first; } return shaderIt->second; } - osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader) + osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate) { std::lock_guard lock(mMutex); ProgramMap::iterator found = mPrograms.find(std::make_pair(vertexShader, fragmentShader)); if (found == mPrograms.end()) { - osg::ref_ptr program (new osg::Program); + if (!programTemplate) programTemplate = mProgramTemplate; + osg::ref_ptr program = programTemplate ? cloneProgram(programTemplate) : osg::ref_ptr(new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); + addLinkedShaders(vertexShader, program); + addLinkedShaders(fragmentShader, program); + found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; } + osg::ref_ptr ShaderManager::cloneProgram(const osg::Program* src) + { + osg::ref_ptr program = static_cast(src->clone(osg::CopyOp::SHALLOW_COPY)); + for (auto [name, idx] : src->getUniformBlockBindingList()) + program->addBindUniformBlock(name, idx); + return program; + } + ShaderManager::DefineMap ShaderManager::getGlobalDefines() { return DefineMap(mGlobalDefines); @@ -355,33 +446,93 @@ namespace Shader void ShaderManager::setGlobalDefines(DefineMap & globalDefines) { mGlobalDefines = globalDefines; - for (auto shaderMapElement: mShaders) + for (const auto& [key, shader]: mShaders) { - std::string templateId = shaderMapElement.first.first; - ShaderManager::DefineMap defines = shaderMapElement.first.second; - osg::ref_ptr shader = shaderMapElement.second; + std::string templateId = key.first; + ShaderManager::DefineMap defines = key.second; if (shader == nullptr) // I'm not sure how to handle a shader that was already broken as there's no way to get a potential replacement to the nodes that need it. continue; std::string shaderSource = mShaderTemplates[templateId]; - if (!parseDefines(shaderSource, defines, mGlobalDefines, templateId) || !parseFors(shaderSource, templateId)) + std::vector linkedShaderNames; + if (!createSourceFromTemplate(shaderSource, linkedShaderNames, templateId, defines)) // We just broke the shader and there's no way to force existing objects back to fixed-function mode as we would when creating the shader. // If we put a nullptr in the shader map, we just lose the ability to put a working one in later. continue; shader->setShaderSource(shaderSource); + + getLinkedShaders(shader, linkedShaderNames, defines); } } void ShaderManager::releaseGLObjects(osg::State *state) { std::lock_guard lock(mMutex); - for (auto shader : mShaders) + for (const auto& [_, shader] : mShaders) + { + if (shader != nullptr) + shader->releaseGLObjects(state); + } + for (const auto& [_, program] : mPrograms) + program->releaseGLObjects(state); + } + + bool ShaderManager::createSourceFromTemplate(std::string& source, std::vector& linkedShaderTemplateNames, const std::string& templateName, const ShaderManager::DefineMap& defines) + { + if (!parseDefines(source, defines, mGlobalDefines, templateName)) + return false; + if (!parseDirectives(source, linkedShaderTemplateNames, defines, mGlobalDefines, templateName)) + return false; + return true; + } + + void ShaderManager::getLinkedShaders(osg::ref_ptr shader, const std::vector& linkedShaderNames, const DefineMap& defines) + { + mLinkedShaders.erase(shader); + if (linkedShaderNames.empty()) + return; + + for (auto& linkedShaderName : linkedShaderNames) { - if (shader.second != nullptr) - shader.second->releaseGLObjects(state); + auto linkedShader = getShader(linkedShaderName, defines, shader->getType()); + if (linkedShader) + mLinkedShaders[shader].emplace_back(linkedShader); } - for (auto program : mPrograms) - program.second->releaseGLObjects(state); + } + + void ShaderManager::addLinkedShaders(osg::ref_ptr shader, osg::ref_ptr program) + { + auto linkedIt = mLinkedShaders.find(shader); + if (linkedIt != mLinkedShaders.end()) + for (const auto& linkedShader : linkedIt->second) + program->addShader(linkedShader); + } + + int ShaderManager::reserveGlobalTextureUnits(Slot slot) + { + int unit = mReservedTextureUnitsBySlot[static_cast(slot)]; + if (unit >= 0) + return unit; + + { + // Texture units from `8 - numberOfShadowMaps` to `8` are used for shadows, so we skip them here. + // TODO: Maybe instead of fixed texture units use `reserveGlobalTextureUnits` for shadows as well. + static const int numberOfShadowMaps = Settings::Manager::getBool("enable shadows", "Shadows") ? + std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8) : + 0; + if (getAvailableTextureUnits() >= 8 && getAvailableTextureUnits() - 1 < 8) + mReservedTextureUnits = mMaxTextureUnits - (8 - numberOfShadowMaps); + } + + if (getAvailableTextureUnits() < 2) + throw std::runtime_error("Can't reserve texture unit; no available units"); + mReservedTextureUnits++; + + unit = mMaxTextureUnits - mReservedTextureUnits; + + mReservedTextureUnitsBySlot[static_cast(slot)] = unit; + + return unit; } } diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 13db30b019..4d3cc9937a 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -4,12 +4,13 @@ #include #include #include +#include +#include #include #include - -#include +#include namespace Shader { @@ -19,6 +20,9 @@ namespace Shader class ShaderManager { public: + + ShaderManager(); + void setShaderPath(const std::string& path); typedef std::map DefineMap; @@ -31,7 +35,13 @@ namespace Shader /// @note Thread safe. osg::ref_ptr getShader(const std::string& templateName, const DefineMap& defines, osg::Shader::Type shaderType); - osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader); + osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate=nullptr); + + const osg::Program* getProgramTemplate() const { return mProgramTemplate; } + void setProgramTemplate(const osg::Program* program) { mProgramTemplate = program; } + + /// Clone an osg::Program including bindUniformBlocks that osg::Program::clone does not copy for some reason. + static osg::ref_ptr cloneProgram(const osg::Program*); /// Get (a copy of) the DefineMap used to construct all shaders DefineMap getGlobalDefines(); @@ -43,7 +53,24 @@ namespace Shader void releaseGLObjects(osg::State* state); + bool createSourceFromTemplate(std::string& source, std::vector& linkedShaderTemplateNames, const std::string& templateName, const ShaderManager::DefineMap& defines); + + void setMaxTextureUnits(int maxTextureUnits) { mMaxTextureUnits = maxTextureUnits; } + int getMaxTextureUnits() const { return mMaxTextureUnits; } + int getAvailableTextureUnits() const { return mMaxTextureUnits - mReservedTextureUnits; } + + enum class Slot + { + OpaqueDepthTexture, + SkyTexture, + }; + + int reserveGlobalTextureUnits(Slot slot); + private: + void getLinkedShaders(osg::ref_ptr shader, const std::vector& linkedShaderNames, const DefineMap& defines); + void addLinkedShaders(osg::ref_ptr shader, osg::ref_ptr program); + std::string mPath; DefineMap mGlobalDefines; @@ -59,13 +86,28 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; + typedef std::vector > ShaderList; + typedef std::map, ShaderList> LinkedShadersMap; + LinkedShadersMap mLinkedShaders; + std::mutex mMutex; + + osg::ref_ptr mProgramTemplate; + + int mMaxTextureUnits = 0; + int mReservedTextureUnits = 0; + + std::array mReservedTextureUnitsBySlot = {-1, -1}; }; - bool parseFors(std::string& source, const std::string& templateName); + bool parseForeachDirective(std::string& source, const std::string& templateName, size_t foundPos); + bool parseLinkDirective(std::string& source, std::string& linkTarget, const std::string& templateName, size_t foundPos); bool parseDefines(std::string& source, const ShaderManager::DefineMap& defines, const ShaderManager::DefineMap& globalDefines, const std::string& templateName); + + bool parseDirectives(std::string& source, std::vector& linkedShaderTemplateNames, const ShaderManager::DefineMap& defines, + const ShaderManager::DefineMap& globalDefines, const std::string& templateName); } #endif diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 10e9e606e5..5f7225c6fc 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -1,51 +1,180 @@ #include "shadervisitor.hpp" +#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 "removedalphafunc.hpp" #include "shadermanager.hpp" namespace Shader { + /** + * Miniature version of osg::StateSet used to track state added by the shader visitor which should be ignored when + * it's applied a second time, and removed when shaders are removed. + * Actual StateAttributes aren't kept as they're recoverable from the StateSet this is attached to - we just want + * the TypeMemberPair as that uniquely identifies which of those StateAttributes it was we're tracking. + * Not all StateSet features have been added yet - we implement an equivalently-named method to each of the StateSet + * methods called in createProgram, and implement new ones as they're needed. + * When expanding tracking to cover new things, ensure they're accounted for in ensureFFP. + */ + class AddedState : public osg::Object + { + public: + AddedState() = default; + AddedState(const AddedState& rhs, const osg::CopyOp& copyOp) + : osg::Object(rhs, copyOp) + , mUniforms(rhs.mUniforms) + , mModes(rhs.mModes) + , mAttributes(rhs.mAttributes) + , mTextureModes(rhs.mTextureModes) + { + } + + void addUniform(const std::string& name) { mUniforms.emplace(name); } + void setMode(osg::StateAttribute::GLMode mode) { mModes.emplace(mode); } + void setAttribute(osg::StateAttribute::TypeMemberPair typeMemberPair) { mAttributes.emplace(typeMemberPair); } + + void setAttribute(const osg::StateAttribute* attribute) + { + mAttributes.emplace(attribute->getTypeMemberPair()); + } + template + void setAttribute(osg::ref_ptr attribute) { setAttribute(attribute.get()); } + + void setAttributeAndModes(const osg::StateAttribute* attribute) + { + setAttribute(attribute); + InterrogateModesHelper helper(this); + attribute->getModeUsage(helper); + } + template + void setAttributeAndModes(osg::ref_ptr attribute) { setAttributeAndModes(attribute.get()); } + + void setTextureMode(unsigned int unit, osg::StateAttribute::GLMode mode) { mTextureModes[unit].emplace(mode); } + void setTextureAttribute(int unit, osg::StateAttribute::TypeMemberPair typeMemberPair) { mTextureAttributes[unit].emplace(typeMemberPair); } + + void setTextureAttribute(unsigned int unit, const osg::StateAttribute* attribute) + { + mTextureAttributes[unit].emplace(attribute->getTypeMemberPair()); + } + template + void setTextureAttribute(unsigned int unit, osg::ref_ptr attribute) { setTextureAttribute(unit, attribute.get()); } + + void setTextureAttributeAndModes(unsigned int unit, const osg::StateAttribute* attribute) + { + setTextureAttribute(unit, attribute); + InterrogateModesHelper helper(this, unit); + attribute->getModeUsage(helper); + } + template + void setTextureAttributeAndModes(unsigned int unit, osg::ref_ptr attribute) { setTextureAttributeAndModes(unit, attribute.get()); } + + bool hasUniform(const std::string& name) { return mUniforms.count(name); } + bool hasMode(osg::StateAttribute::GLMode mode) { return mModes.count(mode); } + bool hasAttribute(const osg::StateAttribute::TypeMemberPair &typeMemberPair) { return mAttributes.count(typeMemberPair); } + bool hasAttribute(osg::StateAttribute::Type type, unsigned int member) { return hasAttribute(osg::StateAttribute::TypeMemberPair(type, member)); } + bool hasTextureMode(int unit, osg::StateAttribute::GLMode mode) + { + auto it = mTextureModes.find(unit); + if (it == mTextureModes.cend()) + return false; + + return it->second.count(mode); + } + + const std::set& getAttributes() { return mAttributes; } + const std::unordered_map>& getTextureAttributes() { return mTextureAttributes; } + + bool empty() + { + return mUniforms.empty() && mModes.empty() && mAttributes.empty() && mTextureModes.empty() && mTextureAttributes.empty(); + } + + META_Object(Shader, AddedState) + + private: + class InterrogateModesHelper : public osg::StateAttribute::ModeUsage + { + public: + InterrogateModesHelper(AddedState* tracker, unsigned int textureUnit = 0) + : mTracker(tracker) + , mTextureUnit(textureUnit) + {} + void usesMode(osg::StateAttribute::GLMode mode) override { mTracker->setMode(mode); } + void usesTextureMode(osg::StateAttribute::GLMode mode) override { mTracker->setTextureMode(mTextureUnit, mode); } + + private: + AddedState* mTracker; + unsigned int mTextureUnit; + }; + + using ModeSet = std::unordered_set; + using AttributeSet = std::set; + + std::unordered_set mUniforms; + ModeSet mModes; + AttributeSet mAttributes; + std::unordered_map mTextureModes; + std::unordered_map mTextureAttributes; + }; ShaderVisitor::ShaderRequirements::ShaderRequirements() : mShaderRequired(false) , mColorMode(0) , mMaterialOverridden(false) + , mAlphaTestOverridden(false) + , mAlphaBlendOverridden(false) + , mAlphaFunc(GL_ALWAYS) + , mAlphaRef(1.0) + , mAlphaBlend(false) + , mBlendFuncOverridden(false) + , mAdditiveBlending(false) , mNormalHeight(false) , mTexStageRequiringTangents(-1) + , mSoftParticles(false) , mNode(nullptr) { } - ShaderVisitor::ShaderRequirements::~ShaderRequirements() - { - - } - - ShaderVisitor::ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string &defaultVsTemplate, const std::string &defaultFsTemplate) + ShaderVisitor::ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string &defaultShaderPrefix) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mForceShaders(false) , mAllowedToModifyStateSets(true) , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) + , mConvertAlphaTestToAlphaToCoverage(false) + , mSupportsNormalsRT(false) , mShaderManager(shaderManager) , mImageManager(imageManager) - , mDefaultVsTemplate(defaultVsTemplate) - , mDefaultFsTemplate(defaultFsTemplate) + , mDefaultShaderPrefix(defaultShaderPrefix) { - mRequirements.emplace_back(); } void ShaderVisitor::setForceShaders(bool force) @@ -76,13 +205,56 @@ namespace Shader return newStateSet.get(); } - const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap", "bumpMap" }; - bool isTextureNameRecognized(const std::string& name) + osg::UserDataContainer* getWritableUserDataContainer(osg::Object& object) { - for (unsigned int i=0; i newUserData = static_cast(object.getUserDataContainer()->clone(osg::CopyOp::SHALLOW_COPY)); + object.setUserDataContainer(newUserData); + return newUserData.get(); + } + + osg::StateSet* getRemovedState(osg::StateSet& stateSet) + { + if (!stateSet.getUserDataContainer()) + return nullptr; + + return static_cast(stateSet.getUserDataContainer()->getUserObject("removedState")); + } + + void updateRemovedState(osg::UserDataContainer& userData, osg::StateSet* removedState) + { + unsigned int index = userData.getUserObjectIndex("removedState"); + if (index < userData.getNumUserObjects()) + userData.setUserObject(index, removedState); + else + userData.addUserObject(removedState); + removedState->setName("removedState"); + } + + AddedState* getAddedState(osg::StateSet& stateSet) + { + if (!stateSet.getUserDataContainer()) + return nullptr; + + return static_cast(stateSet.getUserDataContainer()->getUserObject("addedState")); + } + + void updateAddedState(osg::UserDataContainer& userData, AddedState* addedState) + { + unsigned int index = userData.getUserObjectIndex("addedState"); + if (index < userData.getNumUserObjects()) + userData.setUserObject(index, addedState); + else + userData.addUserObject(addedState); + addedState->setName("addedState"); + } + + const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap", "bumpMap", "glossMap" }; + bool isTextureNameRecognized(std::string_view name) + { + return std::find(std::begin(defaultTextures), std::end(defaultTextures), name) != std::end(defaultTextures); } void ShaderVisitor::applyStateSet(osg::ref_ptr stateset, osg::Node& node) @@ -91,6 +263,17 @@ namespace Shader if (mAllowedToModifyStateSets) writableStateSet = node.getStateSet(); const osg::StateSet::TextureAttributeList& texAttributes = stateset->getTextureAttributeList(); + bool shaderRequired = false; + if (node.getUserValue("shaderRequired", shaderRequired) && shaderRequired) + mRequirements.back().mShaderRequired = true; + + bool softEffect = false; + if (node.getUserValue(Misc::OsgUserValues::sXSoftEffect, softEffect) && softEffect) + mRequirements.back().mSoftParticles = true; + + // Make sure to disregard any state that came from a previous call to createProgram + osg::ref_ptr addedState = getAddedState(*stateset); + if (!texAttributes.empty()) { const osg::Texture* diffuseMap = nullptr; @@ -102,6 +285,11 @@ namespace Shader const osg::StateAttribute *attr = stateset->getTextureAttribute(unit, osg::StateAttribute::TEXTURE); if (attr) { + // If textures ever get removed in createProgram, expand this to check we're operating on main texture attribute list + // rather than the removed list + if (addedState && addedState->hasTextureMode(unit, GL_TEXTURE_2D)) + continue; + const osg::Texture* texture = attr->asTexture(); if (texture) { @@ -145,6 +333,14 @@ namespace Shader { mRequirements.back().mShaderRequired = true; } + else if (texName == "glossMap") + { + mRequirements.back().mShaderRequired = true; + if (!writableStateSet) + writableStateSet = getWritableStateSet(node); + // As well as gloss maps + writableStateSet->setTextureMode(unit, GL_TEXTURE_2D, osg::StateAttribute::ON); + } } else Log(Debug::Error) << "ShaderVisitor encountered unknown texture " << texture; @@ -222,67 +418,99 @@ namespace Shader mRequirements.back().mShaderRequired = true; } } - - if (diffuseMap) - { - if (!writableStateSet) - writableStateSet = getWritableStateSet(node); - // We probably shouldn't construct a new version of this each time as Uniforms use pointer comparison for early-out. - // Also it should probably belong to the shader manager or be applied by the shadows bin - writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); - } } const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); - for (osg::StateSet::AttributeList::const_iterator it = attributes.begin(); it != attributes.end(); ++it) + osg::StateSet::AttributeList removedAttributes; + if (osg::ref_ptr removedState = getRemovedState(*stateset)) + removedAttributes = removedState->getAttributeList(); + + for (const auto* attributeMap : std::initializer_list{ &attributes, &removedAttributes }) { - if (it->first.first == osg::StateAttribute::MATERIAL) + for (osg::StateSet::AttributeList::const_iterator it = attributeMap->begin(); it != attributeMap->end(); ++it) { - // This should probably be moved out of ShaderRequirements and be applied directly now it's a uniform instead of a define - if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED) + if (addedState && attributeMap != &removedAttributes && addedState->hasAttribute(it->first)) + continue; + if (it->first.first == osg::StateAttribute::MATERIAL) { - if (it->second.second & osg::StateAttribute::OVERRIDE) - mRequirements.back().mMaterialOverridden = true; + // This should probably be moved out of ShaderRequirements and be applied directly now it's a uniform instead of a define + if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED) + { + if (it->second.second & osg::StateAttribute::OVERRIDE) + mRequirements.back().mMaterialOverridden = true; - const osg::Material* mat = static_cast(it->second.first.get()); + const osg::Material* mat = static_cast(it->second.first.get()); - if (!writableStateSet) - writableStateSet = getWritableStateSet(node); + int colorMode; + switch (mat->getColorMode()) + { + case osg::Material::OFF: + colorMode = 0; + break; + case osg::Material::EMISSION: + colorMode = 1; + break; + default: + case osg::Material::AMBIENT_AND_DIFFUSE: + colorMode = 2; + break; + case osg::Material::AMBIENT: + colorMode = 3; + break; + case osg::Material::DIFFUSE: + colorMode = 4; + break; + case osg::Material::SPECULAR: + colorMode = 5; + break; + } - int colorMode; - switch (mat->getColorMode()) + mRequirements.back().mColorMode = colorMode; + } + } + else if (it->first.first == osg::StateAttribute::ALPHAFUNC) + { + if (!mRequirements.back().mAlphaTestOverridden || it->second.second & osg::StateAttribute::PROTECTED) { - case osg::Material::OFF: - colorMode = 0; - break; - case osg::Material::EMISSION: - colorMode = 1; - break; - default: - case osg::Material::AMBIENT_AND_DIFFUSE: - colorMode = 2; - break; - case osg::Material::AMBIENT: - colorMode = 3; - break; - case osg::Material::DIFFUSE: - colorMode = 4; - break; - case osg::Material::SPECULAR: - colorMode = 5; - break; + if (it->second.second & osg::StateAttribute::OVERRIDE) + mRequirements.back().mAlphaTestOverridden = true; + + const osg::AlphaFunc* alpha = static_cast(it->second.first.get()); + mRequirements.back().mAlphaFunc = alpha->getFunction(); + mRequirements.back().mAlphaRef = alpha->getReferenceValue(); } + } + else if (it->first.first == osg::StateAttribute::BLENDFUNC) + { + if (!mRequirements.back().mBlendFuncOverridden || it->second.second & osg::StateAttribute::PROTECTED) + { + if (it->second.second & osg::StateAttribute::OVERRIDE) + mRequirements.back().mBlendFuncOverridden = true; - mRequirements.back().mColorMode = colorMode; + const osg::BlendFunc* blend = static_cast(it->second.first.get()); + mRequirements.back().mAdditiveBlending = + blend->getSource() == osg::BlendFunc::SRC_ALPHA && blend->getDestination() == osg::BlendFunc::ONE; + } } } - // Eventually, move alpha testing to discard in shader adn remove deprecated state here + } + + unsigned int alphaBlend = stateset->getMode(GL_BLEND); + if (alphaBlend != osg::StateAttribute::INHERIT && (!mRequirements.back().mAlphaBlendOverridden || alphaBlend & osg::StateAttribute::PROTECTED)) + { + if (alphaBlend & osg::StateAttribute::OVERRIDE) + mRequirements.back().mAlphaBlendOverridden = true; + + mRequirements.back().mAlphaBlend = alphaBlend & osg::StateAttribute::ON; } } void ShaderVisitor::pushRequirements(osg::Node& node) { - mRequirements.push_back(mRequirements.back()); + if (mRequirements.empty()) + mRequirements.emplace_back(); + else + mRequirements.push_back(mRequirements.back()); mRequirements.back().mNode = &node; } @@ -294,14 +522,37 @@ namespace Shader void ShaderVisitor::createProgram(const ShaderRequirements &reqs) { if (!reqs.mShaderRequired && !mForceShaders) + { + ensureFFP(*reqs.mNode); return; + } + /** + * The shader visitor is supposed to be idempotent and undoable. + * That means we need to back up state we've removed (so it can be restored and/or considered by further + * applications of the visitor) and track which state we added (so it can be removed and/or ignored by further + * applications of the visitor). + * Before editing writableStateSet in a way that explicitly removes state or might overwrite existing state, it + * should be copied to removedState, another StateSet, unless it's there already or was added by a previous + * application of the visitor (is in previousAddedState). + * If it's a new class of state that's not already handled by ReinstateRemovedStateVisitor::apply, make sure to + * add handling there. + * Similarly, any time new state is added to writableStateSet, the equivalent method should be called on + * addedState. + * If that method doesn't exist yet, implement it - we don't use a full StateSet as we only need to check + * existence, not equality, and don't need to actually get the value as we can get it from writableStateSet + * instead. + */ osg::Node& node = *reqs.mNode; osg::StateSet* writableStateSet = nullptr; if (mAllowedToModifyStateSets) writableStateSet = node.getOrCreateStateSet(); else writableStateSet = getWritableStateSet(node); + osg::ref_ptr addedState = new AddedState; + osg::ref_ptr previousAddedState = getAddedState(*writableStateSet); + if (!previousAddedState) + previousAddedState = new AddedState; ShaderManager::DefineMap defineMap; for (unsigned int i=0; isecond + std::string("UV")] = std::to_string(texIt->first); } + if (defineMap["diffuseMap"] == "0") + { + writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); + addedState->addUniform("useDiffuseMapForShadowAlpha"); + } + defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); + addedState->addUniform("colorMode"); + + defineMap["alphaFunc"] = std::to_string(reqs.mAlphaFunc); + + defineMap["additiveBlending"] = reqs.mAdditiveBlending ? "1" : "0"; - osg::ref_ptr vertexShader (mShaderManager.getShader(mDefaultVsTemplate, defineMap, osg::Shader::VERTEX)); - osg::ref_ptr fragmentShader (mShaderManager.getShader(mDefaultFsTemplate, defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr removedState; + if ((removedState = getRemovedState(*writableStateSet)) && !mAllowedToModifyStateSets) + removedState = new osg::StateSet(*removedState, osg::CopyOp::SHALLOW_COPY); + if (!removedState) + removedState = new osg::StateSet(); + + defineMap["alphaToCoverage"] = "0"; + defineMap["adjustCoverage"] = "0"; + if (reqs.mAlphaFunc != osg::AlphaFunc::ALWAYS) + { + writableStateSet->addUniform(new osg::Uniform("alphaRef", reqs.mAlphaRef)); + addedState->addUniform("alphaRef"); + + if (!removedState->getAttributePair(osg::StateAttribute::ALPHAFUNC)) + { + const auto* alphaFunc = writableStateSet->getAttributePair(osg::StateAttribute::ALPHAFUNC); + if (alphaFunc && !previousAddedState->hasAttribute(osg::StateAttribute::ALPHAFUNC, 0)) + removedState->setAttribute(alphaFunc->first, alphaFunc->second); + } + // This prevents redundant glAlphaFunc calls while letting the shadows bin still see the test + writableStateSet->setAttribute(RemovedAlphaFunc::getInstance(reqs.mAlphaFunc), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + addedState->setAttribute(RemovedAlphaFunc::getInstance(reqs.mAlphaFunc)); + + // Blending won't work with A2C as we use the alpha channel for coverage. gl_SampleCoverage from ARB_sample_shading would save the day, but requires GLSL 130 + if (mConvertAlphaTestToAlphaToCoverage && !reqs.mAlphaBlend) + { + writableStateSet->setMode(GL_SAMPLE_ALPHA_TO_COVERAGE_ARB, osg::StateAttribute::ON); + addedState->setMode(GL_SAMPLE_ALPHA_TO_COVERAGE_ARB); + defineMap["alphaToCoverage"] = "1"; + } + + // Adjusting coverage isn't safe with blending on as blending requires the alpha to be intact. + // Maybe we could also somehow (e.g. userdata) detect when the diffuse map has coverage-preserving mip maps in the future + if (!reqs.mAlphaBlend) + defineMap["adjustCoverage"] = "1"; + + // Preventing alpha tested stuff shrinking as lower mip levels are used requires knowing the texture size + osg::ref_ptr exts = osg::GLExtensions::Get(0, false); + if (exts && exts->isGpuShader4Supported) + defineMap["useGPUShader4"] = "1"; + // We could fall back to a texture size uniform if EXT_gpu_shader4 is missing + } + + bool simpleLighting = false; + node.getUserValue("simpleLighting", simpleLighting); + if (simpleLighting) + defineMap["endLight"] = "0"; + + if (simpleLighting || dynamic_cast(&node)) + defineMap["forcePPL"] = "0"; + + if (reqs.mAlphaBlend && mSupportsNormalsRT) + { + if (reqs.mSoftParticles) + defineMap["disableNormals"] = "1"; + else + writableStateSet->setAttribute(new osg::Disablei(GL_BLEND, 1)); + } + + if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT && !previousAddedState->hasMode(GL_ALPHA_TEST)) + removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST)); + // This disables the deprecated fixed-function alpha test + writableStateSet->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED); + addedState->setMode(GL_ALPHA_TEST); + + if (!removedState->getModeList().empty() || !removedState->getAttributeList().empty()) + { + // user data is normally shallow copied so shared with the original stateset + osg::ref_ptr writableUserData; + if (mAllowedToModifyStateSets) + writableUserData = writableStateSet->getOrCreateUserDataContainer(); + else + writableUserData = getWritableUserDataContainer(*writableStateSet); + + updateRemovedState(*writableUserData, removedState); + } + + defineMap["softParticles"] = reqs.mSoftParticles ? "1" : "0"; + + Stereo::Manager::instance().shaderStereoDefines(defineMap); + + std::string shaderPrefix; + if (!node.getUserValue("shaderPrefix", shaderPrefix)) + shaderPrefix = mDefaultShaderPrefix; + + osg::ref_ptr vertexShader (mShaderManager.getShader(shaderPrefix + "_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr fragmentShader (mShaderManager.getShader(shaderPrefix + "_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); if (vertexShader && fragmentShader) { - writableStateSet->setAttributeAndModes(mShaderManager.getProgram(vertexShader, fragmentShader), osg::StateAttribute::ON); + auto program = mShaderManager.getProgram(vertexShader, fragmentShader, mProgramTemplate); + writableStateSet->setAttributeAndModes(program, osg::StateAttribute::ON); + addedState->setAttributeAndModes(program); for (std::map::const_iterator texIt = reqs.mTextures.begin(); texIt != reqs.mTextures.end(); ++texIt) { writableStateSet->addUniform(new osg::Uniform(texIt->second.c_str(), texIt->first), osg::StateAttribute::ON); + addedState->addUniform(texIt->second); + } + } + + if (!addedState->empty()) + { + // user data is normally shallow copied so shared with the original stateset + osg::ref_ptr writableUserData; + if (mAllowedToModifyStateSets) + writableUserData = writableStateSet->getOrCreateUserDataContainer(); + else + writableUserData = getWritableUserDataContainer(*writableStateSet); + + updateAddedState(*writableUserData, addedState); + } + } + + void ShaderVisitor::ensureFFP(osg::Node& node) + { + if (!node.getStateSet() || !node.getStateSet()->getAttribute(osg::StateAttribute::PROGRAM)) + return; + osg::StateSet* writableStateSet = nullptr; + if (mAllowedToModifyStateSets) + writableStateSet = node.getStateSet(); + else + writableStateSet = getWritableStateSet(node); + + /** + * We might have been using shaders temporarily with the node (e.g. if a GlowUpdater applied a temporary + * environment map for a temporary enchantment). + * We therefore need to remove any state doing so added, and restore any that it removed. + * This is kept track of in createProgram in the StateSet's userdata. + * If new classes of state get added, handling it here is required - not all StateSet features are implemented + * in AddedState yet as so far they've not been necessary. + * Removed state requires no particular special handling as it's dealt with by merging StateSets. + * We don't need to worry about state in writableStateSet having the OVERRIDE flag as if it's in both, it's also + * in addedState, and gets removed first. + */ + + // user data is normally shallow copied so shared with the original stateset - we'll need to copy before edits + osg::ref_ptr writableUserData; + + if (osg::ref_ptr addedState = getAddedState(*writableStateSet)) + { + if (mAllowedToModifyStateSets) + writableUserData = writableStateSet->getUserDataContainer(); + else + writableUserData = getWritableUserDataContainer(*writableStateSet); + + unsigned int index = writableUserData->getUserObjectIndex("addedState"); + writableUserData->removeUserObject(index); + + // O(n log n) to use StateSet::removeX, but this is O(n) + for (auto itr = writableStateSet->getUniformList().begin(); itr != writableStateSet->getUniformList().end();) + { + if (addedState->hasUniform(itr->first)) + writableStateSet->getUniformList().erase(itr++); + else + ++itr; + } + + for (auto itr = writableStateSet->getModeList().begin(); itr != writableStateSet->getModeList().end();) + { + if (addedState->hasMode(itr->first)) + writableStateSet->getModeList().erase(itr++); + else + ++itr; + } + + // StateAttributes track the StateSets they're attached to + // We don't have access to the function to do that, and can't call removeAttribute with an iterator + for (const auto& [type, member] : addedState->getAttributes()) + writableStateSet->removeAttribute(type, member); + + for (unsigned int unit = 0; unit < writableStateSet->getTextureModeList().size(); ++unit) + { + for (auto itr = writableStateSet->getTextureModeList()[unit].begin(); itr != writableStateSet->getTextureModeList()[unit].end();) + { + if (addedState->hasTextureMode(unit, itr->first)) + writableStateSet->getTextureModeList()[unit].erase(itr++); + else + ++itr; + } + } + + for (const auto& [unit, attributeList] : addedState->getTextureAttributes()) + { + for (const auto& [type, member] : attributeList) + writableStateSet->removeTextureAttribute(unit, type); + } + } + + + if (osg::ref_ptr removedState = getRemovedState(*writableStateSet)) + { + if (!writableUserData) + { + if (mAllowedToModifyStateSets) + writableUserData = writableStateSet->getUserDataContainer(); + else + writableUserData = getWritableUserDataContainer(*writableStateSet); } + + unsigned int index = writableUserData->getUserObjectIndex("removedState"); + writableUserData->removeUserObject(index); + + writableStateSet->merge(*removedState); } } @@ -380,6 +835,8 @@ namespace Shader createProgram(reqs); } + else + ensureFFP(geometry); if (needPop) popRequirements(); @@ -387,13 +844,14 @@ namespace Shader void ShaderVisitor::apply(osg::Drawable& drawable) { - // non-Geometry drawable (e.g. particle system) - bool needPop = (drawable.getStateSet() != nullptr); + bool needPop = drawable.getStateSet(); - if (drawable.getStateSet()) + if (needPop) { pushRequirements(drawable); - applyStateSet(drawable.getStateSet(), drawable); + + if (drawable.getStateSet()) + applyStateSet(drawable.getStateSet(), drawable); } if (!mRequirements.empty()) @@ -413,7 +871,20 @@ namespace Shader if (sourceGeometry && adjustGeometry(*sourceGeometry, reqs)) morph->setSourceGeometry(sourceGeometry); } + else if (auto osgaRig = dynamic_cast(&drawable)) + { + osg::ref_ptr sourceOsgaRigGeometry = osgaRig->getSourceRigGeometry(); + osg::ref_ptr sourceGeometry = sourceOsgaRigGeometry->getSourceGeometry(); + if (sourceGeometry && adjustGeometry(*sourceGeometry, reqs)) + { + sourceOsgaRigGeometry->setSourceGeometry(sourceGeometry); + osgaRig->setSourceRigGeometry(sourceOsgaRigGeometry); + } + } + } + else + ensureFFP(drawable); if (needPop) popRequirements(); @@ -454,4 +925,58 @@ namespace Shader mApplyLightingToEnvMaps = apply; } + void ShaderVisitor::setConvertAlphaTestToAlphaToCoverage(bool convert) + { + mConvertAlphaTestToAlphaToCoverage = convert; + } + + ReinstateRemovedStateVisitor::ReinstateRemovedStateVisitor(bool allowedToModifyStateSets) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mAllowedToModifyStateSets(allowedToModifyStateSets) + { + } + + void ReinstateRemovedStateVisitor::apply(osg::Node& node) + { + // TODO: this may eventually need to remove added state. + // If so, we can migrate from explicitly copying removed state to just calling osg::StateSet::merge. + // Not everything is transferred from removedState yet - implement more when createProgram starts marking more + // as removed. + if (node.getStateSet()) + { + osg::ref_ptr removedState = getRemovedState(*node.getStateSet()); + if (removedState) + { + osg::ref_ptr writableStateSet; + if (mAllowedToModifyStateSets) + writableStateSet = node.getStateSet(); + else + writableStateSet = getWritableStateSet(node); + + // user data is normally shallow copied so shared with the original stateset + osg::ref_ptr writableUserData; + if (mAllowedToModifyStateSets) + writableUserData = writableStateSet->getUserDataContainer(); + else + writableUserData = getWritableUserDataContainer(*writableStateSet); + unsigned int index = writableUserData->getUserObjectIndex("removedState"); + writableUserData->removeUserObject(index); + + for (const auto&[mode, value] : removedState->getModeList()) + writableStateSet->setMode(mode, value); + + for (const auto& attribute : removedState->getAttributeList()) + writableStateSet->setAttribute(attribute.second.first, attribute.second.second); + + for (unsigned int unit = 0; unit < removedState->getTextureModeList().size(); ++unit) + { + for (const auto&[mode, value] : removedState->getTextureModeList()[unit]) + writableStateSet->setTextureMode(unit, mode, value); + } + } + } + + traverse(node); + } + } diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index 6b2353b662..02323f528f 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -1,7 +1,11 @@ #ifndef OPENMW_COMPONENTS_SHADERVISITOR_H #define OPENMW_COMPONENTS_SHADERVISITOR_H +#include + #include +#include +#include namespace Resource { @@ -17,7 +21,9 @@ namespace Shader class ShaderVisitor : public osg::NodeVisitor { public: - ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string& defaultVsTemplate, const std::string& defaultFsTemplate); + ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string& defaultShaderPrefix); + + void setProgramTemplate(const osg::Program* programTemplate) { mProgramTemplate = programTemplate; } /// By default, only bump mapped objects will have a shader added to them. /// Setting force = true will cause all objects to render using shaders, regardless of having a bump map. @@ -40,6 +46,10 @@ namespace Shader void setApplyLightingToEnvMaps(bool apply); + void setConvertAlphaTestToAlphaToCoverage(bool convert); + + void setSupportsNormalsRT(bool supports) { mSupportsNormalsRT = supports; } + void apply(osg::Node& node) override; void apply(osg::Drawable& drawable) override; @@ -63,13 +73,17 @@ namespace Shader bool mApplyLightingToEnvMaps; + bool mConvertAlphaTestToAlphaToCoverage; + + bool mSupportsNormalsRT; + ShaderManager& mShaderManager; Resource::ImageManager& mImageManager; struct ShaderRequirements { ShaderRequirements(); - ~ShaderRequirements(); + ~ShaderRequirements() = default; // std::map mTextures; @@ -77,24 +91,48 @@ namespace Shader bool mShaderRequired; int mColorMode; - + bool mMaterialOverridden; + bool mAlphaTestOverridden; + bool mAlphaBlendOverridden; + + GLenum mAlphaFunc; + float mAlphaRef; + bool mAlphaBlend; + + bool mBlendFuncOverridden; + bool mAdditiveBlending; bool mNormalHeight; // true if normal map has height info in alpha channel // -1 == no tangents required int mTexStageRequiringTangents; + bool mSoftParticles; + // the Node that requested these requirements osg::Node* mNode; }; std::vector mRequirements; - std::string mDefaultVsTemplate; - std::string mDefaultFsTemplate; + std::string mDefaultShaderPrefix; void createProgram(const ShaderRequirements& reqs); + void ensureFFP(osg::Node& node); bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs); + + osg::ref_ptr mProgramTemplate; + }; + + class ReinstateRemovedStateVisitor : public osg::NodeVisitor + { + public: + ReinstateRemovedStateVisitor(bool allowedToModifyStateSets); + + void apply(osg::Node& node) override; + + private: + bool mAllowedToModifyStateSets; }; } diff --git a/components/sqlite3/db.cpp b/components/sqlite3/db.cpp new file mode 100644 index 0000000000..f341fef064 --- /dev/null +++ b/components/sqlite3/db.cpp @@ -0,0 +1,34 @@ +#include "db.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3::operator()(sqlite3* handle) const noexcept + { + sqlite3_close_v2(handle); + } + + Db makeDb(std::string_view path, const char* schema) + { + sqlite3* handle = nullptr; + // All uses of NavMeshDb are protected by a mutex (navmeshtool) or serialized in a single thread (DbWorker) + // so additional synchronization between threads is not required and SQLITE_OPEN_NOMUTEX can be used. + // This is unsafe to use NavMeshDb without external synchronization because of internal state. + const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX; + if (const int ec = sqlite3_open_v2(std::string(path).c_str(), &handle, flags, nullptr); ec != SQLITE_OK) + { + const std::string message(sqlite3_errmsg(handle)); + sqlite3_close(handle); + throw std::runtime_error("Failed to open database: " + message); + } + Db result(handle); + if (const int ec = sqlite3_exec(result.get(), schema, nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed create database schema: " + std::string(sqlite3_errmsg(handle))); + return result; + } +} diff --git a/components/sqlite3/db.hpp b/components/sqlite3/db.hpp new file mode 100644 index 0000000000..293ace4375 --- /dev/null +++ b/components/sqlite3/db.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_DB_H +#define OPENMW_COMPONENTS_SQLITE3_DB_H + +#include +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct CloseSqlite3 + { + void operator()(sqlite3* handle) const noexcept; + }; + + using Db = std::unique_ptr; + + Db makeDb(std::string_view path, const char* schema); +} + +#endif diff --git a/components/sqlite3/request.hpp b/components/sqlite3/request.hpp new file mode 100644 index 0000000000..0a74bf1cb3 --- /dev/null +++ b/components/sqlite3/request.hpp @@ -0,0 +1,284 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_REQUEST_H +#define OPENMW_COMPONENTS_SQLITE3_REQUEST_H + +#include "statement.hpp" +#include "types.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sqlite3 +{ + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, int value) + { + if (const int ec = sqlite3_bind_int(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::int64_t value) + { + if (const int ec = sqlite3_bind_int64(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int64 to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, double value) + { + if (const int ec = sqlite3_bind_double(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind double to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::string_view value) + { + if (sqlite3_bind_text(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind text to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const std::vector& value) + { + if (sqlite3_bind_blob(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const ConstBlob& value) + { + if (sqlite3_bind_blob(&stmt, index, value.mData, value.mSize, SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + template + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value) + { + const int index = sqlite3_bind_parameter_index(&stmt, name); + if (index == 0) + throw std::logic_error("Parameter \"" + std::string(name) + "\" is not found"); + bindParameter(db, stmt, index, value); + } + + inline std::string sqliteTypeToString(int value) + { + switch (value) + { + case SQLITE_INTEGER: return "SQLITE_INTEGER"; + case SQLITE_FLOAT: return "SQLITE_FLOAT"; + case SQLITE_TEXT: return "SQLITE_TEXT"; + case SQLITE_BLOB: return "SQLITE_BLOB"; + case SQLITE_NULL: return "SQLITE_NULL"; + } + return "unsupported(" + std::to_string(value) + ")"; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& /*statement*/, int index, int type, T*& value) + { + if (type != SQLITE_NULL) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT"); + value = nullptr; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& statement, int index, int type, T& value) + { + switch (type) + { + case SQLITE_INTEGER: + value = static_cast(sqlite3_column_int64(&statement, index)); + return; + case SQLITE_FLOAT: + value = static_cast(sqlite3_column_double(&statement, index)); + return; + case SQLITE_NULL: + value = std::decay_t{}; + return; + } + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT or SQLITE_NULL"); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::string& value) + { + if (type != SQLITE_TEXT) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_TEXT"); + const unsigned char* const text = sqlite3_column_text(&statement, index); + if (text == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read text from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(reinterpret_cast(text), reinterpret_cast(text) + size); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::vector& value) + { + if (type != SQLITE_BLOB) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_BLOB"); + const void* const blob = sqlite3_column_blob(&statement, index); + if (blob == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read blob from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(static_cast(blob), static_cast(blob) + size); + } + + template + inline void getColumnsImpl(sqlite3& db, sqlite3_stmt& statement, T& row) + { + if constexpr (0 < index && index <= std::tuple_size_v) + { + const int column = index - 1; + if (const int number = sqlite3_column_count(&statement); column >= number) + throw std::out_of_range("Column number is out of range: " + std::to_string(column) + + " >= " + std::to_string(number)); + const int type = sqlite3_column_type(&statement, column); + switch (type) + { + case SQLITE_INTEGER: + case SQLITE_FLOAT: + case SQLITE_TEXT: + case SQLITE_BLOB: + case SQLITE_NULL: + copyColumn(db, statement, column, type, std::get(row)); + break; + default: + throw std::runtime_error("Column " + std::to_string(column) + + " has unnsupported column type: " + sqliteTypeToString(type)); + } + getColumnsImpl(db, statement, row); + } + } + + template + inline void getColumns(sqlite3& db, sqlite3_stmt& statement, T& row) + { + getColumnsImpl>(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, T& row) + { + auto tuple = std::tie(row); + getColumns(db, statement, tuple); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::tuple& row) + { + getColumns(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::back_insert_iterator& it) + { + typename T::value_type row; + getRow(db, statement, row); + it = std::move(row); + } + + template + inline void prepare(sqlite3& db, Statement& statement, Args&& ... args) + { + if (statement.mNeedReset) + { + if (sqlite3_reset(statement.mHandle.get()) == SQLITE_OK + && sqlite3_clear_bindings(statement.mHandle.get()) == SQLITE_OK) + statement.mNeedReset = false; + else + statement.mHandle = makeStatementHandle(db, statement.mQuery.text()); + } + statement.mQuery.bind(db, *statement.mHandle, std::forward(args) ...); + } + + template + inline bool executeStep(sqlite3& db, const Statement& statement) + { + switch (sqlite3_step(statement.mHandle.get())) + { + case SQLITE_ROW: return true; + case SQLITE_DONE: return false; + } + throw std::runtime_error("Failed to execute statement step: " + std::string(sqlite3_errmsg(&db))); + } + + template + inline I request(sqlite3& db, Statement& statement, I out, std::size_t max, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + for (std::size_t i = 0; executeStep(db, statement) && i < max; ++i) + getRow(db, *statement.mHandle, *out++); + return out; + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed perform request \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } + + template + inline int execute(sqlite3& db, Statement& statement, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + if (executeStep(db, statement)) + throw std::logic_error("Execute cannot return rows"); + return sqlite3_changes(&db); + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed to execute statement \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } +} + +#endif diff --git a/components/sqlite3/statement.cpp b/components/sqlite3/statement.cpp new file mode 100644 index 0000000000..07ca6b8ddf --- /dev/null +++ b/components/sqlite3/statement.cpp @@ -0,0 +1,24 @@ +#include "statement.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3Stmt::operator()(sqlite3_stmt* handle) const noexcept + { + sqlite3_finalize(handle); + } + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query) + { + sqlite3_stmt* stmt = nullptr; + if (const int ec = sqlite3_prepare_v2(&db, query.data(), static_cast(query.size()), &stmt, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to prepare statement for query \"" + std::string(query) + "\": " + + std::string(sqlite3_errmsg(&db))); + return StatementHandle(stmt); + } +} diff --git a/components/sqlite3/statement.hpp b/components/sqlite3/statement.hpp new file mode 100644 index 0000000000..469e63933c --- /dev/null +++ b/components/sqlite3/statement.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_STATEMENT_H +#define OPENMW_COMPONENTS_SQLITE3_STATEMENT_H + +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace Sqlite3 +{ + struct CloseSqlite3Stmt + { + void operator()(sqlite3_stmt* handle) const noexcept; + }; + + using StatementHandle = std::unique_ptr; + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query); + + template + struct Statement + { + bool mNeedReset = false; + StatementHandle mHandle; + Query mQuery; + + explicit Statement(sqlite3& db, Query query = Query {}) + : mHandle(makeStatementHandle(db, query.text())), + mQuery(std::move(query)) {} + }; +} + +#endif diff --git a/components/sqlite3/transaction.cpp b/components/sqlite3/transaction.cpp new file mode 100644 index 0000000000..bafd6e8d32 --- /dev/null +++ b/components/sqlite3/transaction.cpp @@ -0,0 +1,51 @@ +#include "transaction.hpp" + +#include + +#include + +#include +#include + +namespace Sqlite3 +{ + namespace + { + const char* getBeginStatement(TransactionMode mode) + { + switch (mode) + { + case TransactionMode::Default: return "BEGIN"; + case TransactionMode::Deferred: return "BEGIN DEFERRED"; + case TransactionMode::Immediate: return "BEGIN IMMEDIATE"; + case TransactionMode::Exclusive: return "BEGIN EXCLUSIVE"; + } + throw std::logic_error("Invalid transaction mode: " + std::to_string(static_cast>(mode))); + } + } + + void Rollback::operator()(sqlite3* db) const + { + if (db == nullptr) + return; + if (const int ec = sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); ec != SQLITE_OK) + Log(Debug::Debug) << "Failed to rollback SQLite3 transaction: " << sqlite3_errmsg(db) << " (" << ec << ")"; + } + + Transaction::Transaction(sqlite3& db, TransactionMode mode) + : mDb(&db) + { + if (const int ec = sqlite3_exec(&db, getBeginStatement(mode), nullptr, nullptr, nullptr); ec != SQLITE_OK) + { + (void) mDb.release(); + throw std::runtime_error("Failed to start transaction: " + std::string(sqlite3_errmsg(&db)) + " (" + std::to_string(ec) + ")"); + } + } + + void Transaction::commit() + { + if (const int ec = sqlite3_exec(mDb.get(), "COMMIT", nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to commit transaction: " + std::string(sqlite3_errmsg(mDb.get())) + " (" + std::to_string(ec) + ")"); + (void) mDb.release(); + } +} diff --git a/components/sqlite3/transaction.hpp b/components/sqlite3/transaction.hpp new file mode 100644 index 0000000000..ff23bc03b5 --- /dev/null +++ b/components/sqlite3/transaction.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H +#define OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H + +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct Rollback + { + void operator()(sqlite3* handle) const; + }; + + enum class TransactionMode + { + Default, + Deferred, + Immediate, + Exclusive, + }; + + class Transaction + { + public: + explicit Transaction(sqlite3& db, TransactionMode mode = TransactionMode::Default); + + void commit(); + + private: + std::unique_ptr mDb; + }; +} + +#endif diff --git a/components/sqlite3/types.hpp b/components/sqlite3/types.hpp new file mode 100644 index 0000000000..325e9e6608 --- /dev/null +++ b/components/sqlite3/types.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_TYPES_H +#define OPENMW_COMPONENTS_SQLITE3_TYPES_H + +#include + +namespace Sqlite3 +{ + struct ConstBlob + { + const char* mData; + int mSize; + }; +} + +#endif diff --git a/components/std140/ubo.hpp b/components/std140/ubo.hpp new file mode 100644 index 0000000000..6154cd32ac --- /dev/null +++ b/components/std140/ubo.hpp @@ -0,0 +1,162 @@ +#ifndef COMPONENTS_STD140_UBO_H +#define COMPONENTS_STD140_UBO_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace std140 +{ + struct Mat4 + { + using Value = osg::Matrixf; + Value mValue; + static constexpr size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "mat4"; + }; + + struct Vec4 + { + using Value = osg::Vec4f; + Value mValue; + static constexpr size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "vec4"; + }; + + struct Vec3 + { + using Value = osg::Vec3f; + Value mValue; + static constexpr std::size_t sAlign = 4 * sizeof(osg::Vec3f::value_type); + static constexpr std::string_view sTypeName = "vec3"; + }; + + struct Vec2 + { + using Value = osg::Vec2f; + Value mValue; + static constexpr std::size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "vec2"; + }; + + struct Float + { + using Value = float; + Value mValue; + static constexpr std::size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "float"; + }; + + struct Int + { + using Value = std::int32_t; + Value mValue; + static constexpr std::size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "int"; + }; + + struct UInt + { + using Value = std::uint32_t; + Value mValue; + static constexpr std::size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "uint"; + }; + + struct Bool + { + using Value = std::int32_t; + Value mValue; + static constexpr std::size_t sAlign = sizeof(Value); + static constexpr std::string_view sTypeName = "bool"; + }; + + template + class UBO + { + private: + + template + struct contains : std::bool_constant<(std::is_base_of_v || ...)> { }; + + static_assert((contains() && ...)); + + static constexpr size_t roundUpRemainder(size_t x, size_t multiple) + { + size_t remainder = x % multiple; + if (remainder == 0) + return 0; + return multiple - remainder; + } + + template + static constexpr std::size_t getOffset() + { + bool found = false; + std::size_t size = 0; + (( + found = found || std::is_same_v, + size += (found ? 0 : sizeof(typename CArgs::Value) + roundUpRemainder(size, CArgs::sAlign)) + ) , ...); + return size + roundUpRemainder(size, T::sAlign); + } + + public: + + static constexpr size_t getGPUSize() + { + std::size_t size = 0; + ((size += (sizeof(typename CArgs::Value) + roundUpRemainder(size, CArgs::sAlign))), ...); + return size; + } + + static std::string getDefinition(const std::string& name) + { + std::string structDefinition = "struct " + name + " {\n"; + ((structDefinition += (" " + std::string(CArgs::sTypeName) + " " + std::string(CArgs::sName) + ";\n")), ...); + return structDefinition + "};"; + } + + using BufferType = std::array; + using TupleType = std::tuple; + + template + typename T::Value& get() + { + return std::get(mData).mValue; + } + + template + const typename T::Value& get() const + { + return std::get(mData).mValue; + } + + void copyTo(BufferType& buffer) const + { + const auto copy = [&] (const auto& v) { + static_assert(std::is_standard_layout_v>); + constexpr std::size_t offset = getOffset>(); + std::memcpy(buffer.data() + offset, &v.mValue, sizeof(v.mValue)); + }; + + std::apply([&] (const auto& ... v) { (copy(v) , ...); }, mData); + } + + const auto& getData() const + { + return mData; + } + + private: + std::tuple mData; + }; +} + +#endif diff --git a/components/stereo/frustum.cpp b/components/stereo/frustum.cpp new file mode 100644 index 0000000000..40a355ed55 --- /dev/null +++ b/components/stereo/frustum.cpp @@ -0,0 +1,152 @@ +#include "stereomanager.hpp" +#include "multiview.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include "frustum.hpp" + +namespace Stereo +{ + struct MultiviewFrustumCallback final : public Stereo::InitialFrustumCallback + { + MultiviewFrustumCallback(StereoFrustumManager* sfm, osg::Camera* camera) + : Stereo::InitialFrustumCallback(camera) + , mSfm(sfm) + { + + } + + void setInitialFrustum(osg::CullStack& cullStack, osg::BoundingBoxd& bb, bool& nearCulling, bool& farCulling) const override + { + auto cm = cullStack.getCullingMode(); + nearCulling = !!(cm & osg::CullSettings::NEAR_PLANE_CULLING); + farCulling = !!(cm & osg::CullSettings::FAR_PLANE_CULLING); + bb = mSfm->boundingBox(); + } + + StereoFrustumManager* mSfm; + }; + + struct ShadowFrustumCallback final : public SceneUtil::MWShadowTechnique::CustomFrustumCallback + { + ShadowFrustumCallback(StereoFrustumManager* parent) : mParent(parent) + { + } + + void operator()(osgUtil::CullVisitor& cv, osg::BoundingBoxd& customClipSpace, osgUtil::CullVisitor*& sharedFrustumHint) override + { + mParent->customFrustumCallback(cv, customClipSpace, sharedFrustumHint); + } + + StereoFrustumManager* mParent; + }; + + void joinBoundingBoxes(const osg::Matrix& masterProjection, const osg::Matrix& slaveProjection, osg::BoundingBoxd& bb) + { + static const std::array clipCorners = {{ + {-1.0, -1.0, -1.0}, + { 1.0, -1.0, -1.0}, + { 1.0, -1.0, 1.0}, + {-1.0, -1.0, 1.0}, + {-1.0, 1.0, -1.0}, + { 1.0, 1.0, -1.0}, + { 1.0, 1.0, 1.0}, + {-1.0, 1.0, 1.0} + }}; + + osg::Matrix slaveClipToView; + slaveClipToView.invert(slaveProjection); + + for (const auto& clipCorner : clipCorners) + { + auto masterViewVertice = clipCorner * slaveClipToView; + auto masterClipVertice = masterViewVertice * masterProjection; + bb.expandBy(masterClipVertice); + } + } + + StereoFrustumManager::StereoFrustumManager(osg::Camera* camera) + : mCamera(camera) + , mShadowTechnique(nullptr) + , mShadowFrustumCallback(nullptr) + { + if (Stereo::getMultiview()) + { + mMultiviewFrustumCallback = std::make_unique(this, camera); + } + + if (Settings::Manager::getBool("shared shadow maps", "Stereo")) + { + mShadowFrustumCallback = new ShadowFrustumCallback(this); + auto* renderer = static_cast(mCamera->getRenderer()); + for (auto* sceneView : { renderer->getSceneView(0), renderer->getSceneView(1) }) + { + mSharedFrustums[sceneView->getCullVisitorRight()] = sceneView->getCullVisitorLeft(); + } + } + } + + StereoFrustumManager::~StereoFrustumManager() + { + if (mShadowTechnique) + mShadowTechnique->setCustomFrustumCallback(nullptr); + } + + void StereoFrustumManager::setShadowTechnique( + SceneUtil::MWShadowTechnique* shadowTechnique) + { + if (mShadowTechnique) + mShadowTechnique->setCustomFrustumCallback(nullptr); + mShadowTechnique = shadowTechnique; + if (mShadowTechnique) + mShadowTechnique->setCustomFrustumCallback(mShadowFrustumCallback); + } + + void StereoFrustumManager::customFrustumCallback( + osgUtil::CullVisitor& cv, + osg::BoundingBoxd& customClipSpace, + osgUtil::CullVisitor*& sharedFrustumHint) + { + auto it = mSharedFrustums.find(&cv); + if (it != mSharedFrustums.end()) + { + sharedFrustumHint = it->second; + } + + customClipSpace = mBoundingBox; + } + + void StereoFrustumManager::update(std::array projections) + { + mBoundingBox.init(); + for (auto& projection : projections) + joinBoundingBoxes(mCamera->getProjectionMatrix(), projection, mBoundingBox); + } +} diff --git a/components/stereo/frustum.hpp b/components/stereo/frustum.hpp new file mode 100644 index 0000000000..d6a624987d --- /dev/null +++ b/components/stereo/frustum.hpp @@ -0,0 +1,71 @@ +#ifndef STEREO_FRUSTUM_H +#define STEREO_FRUSTUM_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace osg +{ + class FrameBufferObject; + class Texture2D; + class Texture2DMultisample; + class Texture2DArray; +} + +namespace osgViewer +{ + class Viewer; +} + +namespace usgUtil +{ + class CullVisitor; +} + +namespace SceneUtil +{ + class MWShadowTechnique; +} + +namespace Stereo +{ + struct MultiviewFrustumCallback; + struct ShadowFrustumCallback; + + void joinBoundingBoxes(const osg::Matrix& masterProjection, const osg::Matrix& slaveProjection, osg::BoundingBoxd& bb); + + class StereoFrustumManager + { + public: + StereoFrustumManager(osg::Camera* camera); + ~StereoFrustumManager(); + + void update(std::array projections); + + const osg::BoundingBoxd& boundingBox() const { return mBoundingBox; } + + void setShadowTechnique(SceneUtil::MWShadowTechnique* shadowTechnique); + + void customFrustumCallback(osgUtil::CullVisitor& cv, osg::BoundingBoxd& customClipSpace, osgUtil::CullVisitor*& sharedFrustumHint); + + private: + osg::ref_ptr mCamera; + osg::ref_ptr mShadowTechnique; + osg::ref_ptr mShadowFrustumCallback; + std::map< osgUtil::CullVisitor*, osgUtil::CullVisitor*> mSharedFrustums; + osg::BoundingBoxd mBoundingBox; + + std::unique_ptr mMultiviewFrustumCallback; + }; +} + +#endif diff --git a/components/stereo/multiview.cpp b/components/stereo/multiview.cpp new file mode 100644 index 0000000000..e0cac44714 --- /dev/null +++ b/components/stereo/multiview.cpp @@ -0,0 +1,796 @@ +#include "multiview.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef OSG_HAS_MULTIVIEW +#include +#endif + +#include +#include +#include +#include + +#include + +namespace Stereo +{ + namespace + { + bool getMultiviewSupportedImpl(unsigned int contextID) + { +#ifdef OSG_HAS_MULTIVIEW + if (!osg::isGLExtensionSupported(contextID, "GL_OVR_multiview")) + { + Log(Debug::Verbose) << "Disabling Multiview (opengl extension \"GL_OVR_multiview\" not supported)"; + return false; + } + + if (!osg::isGLExtensionSupported(contextID, "GL_OVR_multiview2")) + { + Log(Debug::Verbose) << "Disabling Multiview (opengl extension \"GL_OVR_multiview2\" not supported)"; + return false; + } + return true; +#else + Log(Debug::Verbose) << "Disabling Multiview (OSG does not support multiview)"; + return false; +#endif + } + + bool getMultiviewSupported(unsigned int contextID) + { + static bool supported = getMultiviewSupportedImpl(contextID); + return supported; + } + + bool getTextureViewSupportedImpl(unsigned int contextID) + { + if (!osg::isGLExtensionOrVersionSupported(contextID, "ARB_texture_view", 4.3)) + { + Log(Debug::Verbose) << "Disabling texture views (opengl extension \"ARB_texture_view\" not supported)"; + return false; + } + return true; + } + + bool getTextureViewSupported(unsigned int contextID) + { + static bool supported = getTextureViewSupportedImpl(contextID); + return supported; + } + + bool getMultiviewImpl(unsigned int contextID) + { + if (!Stereo::getStereo()) + { + Log(Debug::Verbose) << "Disabling Multiview (disabled by config)"; + return false; + } + + if (!Settings::Manager::getBool("multiview", "Stereo")) + { + Log(Debug::Verbose) << "Disabling Multiview (disabled by config)"; + return false; + } + + if (!getMultiviewSupported(contextID)) + { + return false; + } + + if (!getTextureViewSupported(contextID)) + { + Log(Debug::Verbose) << "Disabling Multiview (texture views not supported)"; + return false; + } + + Log(Debug::Verbose) << "Enabling Multiview"; + return true; + } + + bool getMultiview(unsigned int contextID) + { + static bool multiView = getMultiviewImpl(contextID); + return multiView; + } + } + + bool getTextureViewSupported() + { + return getTextureViewSupported(0); + } + + bool getMultiview() + { + return getMultiview(0); + } + + void configureExtensions(unsigned int contextID) + { + getTextureViewSupported(contextID); + getMultiviewSupported(contextID); + getMultiview(contextID); + } + + void setVertexBufferHint() + { + if (getStereo() && Settings::Manager::getBool("multiview", "Stereo")) + { + auto* ds = osg::DisplaySettings::instance().get(); + if (!Settings::Manager::getBool("allow display lists for multiview", "Stereo") + && ds->getVertexBufferHint() == osg::DisplaySettings::VertexBufferHint::NO_PREFERENCE) + { + // Note that this only works if this code is executed before realize() is called on the viewer. + // The hint is read by the state object only once, before the user realize operations are run. + // Therefore we have to set this hint without access to a graphics context to let us determine + // if multiview will actually be supported or not. So if the user has requested multiview, we + // will just have to set it regardless. + ds->setVertexBufferHint(osg::DisplaySettings::VertexBufferHint::VERTEX_BUFFER_OBJECT); + Log(Debug::Verbose) << "Disabling display lists"; + } + } + } + + class Texture2DViewSubloadCallback : public osg::Texture2D::SubloadCallback + { + public: + Texture2DViewSubloadCallback(osg::Texture2DArray* textureArray, int layer); + + void load(const osg::Texture2D& texture, osg::State& state) const override; + void subload(const osg::Texture2D& texture, osg::State& state) const override; + + private: + osg::ref_ptr mTextureArray; + int mLayer; + }; + + Texture2DViewSubloadCallback::Texture2DViewSubloadCallback(osg::Texture2DArray* textureArray, int layer) + : mTextureArray(textureArray) + , mLayer(layer) + { + } + + void Texture2DViewSubloadCallback::load(const osg::Texture2D& texture, osg::State& state) const + { + state.checkGLErrors("before Texture2DViewSubloadCallback::load()"); + + auto contextId = state.getContextID(); + auto* gl = osg::GLExtensions::Get(contextId, false); + mTextureArray->apply(state); + + auto sourceTextureObject = mTextureArray->getTextureObject(contextId); + if (!sourceTextureObject) + { + Log(Debug::Error) << "Texture2DViewSubloadCallback: Texture2DArray did not have a texture object"; + return; + } + + osg::Texture::TextureObject* const targetTextureObject = texture.getTextureObject(contextId); + if (targetTextureObject == nullptr) + { + Log(Debug::Error) << "Texture2DViewSubloadCallback: Texture2D did not have a texture object"; + return; + } + + + // OSG already bound this texture ID, giving it a target. + // Delete it and make a new texture ID. + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &targetTextureObject->_id); + glGenTextures(1, &targetTextureObject->_id); + + auto sourceId = sourceTextureObject->_id; + auto targetId = targetTextureObject->_id; + auto internalFormat = sourceTextureObject->_profile._internalFormat; + auto levels = std::max(1, sourceTextureObject->_profile._numMipmapLevels); + + { + ////// OSG BUG + // Texture views require immutable storage. + // OSG should always give immutable storage to sized internal formats, but does not do so for depth formats. + // Fortunately, we can just call glTexStorage3D here to make it immutable. This probably discards depth info for that frame, but whatever. +#ifndef GL_TEXTURE_IMMUTABLE_FORMAT +#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F +#endif + // Store any current binding and re-apply it after so i don't mess with state. + GLint oldBinding = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &oldBinding); + + // Bind the source texture and check if it's immutable. + glBindTexture(GL_TEXTURE_2D_ARRAY, sourceId); + GLint immutable = 0; + glGetTexParameteriv(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_IMMUTABLE_FORMAT, &immutable); + if(!immutable) + { + // It wasn't immutable, so make it immutable. + gl->glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, internalFormat, sourceTextureObject->_profile._width, sourceTextureObject->_profile._height, 2); + state.checkGLErrors("after Texture2DViewSubloadCallback::load()::glTexStorage3D"); + } + glBindTexture(GL_TEXTURE_2D_ARRAY, oldBinding); + } + + gl->glTextureView(targetId, GL_TEXTURE_2D, sourceId, internalFormat, 0, levels, mLayer, 1); + state.checkGLErrors("after Texture2DViewSubloadCallback::load()::glTextureView"); + glBindTexture(GL_TEXTURE_2D, targetId); + } + + void Texture2DViewSubloadCallback::subload(const osg::Texture2D& texture, osg::State& state) const + { + // Nothing to do + } + + osg::ref_ptr createTextureView_Texture2DFromTexture2DArray(osg::Texture2DArray* textureArray, int layer) + { + if (!getTextureViewSupported()) + { + Log(Debug::Error) << "createTextureView_Texture2DFromTexture2DArray: Tried to use a texture view but glTextureView is not supported"; + return nullptr; + } + + osg::ref_ptr texture2d = new osg::Texture2D; + texture2d->setSubloadCallback(new Texture2DViewSubloadCallback(textureArray, layer)); + texture2d->setTextureSize(textureArray->getTextureWidth(), textureArray->getTextureHeight()); + texture2d->setBorderColor(textureArray->getBorderColor()); + texture2d->setBorderWidth(textureArray->getBorderWidth()); + texture2d->setLODBias(textureArray->getLODBias()); + texture2d->setFilter(osg::Texture::FilterParameter::MAG_FILTER, textureArray->getFilter(osg::Texture::FilterParameter::MAG_FILTER)); + texture2d->setFilter(osg::Texture::FilterParameter::MIN_FILTER, textureArray->getFilter(osg::Texture::FilterParameter::MIN_FILTER)); + texture2d->setInternalFormat(textureArray->getInternalFormat()); + texture2d->setNumMipmapLevels(textureArray->getNumMipmapLevels()); + return texture2d; + } + +#ifdef OSG_HAS_MULTIVIEW + //! Draw callback that, if set on a RenderStage, resolves MSAA after draw. Needed when using custom fbo/resolve fbos on renderstages in combination with multiview. + struct MultiviewMSAAResolveCallback : public osgUtil::RenderBin::DrawCallback + { + void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override + { + osgUtil::RenderStage* stage = static_cast(bin); + auto msaaFbo = stage->getFrameBufferObject(); + auto resolveFbo = stage->getMultisampleResolveFramebufferObject(); + if (msaaFbo != mMsaaFbo) + { + mMsaaFbo = msaaFbo; + setupMsaaLayers(); + } + if (resolveFbo != mFbo) + { + mFbo = resolveFbo; + setupLayers(); + } + + // Null the resolve framebuffer to keep osg from doing redundant work. + stage->setMultisampleResolveFramebufferObject(nullptr); + + // Do the actual render work + bin->drawImplementation(renderInfo, previous); + + // Blit layers + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + for (int i = 0; i < 2; i++) + { + mLayers[i]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + mMsaaLayers[i]->apply(state, osg::FrameBufferObject::READ_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, mWidth, mHeight, 0, 0, mWidth, mHeight, GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + msaaFbo->apply(state, osg::FrameBufferObject::READ_DRAW_FRAMEBUFFER); + } + + void setupLayers() + { + const auto& attachments = mFbo->getAttachmentMap(); + for (int i = 0; i < 2; i++) + { + mLayers[i] = new osg::FrameBufferObject; + // Intentionally not using ref& so attachment can be non-const + for (auto [component, attachment] : attachments) + { + osg::Texture2DArray* texture = static_cast(attachment.getTexture()); + mLayers[i]->setAttachment(component, osg::FrameBufferAttachment(texture, i)); + mWidth = texture->getTextureWidth(); + mHeight = texture->getTextureHeight(); + } + } + } + + void setupMsaaLayers() + { + const auto& attachments = mMsaaFbo->getAttachmentMap(); + for (int i = 0; i < 2; i++) + { + mMsaaLayers[i] = new osg::FrameBufferObject; + // Intentionally not using ref& so attachment can be non-const + for (auto [component, attachment] : attachments) + { + osg::Texture2DMultisampleArray* texture = static_cast(attachment.getTexture()); + mMsaaLayers[i]->setAttachment(component, osg::FrameBufferAttachment(texture, i)); + mWidth = texture->getTextureWidth(); + mHeight = texture->getTextureHeight(); + } + } + } + + osg::ref_ptr mFbo; + osg::ref_ptr mMsaaFbo; + osg::ref_ptr mLayers[2]; + osg::ref_ptr mMsaaLayers[2]; + int mWidth; + int mHeight; + }; +#endif + + void setMultiviewMSAAResolveCallback(osgUtil::RenderStage* renderStage) + { +#ifdef OSG_HAS_MULTIVIEW + if (Stereo::getMultiview()) + { + renderStage->setDrawCallback(new MultiviewMSAAResolveCallback); + } +#endif + } + + void setMultiviewMatrices(osg::StateSet* stateset, const std::array& projection, bool createInverseMatrices) + { + auto* projUniform = stateset->getUniform("projectionMatrixMultiView"); + if (!projUniform) + { + projUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "projectionMatrixMultiView", 2); + stateset->addUniform(projUniform, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + + projUniform->setElement(0, projection[0]); + projUniform->setElement(1, projection[1]); + + if (createInverseMatrices) + { + auto* invUniform = stateset->getUniform("invProjectionMatrixMultiView"); + if (!invUniform) + { + invUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "invProjectionMatrixMultiView", 2); + stateset->addUniform(invUniform, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + + invUniform->setElement(0, osg::Matrix::inverse(projection[0])); + invUniform->setElement(1, osg::Matrix::inverse(projection[1])); + } + } + + void setMultiviewCompatibleTextureSize(osg::Texture* tex, int w, int h) + { + switch (tex->getTextureTarget()) + { + case GL_TEXTURE_2D: + static_cast(tex)->setTextureSize(w, h); + break; + case GL_TEXTURE_2D_ARRAY: + static_cast(tex)->setTextureSize(w, h, 2); + break; + case GL_TEXTURE_2D_MULTISAMPLE: + static_cast(tex)->setTextureSize(w, h); + break; +#ifdef OSG_HAS_MULTIVIEW + case GL_TEXTURE_2D_MULTISAMPLE_ARRAY: + static_cast(tex)->setTextureSize(w, h, 2); + break; +#endif + default: + throw std::logic_error("Invalid texture type received"); + } + } + + osg::ref_ptr createMultiviewCompatibleTexture(int width, int height, int samples) + { +#ifdef OSG_HAS_MULTIVIEW + if (Stereo::getMultiview()) + { + if (samples > 1) + { + auto tex = new osg::Texture2DMultisampleArray(); + tex->setTextureSize(width, height, 2); + tex->setNumSamples(samples); + return tex; + } + else + { + auto tex = new osg::Texture2DArray(); + tex->setTextureSize(width, height, 2); + return tex; + } + } + else +#endif + { + if (samples > 1) + { + auto tex = new osg::Texture2DMultisample(); + tex->setTextureSize(width, height); + tex->setNumSamples(samples); + return tex; + } + else + { + auto tex = new osg::Texture2D(); + tex->setTextureSize(width, height); + return tex; + } + } + } + + osg::FrameBufferAttachment createMultiviewCompatibleAttachment(osg::Texture* tex) + { + switch (tex->getTextureTarget()) + { + case GL_TEXTURE_2D: + { + auto* tex2d = static_cast(tex); + return osg::FrameBufferAttachment(tex2d); + } + case GL_TEXTURE_2D_MULTISAMPLE: + { + auto* tex2dMsaa = static_cast(tex); + return osg::FrameBufferAttachment(tex2dMsaa); + } +#ifdef OSG_HAS_MULTIVIEW + case GL_TEXTURE_2D_ARRAY: + { + auto* tex2dArray = static_cast(tex); + return osg::FrameBufferAttachment(tex2dArray, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, 0); + } + case GL_TEXTURE_2D_MULTISAMPLE_ARRAY: + { + auto* tex2dMsaaArray = static_cast(tex); + return osg::FrameBufferAttachment(tex2dMsaaArray, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, 0); + } +#endif + default: + throw std::logic_error("Invalid texture type received"); + } + } + + unsigned int osgFaceControlledByMultiviewShader() + { +#ifdef OSG_HAS_MULTIVIEW + return osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER; +#else + return 0; +#endif + } + + class UpdateRenderStagesCallback : public SceneUtil::NodeCallback + { + public: + UpdateRenderStagesCallback(Stereo::MultiviewFramebuffer* multiviewFramebuffer) + : mMultiviewFramebuffer(multiviewFramebuffer) + { + mViewport = new osg::Viewport(0, 0, multiviewFramebuffer->width(), multiviewFramebuffer->height()); + mViewportStateset = new osg::StateSet(); + mViewportStateset->setAttribute(mViewport.get()); + } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); + + bool msaa = mMultiviewFramebuffer->samples() > 1; + + if (!Stereo::getMultiview()) + { + auto eye = static_cast(Stereo::Manager::instance().getEye(cv)); + + if (msaa) + { + renderStage->setFrameBufferObject(mMultiviewFramebuffer->layerMsaaFbo(eye)); + renderStage->setMultisampleResolveFramebufferObject(mMultiviewFramebuffer->layerFbo(eye)); + } + else + { + renderStage->setFrameBufferObject(mMultiviewFramebuffer->layerFbo(eye)); + } + } + + // OSG tries to do a horizontal split, but we want to render to separate framebuffers instead. + renderStage->setViewport(mViewport); + cv->pushStateSet(mViewportStateset.get()); + traverse(node, cv); + cv->popStateSet(); + } + + private: + Stereo::MultiviewFramebuffer* mMultiviewFramebuffer; + osg::ref_ptr mViewport; + osg::ref_ptr mViewportStateset; + }; + + MultiviewFramebuffer::MultiviewFramebuffer(int width, int height, int samples) + : mWidth(width) + , mHeight(height) + , mSamples(samples) + , mMultiview(getMultiview()) + , mMultiviewFbo{ new osg::FrameBufferObject } + , mLayerFbo{ new osg::FrameBufferObject, new osg::FrameBufferObject } + , mLayerMsaaFbo{ new osg::FrameBufferObject, new osg::FrameBufferObject } + { + } + + MultiviewFramebuffer::~MultiviewFramebuffer() + { + } + + void MultiviewFramebuffer::attachColorComponent(GLint sourceFormat, GLint sourceType, GLint internalFormat) + { + if (mMultiview) + { +#ifdef OSG_HAS_MULTIVIEW + mMultiviewColorTexture = createTextureArray(sourceFormat, sourceType, internalFormat); + mMultiviewFbo->setAttachment(osg::Camera::COLOR_BUFFER, osg::FrameBufferAttachment(mMultiviewColorTexture, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, 0)); + for (unsigned i = 0; i < 2; i++) + { + mColorTexture[i] = createTextureView_Texture2DFromTexture2DArray(mMultiviewColorTexture.get(), i); + mLayerFbo[i]->setAttachment(osg::Camera::COLOR_BUFFER, osg::FrameBufferAttachment(mColorTexture[i])); + } +#endif + } + else + { + for (unsigned i = 0; i < 2; i++) + { + if (mSamples > 1) + mLayerMsaaFbo[i]->setAttachment(osg::Camera::COLOR_BUFFER, osg::FrameBufferAttachment(new osg::RenderBuffer(mWidth, mHeight, internalFormat, mSamples))); + mColorTexture[i] = createTexture(sourceFormat, sourceType, internalFormat); + mLayerFbo[i]->setAttachment(osg::Camera::COLOR_BUFFER, osg::FrameBufferAttachment(mColorTexture[i])); + } + } + } + + void MultiviewFramebuffer::attachDepthComponent(GLint sourceFormat, GLint sourceType, GLint internalFormat) + { + if (mMultiview) + { +#ifdef OSG_HAS_MULTIVIEW + mMultiviewDepthTexture = createTextureArray(sourceFormat, sourceType, internalFormat); + mMultiviewFbo->setAttachment(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, osg::FrameBufferAttachment(mMultiviewDepthTexture, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, 0)); + for (unsigned i = 0; i < 2; i++) + { + mDepthTexture[i] = createTextureView_Texture2DFromTexture2DArray(mMultiviewDepthTexture.get(), i); + mLayerFbo[i]->setAttachment(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, osg::FrameBufferAttachment(mDepthTexture[i])); + } +#endif + } + else + { + for (unsigned i = 0; i < 2; i++) + { + if (mSamples > 1) + mLayerMsaaFbo[i]->setAttachment(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, osg::FrameBufferAttachment(new osg::RenderBuffer(mWidth, mHeight, internalFormat, mSamples))); + mDepthTexture[i] = createTexture(sourceFormat, sourceType, internalFormat); + mLayerFbo[i]->setAttachment(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, osg::FrameBufferAttachment(mDepthTexture[i])); + } + } + } + + osg::FrameBufferObject* MultiviewFramebuffer::multiviewFbo() + { + return mMultiviewFbo; + } + + osg::FrameBufferObject* MultiviewFramebuffer::layerFbo(int i) + { + return mLayerFbo[i]; + } + + osg::FrameBufferObject* MultiviewFramebuffer::layerMsaaFbo(int i) + { + return mLayerMsaaFbo[i]; + } + + osg::Texture2DArray* MultiviewFramebuffer::multiviewColorBuffer() + { + return mMultiviewColorTexture; + } + + osg::Texture2DArray* MultiviewFramebuffer::multiviewDepthBuffer() + { + return mMultiviewDepthTexture; + } + + osg::Texture2D* MultiviewFramebuffer::layerColorBuffer(int i) + { + return mColorTexture[i]; + } + + osg::Texture2D* MultiviewFramebuffer::layerDepthBuffer(int i) + { + return mDepthTexture[i]; + } + void MultiviewFramebuffer::attachTo(osg::Camera* camera) + { +#ifdef OSG_HAS_MULTIVIEW + if (mMultiview) + { + if (mMultiviewColorTexture) + { + camera->attach(osg::Camera::COLOR_BUFFER, mMultiviewColorTexture, 0, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, false, mSamples); + camera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._internalFormat = mMultiviewColorTexture->getInternalFormat(); + camera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._mipMapGeneration = false; + } + if (mMultiviewDepthTexture) + { + camera->attach(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, mMultiviewDepthTexture, 0, osg::Camera::FACE_CONTROLLED_BY_MULTIVIEW_SHADER, false, mSamples); + camera->getBufferAttachmentMap()[osg::Camera::PACKED_DEPTH_STENCIL_BUFFER]._internalFormat = mMultiviewDepthTexture->getInternalFormat(); + camera->getBufferAttachmentMap()[osg::Camera::PACKED_DEPTH_STENCIL_BUFFER]._mipMapGeneration = false; + } + } +#endif + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + + if (!mCullCallback) + mCullCallback = new UpdateRenderStagesCallback(this); + camera->addCullCallback(mCullCallback); + } + + void MultiviewFramebuffer::detachFrom(osg::Camera* camera) + { +#ifdef OSG_HAS_MULTIVIEW + if (mMultiview) + { + if (mMultiviewColorTexture) + { + camera->detach(osg::Camera::COLOR_BUFFER); + } + if (mMultiviewDepthTexture) + { + camera->detach(osg::Camera::PACKED_DEPTH_STENCIL_BUFFER); + } + } +#endif + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER); + if (mCullCallback) + camera->removeCullCallback(mCullCallback); + } + + osg::Texture2D* MultiviewFramebuffer::createTexture(GLint sourceFormat, GLint sourceType, GLint internalFormat) + { + osg::Texture2D* texture = new osg::Texture2D; + texture->setTextureSize(mWidth, mHeight); + texture->setSourceFormat(sourceFormat); + texture->setSourceType(sourceType); + texture->setInternalFormat(internalFormat); + 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); + texture->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + return texture; + } + + osg::Texture2DArray* MultiviewFramebuffer::createTextureArray(GLint sourceFormat, GLint sourceType, GLint internalFormat) + { + osg::Texture2DArray* textureArray = new osg::Texture2DArray; + textureArray->setTextureSize(mWidth, mHeight, 2); + textureArray->setSourceFormat(sourceFormat); + textureArray->setSourceType(sourceType); + textureArray->setInternalFormat(internalFormat); + textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + return textureArray; + } + + + osg::FrameBufferAttachment makeSingleLayerAttachmentFromMultilayerAttachment(osg::FrameBufferAttachment attachment, int layer) + { + osg::Texture* tex = attachment.getTexture(); + + if (tex->getTextureTarget() == GL_TEXTURE_2D_ARRAY) + return osg::FrameBufferAttachment(static_cast(tex), layer, 0); + +#ifdef OSG_HAS_MULTIVIEW + if (tex->getTextureTarget() == GL_TEXTURE_2D_MULTISAMPLE_ARRAY) + return osg::FrameBufferAttachment(static_cast(tex), layer, 0); +#endif + + Log(Debug::Error) << "Attempted to extract a layer from an unlayered texture"; + + return osg::FrameBufferAttachment(); + } + + MultiviewFramebufferResolve::MultiviewFramebufferResolve(osg::FrameBufferObject* msaaFbo, osg::FrameBufferObject* resolveFbo, GLbitfield blitMask) + : mResolveFbo(resolveFbo) + , mMsaaFbo(msaaFbo) + , mBlitMask(blitMask) + { + } + + void MultiviewFramebufferResolve::resolveImplementation(osg::State& state) + { + if (mDirtyLayers) + setupLayers(); + + osg::GLExtensions* ext = state.get(); + + for (int view : {0, 1}) + { + mResolveLayers[view]->apply(state, osg::FrameBufferObject::BindTarget::DRAW_FRAMEBUFFER); + mMsaaLayers[view]->apply(state, osg::FrameBufferObject::BindTarget::READ_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, mWidth, mHeight, 0, 0, mWidth, mHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + } + } + void MultiviewFramebufferResolve::setupLayers() + { + mDirtyLayers = false; + std::vector components; + if (mBlitMask & GL_DEPTH_BUFFER_BIT) + components.push_back(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER); + if (mBlitMask & GL_COLOR_BUFFER_BIT) + components.push_back(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER); + + mMsaaLayers = { new osg::FrameBufferObject, new osg::FrameBufferObject }; + mResolveLayers = { new osg::FrameBufferObject, new osg::FrameBufferObject }; + for (auto component : components) + { + const auto& msaaAttachment = mMsaaFbo->getAttachment(component); + mMsaaLayers[0]->setAttachment(component, makeSingleLayerAttachmentFromMultilayerAttachment(msaaAttachment, 0)); + mMsaaLayers[1]->setAttachment(component, makeSingleLayerAttachmentFromMultilayerAttachment(msaaAttachment, 1)); + + const auto& resolveAttachment = mResolveFbo->getAttachment(component); + mResolveLayers[0]->setAttachment(component, makeSingleLayerAttachmentFromMultilayerAttachment(resolveAttachment, 0)); + mResolveLayers[1]->setAttachment(component, makeSingleLayerAttachmentFromMultilayerAttachment(resolveAttachment, 1)); + + mWidth = msaaAttachment.getTexture()->getTextureWidth(); + mHeight = msaaAttachment.getTexture()->getTextureHeight(); + } + } + +#ifdef OSG_HAS_MULTIVIEW + namespace + { + struct MultiviewFrustumCallback final : public osg::CullSettings::InitialFrustumCallback + { + MultiviewFrustumCallback(Stereo::InitialFrustumCallback* ifc) + : mIfc(ifc) + { + + } + + void setInitialFrustum(osg::CullStack& cullStack, osg::Polytope& frustum) const override + { + bool nearCulling = false; + bool farCulling = false; + osg::BoundingBoxd bb; + mIfc->setInitialFrustum(cullStack, bb, nearCulling, farCulling); + frustum.setToBoundingBox(bb, nearCulling, farCulling); + } + + Stereo::InitialFrustumCallback* mIfc; + }; + } +#endif + + InitialFrustumCallback::InitialFrustumCallback(osg::Camera* camera) + : mCamera(camera) + { +#ifdef OSG_HAS_MULTIVIEW + camera->setInitialFrustumCallback(new MultiviewFrustumCallback(this)); +#endif + } + + InitialFrustumCallback::~InitialFrustumCallback() + { +#ifdef OSG_HAS_MULTIVIEW + osg::ref_ptr camera; + if(mCamera.lock(camera)) + camera->setInitialFrustumCallback(nullptr); +#endif + } +} diff --git a/components/stereo/multiview.hpp b/components/stereo/multiview.hpp new file mode 100644 index 0000000000..418c69e159 --- /dev/null +++ b/components/stereo/multiview.hpp @@ -0,0 +1,144 @@ +#ifndef STEREO_MULTIVIEW_H +#define STEREO_MULTIVIEW_H + +#include +#include +#include +#include + +#include +#include + +namespace osg +{ + class FrameBufferObject; + class Texture; + class Texture2D; + class Texture2DArray; +} + +namespace osgUtil +{ + class RenderStage; +} + +namespace Stereo +{ + class UpdateRenderStagesCallback; + + //! Check if TextureView is supported. Results are undefined if called before configureExtensions(). + bool getTextureViewSupported(); + + //! Check if Multiview should be used. Results are undefined if called before configureExtensions(). + bool getMultiview(); + + //! Use the provided context to check what extensions are supported and configure use of multiview based on extensions and settings. + void configureExtensions(unsigned int contextID); + + //! Sets the appropriate vertex buffer hint on OSG's display settings if needed + void setVertexBufferHint(); + + //! Creates a Texture2D as a texture view into a Texture2DArray + osg::ref_ptr createTextureView_Texture2DFromTexture2DArray(osg::Texture2DArray* textureArray, int layer); + + //! Class that manages the specifics of GL_OVR_Multiview aware framebuffers, separating the layers into separate framebuffers, and disabling + class MultiviewFramebuffer + { + public: + MultiviewFramebuffer(int width, int height, int samples); + ~MultiviewFramebuffer(); + + void attachColorComponent(GLint sourceFormat, GLint sourceType, GLint internalFormat); + void attachDepthComponent(GLint sourceFormat, GLint sourceType, GLint internalFormat); + + osg::FrameBufferObject* multiviewFbo(); + osg::FrameBufferObject* layerFbo(int i); + osg::FrameBufferObject* layerMsaaFbo(int i); + osg::Texture2DArray* multiviewColorBuffer(); + osg::Texture2DArray* multiviewDepthBuffer(); + osg::Texture2D* layerColorBuffer(int i); + osg::Texture2D* layerDepthBuffer(int i); + + void attachTo(osg::Camera* camera); + void detachFrom(osg::Camera* camera); + + int width() const { return mWidth; } + int height() const { return mHeight; } + int samples() const { return mSamples; }; + + private: + osg::Texture2D* createTexture(GLint sourceFormat, GLint sourceType, GLint internalFormat); + osg::Texture2DArray* createTextureArray(GLint sourceFormat, GLint sourceType, GLint internalFormat); + + int mWidth; + int mHeight; + int mSamples; + bool mMultiview; + osg::ref_ptr mCullCallback; + osg::ref_ptr mMultiviewFbo; + std::array, 2> mLayerFbo; + std::array, 2> mLayerMsaaFbo; + osg::ref_ptr mMultiviewColorTexture; + osg::ref_ptr mMultiviewDepthTexture; + std::array, 2> mColorTexture; + std::array, 2> mDepthTexture; + }; + + //! Sets up a draw callback on the render stage that performs the MSAA resolve operation + void setMultiviewMSAAResolveCallback(osgUtil::RenderStage* renderStage); + + //! Sets up or updates multiview matrices for the given stateset + void setMultiviewMatrices(osg::StateSet* stateset, const std::array& projection, bool createInverseMatrices = false); + + //! Sets the width/height of a texture by first down-casting it to the appropriate type. Sets depth to 2 always for Texture2DArray and Texture2DMultisampleArray. + void setMultiviewCompatibleTextureSize(osg::Texture* tex, int w, int h); + + //! Creates a texture (Texture2D, Texture2DMultisample, Texture2DArray, or Texture2DMultisampleArray) based on multiview settings and sample count. + osg::ref_ptr createMultiviewCompatibleTexture(int width, int height, int samples); + + //! Returns a framebuffer attachment from the texture, returning a multiview attachment if the texture is one of Texture2DArray or Texture2DMultisampleArray + osg::FrameBufferAttachment createMultiviewCompatibleAttachment(osg::Texture* tex); + + //! If OSG has multiview, returns the magic number used to tell OSG to create a multiview attachment. Otherwise returns 0. + unsigned int osgFaceControlledByMultiviewShader(); + + //! Implements resolving a multisamples multiview framebuffer. Does not automatically reflect changes to the fbo attachments, must call dirty() when the fbo attachments change. + class MultiviewFramebufferResolve + { + public: + MultiviewFramebufferResolve(osg::FrameBufferObject* msaaFbo, osg::FrameBufferObject* resolveFbo, GLbitfield blitMask); + + void resolveImplementation(osg::State& state); + + void dirty() { mDirtyLayers = true; } + + const osg::FrameBufferObject* resolveFbo() const { return mResolveFbo; }; + const osg::FrameBufferObject* msaaFbo() const { return mMsaaFbo; }; + + private: + void setupLayers(); + + osg::ref_ptr mResolveFbo; + std::array, 2> mResolveLayers{}; + osg::ref_ptr mMsaaFbo; + std::array, 2> mMsaaLayers{}; + + GLbitfield mBlitMask; + bool mDirtyLayers = true; + int mWidth = -1; + int mHeight = -1; + }; + + //! Wrapper for osg::CullSettings::InitialFrustumCallback, to avoid exposing osg multiview interfaces outside of multiview.cpp + struct InitialFrustumCallback + { + InitialFrustumCallback(osg::Camera* camera); + virtual ~InitialFrustumCallback(); + + virtual void setInitialFrustum(osg::CullStack& cullStack, osg::BoundingBoxd& bb, bool& nearCulling, bool& farCulling) const = 0; + + osg::observer_ptr mCamera; + }; +} + +#endif diff --git a/components/stereo/stereomanager.cpp b/components/stereo/stereomanager.cpp new file mode 100644 index 0000000000..2c4b8a35e6 --- /dev/null +++ b/components/stereo/stereomanager.cpp @@ -0,0 +1,455 @@ +#include "stereomanager.hpp" +#include "multiview.hpp" +#include "frustum.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +namespace Stereo +{ + // Update stereo view/projection during update + class StereoUpdateCallback final : public osg::Callback + { + public: + StereoUpdateCallback(Manager* stereoView) : stereoView(stereoView) {} + + bool run(osg::Object* object, osg::Object* data) override + { + auto b = traverse(object, data); + stereoView->update(); + return b; + } + + Manager* stereoView; + }; + + // Update states during cull + class BruteForceStereoStatesetUpdateCallback final : public SceneUtil::StateSetUpdater + { + public: + BruteForceStereoStatesetUpdateCallback(Manager* manager) + : mManager(manager) + { + } + + protected: + virtual void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("projectionMatrix", osg::Matrixf{})); + } + + virtual void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override + { + } + + void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(mManager->computeEyeViewOffset(0) * mManager->computeEyeProjection(0, SceneUtil::AutoDepth::isReversed())); + } + + void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(mManager->computeEyeViewOffset(1) * mManager->computeEyeProjection(1, SceneUtil::AutoDepth::isReversed())); + } + + private: + Manager* mManager; + }; + + // Update states during cull + class MultiviewStereoStatesetUpdateCallback : public SceneUtil::StateSetUpdater + { + public: + MultiviewStereoStatesetUpdateCallback(Manager* manager) + : mManager(manager) + { + } + + protected: + virtual void setDefaults(osg::StateSet* stateset) + { + stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "invProjectionMatrixMultiView", 2)); + } + + virtual void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + mManager->updateMultiviewStateset(stateset); + } + + private: + Manager* mManager; + }; + + static Manager* sInstance = nullptr; + + Manager& Manager::instance() + { + return *sInstance; + } + + struct CustomViewCallback : public Manager::UpdateViewCallback + { + public: + CustomViewCallback(); + + void updateView(View& left, View& right) override; + + private: + View mLeft; + View mRight; + }; + + Manager::Manager(osgViewer::Viewer* viewer) + : mViewer(viewer) + , mMainCamera(mViewer->getCamera()) + , mUpdateCallback(new StereoUpdateCallback(this)) + , mMasterProjectionMatrix(osg::Matrixd::identity()) + , mEyeResolutionOverriden(false) + , mEyeResolutionOverride(0,0) + , mFrustumManager(nullptr) + , mUpdateViewCallback(nullptr) + { + if (sInstance) + throw std::logic_error("Double instance of Stereo::Manager"); + sInstance = this; + + if (Settings::Manager::getBool("use custom view", "Stereo")) + mUpdateViewCallback = std::make_shared(); + + if (Settings::Manager::getBool("use custom eye resolution", "Stereo")) + { + osg::Vec2i eyeResolution = osg::Vec2i(); + eyeResolution.x() = Settings::Manager::getInt("eye resolution x", "Stereo View"); + eyeResolution.y() = Settings::Manager::getInt("eye resolution y", "Stereo View"); + overrideEyeResolution(eyeResolution); + } + } + + Manager::~Manager() + { + } + + void Manager::initializeStereo(osg::GraphicsContext* gc) + { + mMainCamera->addUpdateCallback(mUpdateCallback); + mFrustumManager = std::make_unique(mViewer->getCamera()); + + auto ci = gc->getState()->getContextID(); + configureExtensions(ci); + + if(getMultiview()) + setupOVRMultiView2Technique(); + else + setupBruteForceTechnique(); + + updateStereoFramebuffer(); + + } + + void Manager::shaderStereoDefines(Shader::ShaderManager::DefineMap& defines) const + { + if (getMultiview()) + { + defines["useOVR_multiview"] = "1"; + defines["numViews"] = "2"; + } + else + { + defines["useOVR_multiview"] = "0"; + defines["numViews"] = "1"; + } + } + + void Manager::overrideEyeResolution(const osg::Vec2i& eyeResolution) + { + mEyeResolutionOverride = eyeResolution; + mEyeResolutionOverriden = true; + + //if (mMultiviewFramebuffer) + // updateStereoFramebuffer(); + } + + void Manager::screenResolutionChanged() + { + updateStereoFramebuffer(); + } + + osg::Vec2i Manager::eyeResolution() + { + if (mEyeResolutionOverriden) + return mEyeResolutionOverride; + auto width = mMainCamera->getViewport()->width() / 2; + auto height = mMainCamera->getViewport()->height(); + + return osg::Vec2i(width, height); + } + + void Manager::disableStereoForNode(osg::Node* node) + { + // Re-apply the main camera's full viewport to return to full screen rendering. + node->getOrCreateStateSet()->setAttribute(mMainCamera->getViewport()); + } + + void Manager::setShadowTechnique(SceneUtil::MWShadowTechnique* shadowTechnique) + { + if (mFrustumManager) + mFrustumManager->setShadowTechnique(shadowTechnique); + } + + void Manager::setupBruteForceTechnique() + { + auto* ds = osg::DisplaySettings::instance().get(); + ds->setStereo(true); + ds->setStereoMode(osg::DisplaySettings::StereoMode::HORIZONTAL_SPLIT); + ds->setUseSceneViewForStereoHint(true); + + mMainCamera->addCullCallback(new BruteForceStereoStatesetUpdateCallback(this)); + + struct ComputeStereoMatricesCallback : public osgUtil::SceneView::ComputeStereoMatricesCallback + { + ComputeStereoMatricesCallback(Manager* sv) + : mManager(sv) + { + + } + + osg::Matrixd computeLeftEyeProjection(const osg::Matrixd& projection) const override + { + (void)projection; + return mManager->computeEyeViewOffset(0) * mManager->computeEyeProjection(0, false); + } + + osg::Matrixd computeLeftEyeView(const osg::Matrixd& view) const override + { + return view; + } + + osg::Matrixd computeRightEyeProjection(const osg::Matrixd& projection) const override + { + (void)projection; + return mManager->computeEyeViewOffset(1) * mManager->computeEyeProjection(1, false); + } + + osg::Matrixd computeRightEyeView(const osg::Matrixd& view) const override + { + return view; + } + + Manager* mManager; + }; + + auto* renderer = static_cast(mMainCamera->getRenderer()); + for (auto* sceneView : { renderer->getSceneView(0), renderer->getSceneView(1) }) + { + sceneView->setComputeStereoMatricesCallback(new ComputeStereoMatricesCallback(this)); + + auto* cvMain = sceneView->getCullVisitor(); + auto* cvLeft = sceneView->getCullVisitorLeft(); + auto* cvRight = sceneView->getCullVisitorRight(); + if (!cvMain) + sceneView->setCullVisitor(cvMain = new osgUtil::CullVisitor()); + if (!cvLeft) + sceneView->setCullVisitor(cvLeft = cvMain->clone()); + if (!cvRight) + sceneView->setCullVisitor(cvRight = cvMain->clone()); + + // Osg by default gives cullVisitorLeft and cullVisitor the same identifier. + // So we make our own to avoid confusion + cvMain->setIdentifier(mIdentifierMain); + cvLeft->setIdentifier(mIdentifierLeft); + cvRight->setIdentifier(mIdentifierRight); + } + } + + void Manager::setupOVRMultiView2Technique() + { + auto* ds = osg::DisplaySettings::instance().get(); + ds->setStereo(false); + + mMainCamera->addCullCallback(new MultiviewStereoStatesetUpdateCallback(this)); + } + + void Manager::updateStereoFramebuffer() + { + //VR-TODO: in VR, still need to have this framebuffer attached before the postprocessor is created + //auto samples = Settings::Manager::getInt("antialiasing", "Video"); + //auto eyeRes = eyeResolution(); + + //if (mMultiviewFramebuffer) + // mMultiviewFramebuffer->detachFrom(mMainCamera); + //mMultiviewFramebuffer = std::make_shared(static_cast(eyeRes.x()), static_cast(eyeRes.y()), samples); + //mMultiviewFramebuffer->attachColorComponent(SceneUtil::Color::colorSourceFormat(), SceneUtil::Color::colorSourceType(), SceneUtil::Color::colorInternalFormat()); + //mMultiviewFramebuffer->attachDepthComponent(SceneUtil::AutoDepth::depthSourceFormat(), SceneUtil::AutoDepth::depthSourceType(), SceneUtil::AutoDepth::depthInternalFormat()); + //mMultiviewFramebuffer->attachTo(mMainCamera); + } + + void Manager::update() + { + double near_ = 1.f; + double far_ = 10000.f; + + near_ = Settings::Manager::getFloat("near clip", "Camera"); + far_ = Settings::Manager::getFloat("viewing distance", "Camera"); + + if (mUpdateViewCallback) + { + mUpdateViewCallback->updateView(mView[0], mView[1]); + mViewOffsetMatrix[0] = mView[0].viewMatrix(true); + mViewOffsetMatrix[1] = mView[1].viewMatrix(true); + mProjectionMatrix[0] = mView[0].perspectiveMatrix(near_, far_, false); + mProjectionMatrix[1] = mView[1].perspectiveMatrix(near_, far_, false); + if (SceneUtil::AutoDepth::isReversed()) + { + mProjectionMatrixReverseZ[0] = mView[0].perspectiveMatrix(near_, far_, true); + mProjectionMatrixReverseZ[1] = mView[1].perspectiveMatrix(near_, far_, true); + } + + View masterView; + masterView.fov.angleDown = std::min(mView[0].fov.angleDown, mView[1].fov.angleDown); + masterView.fov.angleUp = std::max(mView[0].fov.angleUp, mView[1].fov.angleUp); + masterView.fov.angleLeft = std::min(mView[0].fov.angleLeft, mView[1].fov.angleLeft); + masterView.fov.angleRight = std::max(mView[0].fov.angleRight, mView[1].fov.angleRight); + auto projectionMatrix = masterView.perspectiveMatrix(near_, far_, false); + mMainCamera->setProjectionMatrix(projectionMatrix); + } + else + { + auto* ds = osg::DisplaySettings::instance().get(); + auto viewMatrix = mMainCamera->getViewMatrix(); + auto projectionMatrix = mMainCamera->getProjectionMatrix(); + auto s = ds->getEyeSeparation() * Constants::UnitsPerMeter; + mViewOffsetMatrix[0] = osg::Matrixd( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + s, 0.0, 0.0, 1.0); + mViewOffsetMatrix[1] = osg::Matrixd( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + -s, 0.0, 0.0, 1.0); + mProjectionMatrix[0] = ds->computeLeftEyeProjectionImplementation(projectionMatrix); + mProjectionMatrix[1] = ds->computeRightEyeProjectionImplementation(projectionMatrix); + if (SceneUtil::AutoDepth::isReversed()) + { + mProjectionMatrixReverseZ[0] = ds->computeLeftEyeProjectionImplementation(mMasterProjectionMatrix); + mProjectionMatrixReverseZ[1] = ds->computeRightEyeProjectionImplementation(mMasterProjectionMatrix); + } + } + + mFrustumManager->update( + { + mViewOffsetMatrix[0] * mProjectionMatrix[0], + mViewOffsetMatrix[1] * mProjectionMatrix[1] + }); + } + + void Manager::updateMultiviewStateset(osg::StateSet* stateset) + { + std::array projectionMatrices; + + for (int view : {0, 1}) + projectionMatrices[view] = computeEyeViewOffset(view) * computeEyeProjection(view, SceneUtil::AutoDepth::isReversed()); + + Stereo::setMultiviewMatrices(stateset, projectionMatrices, true); + } + + void Manager::setUpdateViewCallback(std::shared_ptr cb) + { + mUpdateViewCallback = cb; + } + + void Manager::setCullCallback(osg::ref_ptr cb) + { + mMainCamera->setCullCallback(cb); + } + + osg::Matrixd Manager::computeEyeProjection(int view, bool reverseZ) const + { + return reverseZ ? mProjectionMatrixReverseZ[view] : mProjectionMatrix[view]; + } + + osg::Matrixd Manager::computeEyeViewOffset(int view) const + { + return mViewOffsetMatrix[view]; + } + + Eye Manager::getEye(const osgUtil::CullVisitor* cv) const + { + if (cv->getIdentifier() == mIdentifierMain) + return Eye::Center; + if (cv->getIdentifier() == mIdentifierLeft) + return Eye::Left; + if (cv->getIdentifier() == mIdentifierRight) + return Eye::Right; + return Eye::Center; + } + + bool getStereo() + { + static bool stereo = Settings::Manager::getBool("stereo enabled", "Stereo") || osg::DisplaySettings::instance().get()->getStereo(); + return stereo; + } + + CustomViewCallback::CustomViewCallback() + { + mLeft.pose.position.x() = Settings::Manager::getDouble("left eye offset x", "Stereo View"); + mLeft.pose.position.y() = Settings::Manager::getDouble("left eye offset y", "Stereo View"); + mLeft.pose.position.z() = Settings::Manager::getDouble("left eye offset z", "Stereo View"); + mLeft.pose.orientation.x() = Settings::Manager::getDouble("left eye orientation x", "Stereo View"); + mLeft.pose.orientation.y() = Settings::Manager::getDouble("left eye orientation y", "Stereo View"); + mLeft.pose.orientation.z() = Settings::Manager::getDouble("left eye orientation z", "Stereo View"); + mLeft.pose.orientation.w() = Settings::Manager::getDouble("left eye orientation w", "Stereo View"); + mLeft.fov.angleLeft = Settings::Manager::getDouble("left eye fov left", "Stereo View"); + mLeft.fov.angleRight = Settings::Manager::getDouble("left eye fov right", "Stereo View"); + mLeft.fov.angleUp = Settings::Manager::getDouble("left eye fov up", "Stereo View"); + mLeft.fov.angleDown = Settings::Manager::getDouble("left eye fov down", "Stereo View"); + + mRight.pose.position.x() = Settings::Manager::getDouble("right eye offset x", "Stereo View"); + mRight.pose.position.y() = Settings::Manager::getDouble("right eye offset y", "Stereo View"); + mRight.pose.position.z() = Settings::Manager::getDouble("right eye offset z", "Stereo View"); + mRight.pose.orientation.x() = Settings::Manager::getDouble("right eye orientation x", "Stereo View"); + mRight.pose.orientation.y() = Settings::Manager::getDouble("right eye orientation y", "Stereo View"); + mRight.pose.orientation.z() = Settings::Manager::getDouble("right eye orientation z", "Stereo View"); + mRight.pose.orientation.w() = Settings::Manager::getDouble("right eye orientation w", "Stereo View"); + mRight.fov.angleLeft = Settings::Manager::getDouble("right eye fov left", "Stereo View"); + mRight.fov.angleRight = Settings::Manager::getDouble("right eye fov right", "Stereo View"); + mRight.fov.angleUp = Settings::Manager::getDouble("right eye fov up", "Stereo View"); + mRight.fov.angleDown = Settings::Manager::getDouble("right eye fov down", "Stereo View"); + } + + void CustomViewCallback::updateView(View& left, View& right) + { + left = mLeft; + right = mRight; + } +} diff --git a/components/stereo/stereomanager.hpp b/components/stereo/stereomanager.hpp new file mode 100644 index 0000000000..6a873e00f3 --- /dev/null +++ b/components/stereo/stereomanager.hpp @@ -0,0 +1,132 @@ +#ifndef STEREO_MANAGER_H +#define STEREO_MANAGER_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace osg +{ + class FrameBufferObject; + class Texture2D; + class Texture2DArray; +} + +namespace osgViewer +{ + class Viewer; +} + +namespace SceneUtil +{ + class MWShadowTechnique; +} + +namespace Stereo +{ + class MultiviewFramebuffer; + class StereoFrustumManager; + class MultiviewStereoStatesetUpdateCallback; + + bool getStereo(); + + //! Class that provides tools for managing stereo mode + class Manager + { + public: + struct UpdateViewCallback + { + virtual ~UpdateViewCallback() = default; + + //! Called during the update traversal of every frame to update stereo views. + virtual void updateView(View& left, View& right) = 0; + }; + + //! Gets the singleton instance + static Manager& instance(); + + Manager(osgViewer::Viewer* viewer); + ~Manager(); + + //! Called during update traversal + void update(); + + void initializeStereo(osg::GraphicsContext* gc); + + //! Callback that updates stereo configuration during the update pass + void setUpdateViewCallback(std::shared_ptr cb); + + //! Set the cull callback on the appropriate camera object + void setCullCallback(osg::ref_ptr cb); + + osg::Matrixd computeEyeProjection(int view, bool reverseZ) const; + osg::Matrixd computeEyeViewOffset(int view) const; + + //! Sets up any definitions necessary for stereo rendering + void shaderStereoDefines(Shader::ShaderManager::DefineMap& defines) const; + + const std::shared_ptr& multiviewFramebuffer() { return mMultiviewFramebuffer; }; + + //! Sets rendering resolution of each eye to eyeResolution. + //! Once set, there will no longer be any connection between rendering resolution and screen/window resolution. + void overrideEyeResolution(const osg::Vec2i& eyeResolution); + + //! Notify stereo manager that the screen/window resolution has changed. + void screenResolutionChanged(); + + //! Get current eye resolution + osg::Vec2i eyeResolution(); + + //! The projection intended for rendering. When reverse Z is enabled, this is not the same as the camera's projection matrix, + //! and therefore must be provided to the manager explicitly. + void setMasterProjectionMatrix(const osg::Matrixd& projectionMatrix) { mMasterProjectionMatrix = projectionMatrix; } + + //! Causes the subgraph represented by the node to draw to the full viewport. + //! This has no effect if stereo is not enabled + void disableStereoForNode(osg::Node* node); + + void setShadowTechnique(SceneUtil::MWShadowTechnique* shadowTechnique); + + /// Determine which view the cull visitor belongs to + Eye getEye(const osgUtil::CullVisitor* cv) const; + + private: + friend class MultiviewStereoStatesetUpdateCallback; + void updateMultiviewStateset(osg::StateSet* stateset); + void updateStereoFramebuffer(); + void setupBruteForceTechnique(); + void setupOVRMultiView2Technique(); + + osg::ref_ptr mViewer; + osg::ref_ptr mMainCamera; + osg::ref_ptr mUpdateCallback; + std::string mError; + osg::Matrixd mMasterProjectionMatrix; + std::shared_ptr mMultiviewFramebuffer; + bool mEyeResolutionOverriden; + osg::Vec2i mEyeResolutionOverride; + + std::array mView; + std::array mViewOffsetMatrix; + std::array mProjectionMatrix; + std::array mProjectionMatrixReverseZ; + + std::unique_ptr mFrustumManager; + std::shared_ptr mUpdateViewCallback; + + using Identifier = osgUtil::CullVisitor::Identifier; + osg::ref_ptr mIdentifierMain = new Identifier(); + osg::ref_ptr mIdentifierLeft = new Identifier(); + osg::ref_ptr mIdentifierRight = new Identifier(); + }; +} + +#endif diff --git a/components/stereo/types.cpp b/components/stereo/types.cpp new file mode 100644 index 0000000000..4829e32b55 --- /dev/null +++ b/components/stereo/types.cpp @@ -0,0 +1,168 @@ +#include "types.hpp" + +#include + +namespace Stereo +{ + + Pose Pose::operator+(const Pose& rhs) + { + Pose pose = *this; + pose.position += this->orientation * rhs.position; + pose.orientation = rhs.orientation * this->orientation; + return pose; + } + + const Pose& Pose::operator+=(const Pose& rhs) + { + *this = *this + rhs; + return *this; + } + + Pose Pose::operator*(float scalar) + { + Pose pose = *this; + pose.position *= scalar; + return pose; + } + + const Pose& Pose::operator*=(float scalar) + { + *this = *this * scalar; + return *this; + } + + Pose Pose::operator/(float scalar) + { + Pose pose = *this; + pose.position /= scalar; + return pose; + } + const Pose& Pose::operator/=(float scalar) + { + *this = *this / scalar; + return *this; + } + + bool Pose::operator==(const Pose& rhs) const + { + return position == rhs.position && orientation == rhs.orientation; + } + + osg::Matrix View::viewMatrix(bool useGLConventions) + { + auto position = pose.position; + auto orientation = pose.orientation; + + if (useGLConventions) + { + // When applied as an offset to an existing view matrix, + // that view matrix will already convert points to a camera space + // with opengl conventions. So we need to convert offsets to opengl + // conventions. + float y = position.y(); + float z = position.z(); + position.y() = z; + position.z() = -y; + + y = orientation.y(); + z = orientation.z(); + orientation.y() = z; + orientation.z() = -y; + + osg::Matrix viewMatrix; + viewMatrix.setTrans(-position); + viewMatrix.postMultRotate(orientation.conj()); + return viewMatrix; + } + else + { + osg::Vec3d forward = orientation * osg::Vec3d(0, 1, 0); + osg::Vec3d up = orientation * osg::Vec3d(0, 0, 1); + osg::Matrix viewMatrix; + viewMatrix.makeLookAt(position, position + forward, up); + + return viewMatrix; + } + } + + osg::Matrix View::perspectiveMatrix(float near, float far, bool reverseZ) + { + const float tanLeft = tanf(fov.angleLeft); + const float tanRight = tanf(fov.angleRight); + const float tanDown = tanf(fov.angleDown); + const float tanUp = tanf(fov.angleUp); + + const float tanWidth = tanRight - tanLeft; + const float tanHeight = tanUp - tanDown; + + float matrix[16] = {}; + + matrix[0] = 2 / tanWidth; + matrix[4] = 0; + matrix[8] = (tanRight + tanLeft) / tanWidth; + matrix[12] = 0; + + matrix[1] = 0; + matrix[5] = 2 / tanHeight; + matrix[9] = (tanUp + tanDown) / tanHeight; + matrix[13] = 0; + + if (reverseZ) { + matrix[2] = 0; + matrix[6] = 0; + matrix[10] = (2.f * near) / (far - near); + matrix[14] = ((2.f * near) * far) / (far - near); + } + else { + matrix[2] = 0; + matrix[6] = 0; + matrix[10] = -(far + near) / (far - near); + matrix[14] = -(far * (2.f * near)) / (far - near); + } + + matrix[3] = 0; + matrix[7] = 0; + matrix[11] = -1; + matrix[15] = 0; + + return osg::Matrix(matrix); + } + + bool FieldOfView::operator==(const FieldOfView& rhs) const + { + return angleDown == rhs.angleDown + && angleUp == rhs.angleUp + && angleLeft == rhs.angleLeft + && angleRight == rhs.angleRight; + } + + bool View::operator==(const View& rhs) const + { + return pose == rhs.pose && fov == rhs.fov; + } + + std::ostream& operator <<( + std::ostream& os, + const Pose& pose) + { + os << "position=" << pose.position << ", orientation=" << pose.orientation; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const FieldOfView& fov) + { + os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const View& view) + { + os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >"; + return os; + } +} diff --git a/components/stereo/types.hpp b/components/stereo/types.hpp new file mode 100644 index 0000000000..9f2aa074d5 --- /dev/null +++ b/components/stereo/types.hpp @@ -0,0 +1,61 @@ +#ifndef STEREO_TYPES_H +#define STEREO_TYPES_H + +#include +#include + + +namespace Stereo +{ + enum class Eye + { + Left = 0, + Right = 1, + Center = 2 + }; + + struct Pose + { + //! Position in space + osg::Vec3 position{ 0,0,0 }; + //! Orientation in space. + osg::Quat orientation{ 0,0,0,1 }; + + //! Add one pose to another + Pose operator+(const Pose& rhs); + const Pose& operator+=(const Pose& rhs); + + //! Scale a pose (does not affect orientation) + Pose operator*(float scalar); + const Pose& operator*=(float scalar); + Pose operator/(float scalar); + const Pose& operator/=(float scalar); + + bool operator==(const Pose& rhs) const; + }; + + struct FieldOfView { + float angleLeft{ 0.f }; + float angleRight{ 0.f }; + float angleUp{ 0.f }; + float angleDown{ 0.f }; + + bool operator==(const FieldOfView& rhs) const; + }; + + struct View + { + Pose pose; + FieldOfView fov; + bool operator==(const View& rhs) const; + + osg::Matrix viewMatrix(bool useGLConventions); + osg::Matrix perspectiveMatrix(float near, float far, bool reverseZ); + }; + + std::ostream& operator <<(std::ostream& os, const Pose& pose); + std::ostream& operator <<(std::ostream& os, const FieldOfView& fov); + std::ostream& operator <<(std::ostream& os, const View& view); +} + +#endif diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp index 5871b96bc0..658c431b1b 100644 --- a/components/terrain/buffercache.cpp +++ b/components/terrain/buffercache.cpp @@ -12,8 +12,8 @@ namespace template osg::ref_ptr createIndexBuffer(unsigned int flags, unsigned int verts) { - // LOD level n means every 2^n-th vertex is kept - size_t lodLevel = (flags >> (4*4)); + // LOD level n means every 2^n-th vertex is kept, but we currently handle LOD elsewhere. + size_t lodLevel = 0;//(flags >> (4*4)); size_t lodDeltas[4]; for (int i=0; i<4; ++i) @@ -186,7 +186,7 @@ namespace Terrain int vertexCount = numVerts * numVerts; - osg::ref_ptr uvs (new osg::Vec2Array); + osg::ref_ptr uvs (new osg::Vec2Array(osg::Array::BIND_PER_VERTEX)); uvs->reserve(vertexCount); for (unsigned int col = 0; col < numVerts; ++col) @@ -245,13 +245,13 @@ namespace Terrain { { std::lock_guard lock(mIndexBufferMutex); - for (auto indexbuffer : mIndexBufferMap) - indexbuffer.second->releaseGLObjects(state); + for (const auto& [_, indexbuffer] : mIndexBufferMap) + indexbuffer->releaseGLObjects(state); } { std::lock_guard lock(mUvBufferMutex); - for (auto uvbuffer : mUvBufferMap) - uvbuffer.second->releaseGLObjects(state); + for (const auto& [_, uvbuffer] : mUvBufferMap) + uvbuffer->releaseGLObjects(state); } } diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index 47c567f544..fb6593483d 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -3,28 +3,32 @@ #include #include #include -#include #include "world.hpp" -#include "../esm/loadland.hpp" + +#include +#include +#include namespace Terrain { -CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask): - mWorld(world), - mRoot(root), - mBorderMask(borderMask) +CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask, Resource::SceneManager* sceneManager) + : mWorld(world) + , mSceneManager(sceneManager) + , mRoot(root) + , mBorderMask(borderMask) { } -void CellBorder::createCellBorderGeometry(int x, int y) +osg::ref_ptr CellBorder::createBorderGeometry(float x, float y, float size, Terrain::Storage* terrain, Resource::SceneManager* sceneManager, int mask, + float offset, osg::Vec4f color) { const int cellSize = ESM::Land::REAL_SIZE; const int borderSegments = 40; - const float offset = 10.0; osg::Vec3 cellCorner = osg::Vec3(x * cellSize,y * cellSize,0); + size *= cellSize; osg::ref_ptr vertices = new osg::Vec3Array; osg::ref_ptr colors = new osg::Vec4Array; @@ -32,22 +36,22 @@ void CellBorder::createCellBorderGeometry(int x, int y) normals->push_back(osg::Vec3(0.0f,-1.0f, 0.0f)); - float borderStep = cellSize / ((float) borderSegments); + float borderStep = size / ((float)borderSegments); for (int i = 0; i <= 2 * borderSegments; ++i) { osg::Vec3f pos = i < borderSegments ? osg::Vec3(i * borderStep,0.0f,0.0f) : - osg::Vec3(cellSize,(i - borderSegments) * borderStep,0.0f); + osg::Vec3(size, (i - borderSegments) * borderStep,0.0f); pos += cellCorner; - pos += osg::Vec3f(0,0,mWorld->getHeightAt(pos) + offset); + pos += osg::Vec3f(0,0, terrain->getHeightAt(pos) + offset); vertices->push_back(pos); osg::Vec4f col = i % 2 == 0 ? osg::Vec4f(0,0,0,1) : - osg::Vec4f(1,1,0,1); + color; colors->push_back(col); } @@ -61,10 +65,10 @@ void CellBorder::createCellBorderGeometry(int x, int y) border->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP,0,vertices->size())); - osg::ref_ptr borderGeode = new osg::Geode; - borderGeode->addDrawable(border.get()); + osg::ref_ptr borderGroup = new osg::Group; + borderGroup->addChild(border.get()); - osg::StateSet *stateSet = borderGeode->getOrCreateStateSet(); + osg::StateSet *stateSet = borderGroup->getOrCreateStateSet(); osg::ref_ptr material (new osg::Material); material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); stateSet->setAttribute(material); @@ -73,11 +77,18 @@ void CellBorder::createCellBorderGeometry(int x, int y) polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); - borderGeode->setNodeMask(mBorderMask); + sceneManager->recreateShaders(borderGroup, "debug"); + borderGroup->setNodeMask(mask); + + return borderGroup; +} - mRoot->addChild(borderGeode); +void CellBorder::createCellBorderGeometry(int x, int y) +{ + auto borderGroup = createBorderGeometry(x, y, 1.f, mWorld->getStorage(), mSceneManager, mBorderMask); + mRoot->addChild(borderGroup); - mCellBorderNodes[std::make_pair(x,y)] = borderGeode; + mCellBorderNodes[std::make_pair(x,y)] = borderGroup; } void CellBorder::destroyCellBorderGeometry(int x, int y) diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index 908cdea097..4481816a55 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -4,8 +4,14 @@ #include #include +namespace Resource +{ + class SceneManager; +} + namespace Terrain { + class Storage; class World; /** @@ -16,7 +22,7 @@ namespace Terrain public: typedef std::map, osg::ref_ptr > CellGrid; - CellBorder(Terrain::World *world, osg::Group *root, int borderMask); + CellBorder(Terrain::World *world, osg::Group *root, int borderMask, Resource::SceneManager* sceneManager); void createCellBorderGeometry(int x, int y); void destroyCellBorderGeometry(int x, int y); @@ -26,8 +32,11 @@ namespace Terrain */ void destroyCellBorderGeometry(); + static osg::ref_ptr createBorderGeometry(float x, float y, float size, Storage* terrain, Resource::SceneManager* sceneManager, int mask, float offset = 10.0, osg::Vec4f color = { 1,1,0,0 }); + protected: Terrain::World *mWorld; + Resource::SceneManager* mSceneManager; osg::Group *mRoot; CellGrid mCellBorderNodes; diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index 041414a87a..ae4fd339aa 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -1,9 +1,6 @@ #include "chunkmanager.hpp" -#include - #include -#include #include #include @@ -40,15 +37,33 @@ ChunkManager::ChunkManager(Storage *storage, Resource::SceneManager *sceneMgr, T mMultiPassRoot->setAttributeAndModes(material, osg::StateAttribute::ON); } -osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f ¢er, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile) +struct FindChunkTemplate { + void operator() (ChunkId id, osg::Object* obj) + { + if (std::get<0>(id) == std::get<0>(mId) && std::get<1>(id) == std::get<1>(mId)) + mFoundTemplate = obj; + } + ChunkId mId; + osg::ref_ptr mFoundTemplate; +}; + +osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) +{ + // Override lod with the vertexLodMod adjusted value. + // TODO: maybe we can refactor this code by moving all vertexLodMod code into this class. + lod = static_cast(lodFlags >> (4*4)); ChunkId id = std::make_tuple(center, lod, lodFlags); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { - osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile); + FindChunkTemplate find; + find.mId = id; + mCache->call(find); + TerrainDrawable* templateGeometry = find.mFoundTemplate ? static_cast(find.mFoundTemplate.get()) : nullptr; + osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile, templateGeometry); mCache->addEntryToObjectCache(id, node.get()); return node; } @@ -163,27 +178,48 @@ std::vector > ChunkManager::createPasses(float chunk float blendmapScale = mStorage->getBlendmapScale(chunkSize); - return ::Terrain::createPasses(useShaders, &mSceneManager->getShaderManager(), layers, blendmapTextures, blendmapScale, blendmapScale); + return ::Terrain::createPasses(useShaders, mSceneManager, layers, blendmapTextures, blendmapScale, blendmapScale); } -osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile) +osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry) { - osg::ref_ptr positions (new osg::Vec3Array); - osg::ref_ptr normals (new osg::Vec3Array); - osg::ref_ptr colors (new osg::Vec4ubArray); - colors->setNormalize(true); - - osg::ref_ptr vbo (new osg::VertexBufferObject); - positions->setVertexBufferObject(vbo); - normals->setVertexBufferObject(vbo); - colors->setVertexBufferObject(vbo); - - mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); - osg::ref_ptr geometry (new TerrainDrawable); - geometry->setVertexArray(positions); - geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); - geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + + if (!templateGeometry) + { + osg::ref_ptr positions (new osg::Vec3Array); + osg::ref_ptr normals (new osg::Vec3Array); + osg::ref_ptr colors (new osg::Vec4ubArray); + colors->setNormalize(true); + + mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + else + { + // Unfortunately we need to copy vertex data because of poor coupling with VertexBufferObject. + osg::ref_ptr positions = static_cast(templateGeometry->getVertexArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr normals = static_cast(templateGeometry->getNormalArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr colors = static_cast(templateGeometry->getColorArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + geometry->setUseDisplayList(false); geometry->setUseVertexBufferObjects(true); @@ -197,39 +233,50 @@ osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Ve bool useCompositeMap = chunkSize >= mCompositeMapLevel; unsigned int numUvSets = useCompositeMap ? 1 : 2; - for (unsigned int i=0; isetTexCoordArray(i, mBufferCache.getUVBuffer(numVerts)); + geometry->setTexCoordArrayList(osg::Geometry::ArrayList(numUvSets, mBufferCache.getUVBuffer(numVerts))); geometry->createClusterCullingCallback(); geometry->setStateSet(mMultiPassRoot); - if (useCompositeMap) + if (templateGeometry) { - osg::ref_ptr compositeMap = new CompositeMap; - compositeMap->mTexture = createCompositeMapRTT(); + if (templateGeometry->getCompositeMap()) + { + geometry->setCompositeMap(templateGeometry->getCompositeMap()); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); + } + geometry->setPasses(templateGeometry->getPasses()); + } + else + { + if (useCompositeMap) + { + osg::ref_ptr compositeMap = new CompositeMap; + compositeMap->mTexture = createCompositeMapRTT(); - createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); + createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); - mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); + mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); - geometry->setCompositeMap(compositeMap); - geometry->setCompositeMapRenderer(mCompositeMapRenderer); + geometry->setCompositeMap(compositeMap); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); - TextureLayer layer; - layer.mDiffuseMap = compositeMap->mTexture; - layer.mParallax = false; - layer.mSpecular = false; - geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), &mSceneManager->getShaderManager(), std::vector(1, layer), std::vector >(), 1.f, 1.f)); - } - else - { - geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + TextureLayer layer; + layer.mDiffuseMap = compositeMap->mTexture; + layer.mParallax = false; + layer.mSpecular = false; + geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), mSceneManager, std::vector(1, layer), std::vector >(), 1.f, 1.f)); + } + else + { + geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + } } geometry->setupWaterBoundingBox(-1, chunkSize * mStorage->getCellWorldSize() / numVerts); - if (compile && mSceneManager->getIncrementalCompileOperation()) + if (!templateGeometry && compile && mSceneManager->getIncrementalCompileOperation()) { mSceneManager->getIncrementalCompileOperation()->add(geometry); } diff --git a/components/terrain/chunkmanager.hpp b/components/terrain/chunkmanager.hpp index 118df698f1..9b85e81330 100644 --- a/components/terrain/chunkmanager.hpp +++ b/components/terrain/chunkmanager.hpp @@ -26,6 +26,7 @@ namespace Terrain class CompositeMapRenderer; class Storage; class CompositeMap; + class TerrainDrawable; typedef std::tuple ChunkId; // Center, Lod, Lod Flags @@ -35,7 +36,7 @@ namespace Terrain public: ChunkManager(Storage* storage, Resource::SceneManager* sceneMgr, TextureManager* textureManager, CompositeMapRenderer* renderer); - osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile) override; + osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; void setCompositeMapSize(unsigned int size) { mCompositeMapSize = size; } void setCompositeMapLevel(float level) { mCompositeMapLevel = level; } @@ -51,7 +52,7 @@ namespace Terrain void releaseGLObjects(osg::State* state) override; private: - osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile); + osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry); osg::ref_ptr createCompositeMapRTT(); diff --git a/components/terrain/compositemaprenderer.cpp b/components/terrain/compositemaprenderer.cpp index 2a3fa47daa..55da3d347a 100644 --- a/components/terrain/compositemaprenderer.cpp +++ b/components/terrain/compositemaprenderer.cpp @@ -4,9 +4,6 @@ #include #include -#include -#include - #include namespace Terrain @@ -21,8 +18,6 @@ CompositeMapRenderer::CompositeMapRenderer() mFBO = new osg::FrameBufferObject; - mUnrefQueue = new SceneUtil::UnrefQueue; - getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); } @@ -30,11 +25,6 @@ CompositeMapRenderer::~CompositeMapRenderer() { } -void CompositeMapRenderer::setWorkQueue(SceneUtil::WorkQueue* workQueue) -{ - mWorkQueue = workQueue; -} - void CompositeMapRenderer::drawImplementation(osg::RenderInfo &renderInfo) const { double dt = mTimer.time_s(); @@ -45,9 +35,6 @@ void CompositeMapRenderer::drawImplementation(osg::RenderInfo &renderInfo) const double availableTime = std::max((targetFrameTime - dt)*conservativeTimeRatio, mMinimumTimeAvailable); - if (mWorkQueue) - mUnrefQueue->flush(mWorkQueue.get()); - std::lock_guard lock(mMutex); if (mImmediateCompileSet.empty() && mCompileSet.empty()) @@ -139,10 +126,6 @@ void CompositeMapRenderer::compile(CompositeMap &compositeMap, osg::RenderInfo & ++compositeMap.mCompiled; - if (mWorkQueue) - { - mUnrefQueue->push(compositeMap.mDrawables[i]); - } compositeMap.mDrawables[i] = nullptr; if (timeLeft) diff --git a/components/terrain/compositemaprenderer.hpp b/components/terrain/compositemaprenderer.hpp index 257173af46..f0021c88fe 100644 --- a/components/terrain/compositemaprenderer.hpp +++ b/components/terrain/compositemaprenderer.hpp @@ -13,12 +13,6 @@ namespace osg class Texture2D; } -namespace SceneUtil -{ - class UnrefQueue; - class WorkQueue; -} - namespace Terrain { @@ -45,9 +39,6 @@ namespace Terrain void compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo, double* timeLeft) const; - /// Set a WorkQueue to delete compiled composite map layers in the background thread - void setWorkQueue(SceneUtil::WorkQueue* workQueue); - /// Set the available time in seconds for compiling (non-immediate) composite maps each frame void setMinimumTimeAvailableForCompile(double time); @@ -67,9 +58,6 @@ namespace Terrain double mMinimumTimeAvailable; mutable osg::Timer mTimer; - osg::ref_ptr mUnrefQueue; - osg::ref_ptr mWorkQueue; - typedef std::set > CompileSet; mutable CompileSet mCompileSet; diff --git a/components/terrain/heightcull.hpp b/components/terrain/heightcull.hpp new file mode 100644 index 0000000000..d7db5fbccb --- /dev/null +++ b/components/terrain/heightcull.hpp @@ -0,0 +1,62 @@ +#ifndef COMPONENTS_TERRAIN_HEIGHTCULL_H +#define COMPONENTS_TERRAIN_HEIGHTCULL_H + +#include +#include + +#include + +#include + +namespace osg +{ + class Node; + class NodeVisitor; +} + +namespace Terrain +{ + class HeightCullCallback : public SceneUtil::NodeCallback + { + public: + void setLowZ(float z) + { + mLowZ = z; + } + float getLowZ() const + { + return mLowZ; + } + + void setHighZ(float highZ) + { + mHighZ = highZ; + } + float getHighZ() const + { + return mHighZ; + } + + void setCullMask(unsigned int mask) + { + mMask = mask; + } + unsigned int getCullMask() const + { + return mMask; + } + + void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (mLowZ <= mHighZ) + traverse(node, nv); + } + private: + float mLowZ{ -std::numeric_limits::max() }; + float mHighZ{ std::numeric_limits::max() }; + unsigned int mMask{ ~0u }; + }; + +} + +#endif diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index e662f4439f..1c6770e6bc 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -6,8 +6,12 @@ #include #include #include +#include +#include +#include #include +#include #include @@ -86,7 +90,7 @@ namespace osg::ref_ptr mValue; EqualDepth() - : mValue(new osg::Depth) + : mValue(new SceneUtil::AutoDepth) { mValue->setFunction(osg::Depth::EQUAL); } @@ -105,9 +109,8 @@ namespace osg::ref_ptr mValue; LequalDepth() - : mValue(new osg::Depth) + : mValue(new SceneUtil::AutoDepth(osg::Depth::LEQUAL)) { - mValue->setFunction(osg::Depth::LEQUAL); } }; @@ -166,17 +169,40 @@ namespace mValue->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); } }; + + class UniformCollection + { + public: + static const UniformCollection& value() + { + static UniformCollection instance; + return instance; + } + + osg::ref_ptr mDiffuseMap; + osg::ref_ptr mBlendMap; + osg::ref_ptr mNormalMap; + osg::ref_ptr mColorMode; + + UniformCollection() + : mDiffuseMap(new osg::Uniform("diffuseMap", 0)) + , mBlendMap(new osg::Uniform("blendMap", 1)) + , mNormalMap(new osg::Uniform("normalMap", 2)) + , mColorMode(new osg::Uniform("colorMode", 2)) + { + } + }; } namespace Terrain { - std::vector > createPasses(bool useShaders, Shader::ShaderManager* shaderManager, const std::vector &layers, + std::vector > createPasses(bool useShaders, Resource::SceneManager* sceneManager, const std::vector &layers, const std::vector > &blendmaps, int blendmapScale, float layerTileSize) { + auto& shaderManager = sceneManager->getShaderManager(); std::vector > passes; unsigned int blendmapIndex = 0; - unsigned int passIndex = 0; for (std::vector::const_iterator it = layers.begin(); it != layers.end(); ++it) { bool firstLayer = (it == layers.begin()); @@ -186,7 +212,9 @@ namespace Terrain if (!blendmaps.empty()) { stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - stateset->setRenderBinDetails(passIndex++, "RenderBin"); + if (sceneManager->getSupportsNormalsRT()) + stateset->setAttribute(new osg::Disablei(GL_BLEND, 1)); + stateset->setRenderBinDetails(firstLayer ? 0 : 1, "RenderBin"); if (!firstLayer) { stateset->setAttributeAndModes(BlendFunc::value(), osg::StateAttribute::ON); @@ -199,32 +227,28 @@ namespace Terrain } } - int texunit = 0; - if (useShaders) { - stateset->setTextureAttributeAndModes(texunit, it->mDiffuseMap); + stateset->setTextureAttributeAndModes(0, it->mDiffuseMap); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("diffuseMap", texunit)); + stateset->addUniform(UniformCollection::value().mDiffuseMap); if (!blendmaps.empty()) { - ++texunit; osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->addUniform(new osg::Uniform("blendMap", texunit)); + stateset->setTextureAttributeAndModes(1, blendmap.get()); + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->addUniform(UniformCollection::value().mBlendMap); } if (it->mNormalMap) { - ++texunit; - stateset->setTextureAttributeAndModes(texunit, it->mNormalMap); - stateset->addUniform(new osg::Uniform("normalMap", texunit)); + stateset->setTextureAttributeAndModes(2, it->mNormalMap); + stateset->addUniform(UniformCollection::value().mNormalMap); } Shader::ShaderManager::DefineMap defineMap; @@ -232,41 +256,39 @@ namespace Terrain defineMap["blendMap"] = (!blendmaps.empty()) ? "1" : "0"; defineMap["specularMap"] = it->mSpecular ? "1" : "0"; defineMap["parallax"] = (it->mNormalMap && it->mParallax) ? "1" : "0"; + defineMap["writeNormals"] = (it == layers.end() - 1) ? "1" : "0"; + Stereo::Manager::instance().shaderStereoDefines(defineMap); - osg::ref_ptr vertexShader = shaderManager->getShader("terrain_vertex.glsl", defineMap, osg::Shader::VERTEX); - osg::ref_ptr fragmentShader = shaderManager->getShader("terrain_fragment.glsl", defineMap, osg::Shader::FRAGMENT); + osg::ref_ptr vertexShader = shaderManager.getShader("terrain_vertex.glsl", defineMap, osg::Shader::VERTEX); + osg::ref_ptr fragmentShader = shaderManager.getShader("terrain_fragment.glsl", defineMap, osg::Shader::FRAGMENT); if (!vertexShader || !fragmentShader) { // Try again without shader. Error already logged by above - return createPasses(false, shaderManager, layers, blendmaps, blendmapScale, layerTileSize); + return createPasses(false, sceneManager, layers, blendmaps, blendmapScale, layerTileSize); } - stateset->setAttributeAndModes(shaderManager->getProgram(vertexShader, fragmentShader)); - stateset->addUniform(new osg::Uniform("colorMode", 2)); + stateset->setAttributeAndModes(shaderManager.getProgram(vertexShader, fragmentShader)); + stateset->addUniform(UniformCollection::value().mColorMode); } else { // Add the actual layer texture osg::ref_ptr tex = it->mDiffuseMap; - stateset->setTextureAttributeAndModes(texunit, tex.get()); + stateset->setTextureAttributeAndModes(0, tex.get()); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); // Multiply by the alpha map if (!blendmaps.empty()) { osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); + stateset->setTextureAttributeAndModes(1, blendmap.get()); // This is to map corner vertices directly to the center of a blendmap texel. - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->setTextureAttributeAndModes(texunit, TexEnvCombine::value(), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->setTextureAttributeAndModes(1, TexEnvCombine::value(), osg::StateAttribute::ON); } } diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index 5f78af6a06..d5ef40a29e 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -10,9 +10,9 @@ namespace osg class Texture2D; } -namespace Shader +namespace Resource { - class ShaderManager; + class SceneManager; } namespace Terrain @@ -26,7 +26,7 @@ namespace Terrain bool mSpecular; }; - std::vector > createPasses(bool useShaders, Shader::ShaderManager* shaderManager, + std::vector > createPasses(bool useShaders, Resource::SceneManager* sceneManager, const std::vector& layers, const std::vector >& blendmaps, int blendmapScale, float layerTileSize); diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 7baea45c82..81d3ccb32d 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -10,6 +10,29 @@ namespace Terrain { +float distance(const osg::BoundingBox& box, const osg::Vec3f& v) +{ + if (box.contains(v)) + return 0; + else + { + osg::Vec3f maxDist(0,0,0); + if (v.x() < box.xMin()) + maxDist.x() = box.xMin() - v.x(); + else if (v.x() > box.xMax()) + maxDist.x() = v.x() - box.xMax(); + if (v.y() < box.yMin()) + maxDist.y() = box.yMin() - v.y(); + else if (v.y() > box.yMax()) + maxDist.y() = v.y() - box.yMax(); + if (v.z() < box.zMin()) + maxDist.z() = box.zMin() - v.z(); + else if (v.z() > box.zMax()) + maxDist.z() = v.z() - box.zMax(); + return maxDist.length(); + } +} + ChildDirection reflect(ChildDirection dir, Direction dir2) { assert(dir != Root); @@ -63,7 +86,7 @@ QuadTreeNode::QuadTreeNode(QuadTreeNode* parent, ChildDirection direction, float , mCenter(center) { for (unsigned int i=0; i<4; ++i) - mNeighbours[i] = 0; + mNeighbours[i] = nullptr; } QuadTreeNode::~QuadTreeNode() @@ -78,25 +101,7 @@ QuadTreeNode *QuadTreeNode::getNeighbour(Direction dir) float QuadTreeNode::distance(const osg::Vec3f& v) const { const osg::BoundingBox& box = getBoundingBox(); - if (box.contains(v)) - return 0; - else - { - osg::Vec3f maxDist(0,0,0); - if (v.x() < box.xMin()) - maxDist.x() = box.xMin() - v.x(); - else if (v.x() > box.xMax()) - maxDist.x() = v.x() - box.xMax(); - if (v.y() < box.yMin()) - maxDist.y() = box.yMin() - v.y(); - else if (v.y() > box.yMax()) - maxDist.y() = v.y() - box.yMax(); - if (v.z() < box.zMin()) - maxDist.z() = box.zMin() - v.z(); - else if (v.z() > box.zMax()) - maxDist.z() = v.z() - box.zMax(); - return maxDist.length(); - } + return Terrain::distance(box, v); } void QuadTreeNode::initNeighbours() diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index 7ae59b92f0..aba78b8b3a 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -34,6 +34,8 @@ namespace Terrain class ViewData; + float distance(const osg::BoundingBox&, const osg::Vec3f& v); + class QuadTreeNode : public osg::Group { public: diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 57c09000c1..5f39c4fc28 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -3,13 +3,14 @@ #include #include #include +#include #include -#include #include -#include #include +#include +#include #include "quadtreenode.hpp" #include "storage.hpp" @@ -17,6 +18,7 @@ #include "chunkmanager.hpp" #include "compositemaprenderer.hpp" #include "terraindrawable.hpp" +#include "heightcull.hpp" namespace { @@ -38,9 +40,9 @@ namespace return 1 << depth; } - int Log2( unsigned int n ) + unsigned int Log2( unsigned int n ) { - int targetlevel = 0; + unsigned int targetlevel = 0; while (n >>= 1) ++targetlevel; return targetlevel; } @@ -53,11 +55,12 @@ namespace Terrain class DefaultLodCallback : public LodCallback { public: - DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid) + DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid, float distanceModifier=0.f) : mFactor(factor) , mMinSize(minSize) , mViewDistance(viewDistance) , mActiveGrid(grid) + , mDistanceModifier(distanceModifier) { } @@ -65,8 +68,7 @@ public: { const osg::Vec2f& center = node->getCenter(); bool activeGrid = (center.x() > mActiveGrid.x() && center.y() > mActiveGrid.y() && center.x() < mActiveGrid.z() && center.y() < mActiveGrid.w()); - if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded - return StopTraversal; + if (node->getSize()>1) { float halfSize = node->getSize()/2; @@ -76,11 +78,18 @@ public: if (intersects) return Deeper; } - - int nativeLodLevel = Log2(static_cast(node->getSize()/mMinSize)); - int lodLevel = Log2(static_cast(dist/(Constants::CellSizeInUnits*mMinSize*mFactor))); - - return nativeLodLevel <= lodLevel ? StopTraversalAndUse : Deeper; + dist = std::max(0.f, dist + mDistanceModifier); + if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded + return StopTraversal; + return getNativeLodLevel(node, mMinSize) <= convertDistanceToLodLevel(dist, mMinSize, mFactor) ? StopTraversalAndUse : Deeper; + } + static unsigned int getNativeLodLevel(const QuadTreeNode* node, float minSize) + { + return Log2(static_cast(node->getSize()/minSize)); + } + static unsigned int convertDistanceToLodLevel(float dist, float minSize, float factor) + { + return Log2(static_cast(dist/(Constants::CellSizeInUnits*minSize*factor))); } private: @@ -88,10 +97,9 @@ private: float mMinSize; float mViewDistance; osg::Vec4i mActiveGrid; + float mDistanceModifier; }; -const float MIN_SIZE = 1/8.f; - class RootNode : public QuadTreeNode { public: @@ -214,8 +222,8 @@ public: { // We arrived at a leaf. // Since the tree is used for LOD level selection instead of culling, we do not need to load the actual height data here. - float minZ = -std::numeric_limits::max(); - float maxZ = std::numeric_limits::max(); + constexpr float minZ = -std::numeric_limits::max(); + constexpr float maxZ = std::numeric_limits::max(); float cellWorldSize = mStorage->getCellWorldSize(); osg::BoundingBox boundingBox(osg::Vec3f((center.x()-halfSize)*cellWorldSize, (center.y()-halfSize)*cellWorldSize, minZ), osg::Vec3f((center.x()+halfSize)*cellWorldSize, (center.y()+halfSize)*cellWorldSize, maxZ)); @@ -243,31 +251,60 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask, int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize) +class DebugChunkManager : public QuadTreeWorld::ChunkManager +{ +public: + DebugChunkManager(Resource::SceneManager* sceneManager, Storage* storage, unsigned int nodeMask) : mSceneManager(sceneManager), mStorage(storage), mNodeMask(nodeMask) {} + osg::ref_ptr getChunk(float size, const osg::Vec2f& chunkCenter, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + { + osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 }; + auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size, mStorage, mSceneManager, mNodeMask, 5.f, { 1, 0, 0, 0 }); + osg::ref_ptr pat = new SceneUtil::PositionAttitudeTransform; + pat->setPosition(-center*Constants::CellSizeInUnits); + pat->addChild(chunkBorder); + return pat; + } + unsigned int getNodeMask() { return mNodeMask; } +private: + Resource::SceneManager* mSceneManager; + Storage* mStorage; + unsigned int mNodeMask; +}; + +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks) : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) , mLodFactor(lodFactor) , mVertexLodMod(vertexLodMod) , mViewDistance(std::numeric_limits::max()) + , mMinSize(1/8.f) + , mDebugTerrainChunks(debugChunks) { mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapLevel(compMapLevel); mChunkManager->setMaxCompositeGeometrySize(maxCompGeometrySize); mChunkManagers.push_back(mChunkManager.get()); + + if (mDebugTerrainChunks) + { + mDebugChunkManager = std::make_unique(mResourceSystem->getSceneManager(), mStorage, borderMask); + addChunkManager(mDebugChunkManager.get()); + } } QuadTreeWorld::~QuadTreeWorld() { } -/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the data set. +/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the vertex data set, +/// NOT relative to mMinSize as is the case with node LODs. unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod) { - int lod = Log2(int(node->getSize())); + unsigned int vertexLod = DefaultLodCallback::getNativeLodLevel(node, 1); if (vertexLodMod > 0) { - lod = std::max(0, lod-vertexLodMod); + vertexLod = static_cast(std::max(0, static_cast(vertexLod)-vertexLodMod)); } else if (vertexLodMod < 0) { @@ -278,13 +315,13 @@ unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod) size *= 2; vertexLodMod = std::min(0, vertexLodMod+1); } - lod += std::abs(vertexLodMod); + vertexLod += std::abs(vertexLodMod); } - return lod; + return vertexLod; } /// get the flags to use for stitching in the index buffer so that chunks of different LOD connect seamlessly -unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const ViewData* vd) +unsigned int getLodFlags(QuadTreeNode* node, unsigned int ourVertexLod, int vertexLodMod, const ViewData* vd) { unsigned int lodFlags = 0; for (unsigned int i=0; i<4; ++i) @@ -297,32 +334,33 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const // our detail and the neighbour would handle stitching by itself. while (neighbour && !vd->contains(neighbour)) neighbour = neighbour->getParent(); - int lod = 0; + unsigned int lod = 0; if (neighbour) lod = getVertexLod(neighbour, vertexLodMod); - if (lod <= ourLod) // We only need to worry about neighbours less detailed than we are - + if (lod <= ourVertexLod) // We only need to worry about neighbours less detailed than we are - lod = 0; // neighbours with more detail will do the stitching themselves // Use 4 bits for each LOD delta if (lod > 0) { - lodFlags |= static_cast(lod - ourLod) << (4*i); + lodFlags |= (lod - ourVertexLod) << (4*i); } } + // Use the remaining bits for our vertex LOD + lodFlags |= (ourVertexLod << (4*4)); return lodFlags; } -void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile) +void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile) { if (!vd->hasChanged() && entry.mRenderingNode) return; - int ourLod = getVertexLod(entry.mNode, vertexLodMod); - if (vd->hasChanged()) { + unsigned int ourVertexLod = getVertexLod(entry.mNode, mVertexLodMod); // have to recompute the lodFlags in case a neighbour has changed LOD. - unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, vertexLodMod, vd); + unsigned int lodFlags = getLodFlags(entry.mNode, ourVertexLod, mVertexLodMod, vd); if (lodFlags != entry.mLodFlags) { entry.mRenderingNode = nullptr; @@ -338,9 +376,9 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f const osg::Vec2f& center = entry.mNode->getCenter(); bool activeGrid = (center.x() > gridbounds.x() && center.y() > gridbounds.y() && center.x() < gridbounds.z() && center.y() < gridbounds.w()); - for (QuadTreeWorld::ChunkManager* m : chunkManagers) + for (QuadTreeWorld::ChunkManager* m : mChunkManagers) { - osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); + osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); if (n) pat->addChild(n); } entry.mRenderingNode = pat; @@ -362,7 +400,7 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil: static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); osg::BoundingBox bb = static_cast(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); if (!bb.valid()) continue; @@ -404,44 +442,32 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) { bool isCullVisitor = nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR; if (!isCullVisitor && nv.getVisitorType() != osg::NodeVisitor::INTERSECTION_VISITOR) - { - if (nv.getName().find("AcceptedByComponentsTerrainQuadTreeWorld") != std::string::npos) - { - if (nv.getName().find("SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds") != std::string::npos) - { - SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds* clsb = static_cast(&nv); - clsb->apply(*this); - } - else - nv.apply(*mRootNode); - } return; - } osg::Object * viewer = isCullVisitor ? static_cast(&nv)->getCurrentCamera() : nullptr; bool needsUpdate = true; - ViewData *vd = mViewDataMap->getViewData(viewer, nv.getViewPoint(), mActiveGrid, needsUpdate); - + osg::Vec3f viewPoint = viewer ? nv.getViewPoint() : nv.getEyePoint(); + ViewData *vd = mViewDataMap->getViewData(viewer, viewPoint, mActiveGrid, needsUpdate); if (needsUpdate) { vd->reset(); - DefaultLodCallback lodCallback(mLodFactor, MIN_SIZE, mViewDistance, mActiveGrid); - mRootNode->traverseNodes(vd, nv.getViewPoint(), &lodCallback); + DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, mActiveGrid); + mRootNode->traverseNodes(vd, viewPoint, &lodCallback); } const float cellWorldSize = mStorage->getCellWorldSize(); for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, mActiveGrid, mChunkManagers, false); + ViewDataEntry& entry = vd->getEntry(i); + loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false); entry.mRenderingNode->accept(nv); } - if (isCullVisitor) + if (mHeightCullCallback && isCullVisitor) updateWaterCullingView(mHeightCullCallback, vd, static_cast(&nv), mStorage->getCellWorldSize(), !isGridEmpty()); - vd->markUnchanged(); + vd->setChanged(false); double referenceTime = nv.getFrameStamp() ? nv.getFrameStamp()->getReferenceTime() : 0.0; if (referenceTime != 0.0) @@ -457,7 +483,7 @@ void QuadTreeWorld::ensureQuadTreeBuilt() if (mQuadTreeBuilt) return; - QuadTreeBuilder builder(mStorage, MIN_SIZE); + QuadTreeBuilder builder(mStorage, mMinSize); builder.build(); mRootNode = builder.getRootNode(); @@ -474,9 +500,8 @@ void QuadTreeWorld::enable(bool enabled) if (!mRootNode->getNumParents()) mTerrainRoot->addChild(mRootNode); } - - if (mRootNode) - mRootNode->setNodeMask(enabled ? ~0 : 0); + else if (mRootNode) + mTerrainRoot->removeChild(mRootNode); } View* QuadTreeWorld::createView() @@ -484,45 +509,58 @@ View* QuadTreeWorld::createView() return mViewDataMap->createIndependentView(); } -void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::Vec4i &grid, std::atomic &abort, std::atomic &progress, int& progressTotal) +void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::Vec4i &grid, std::atomic &abort, Loading::Reporter& reporter) { ensureQuadTreeBuilt(); + const float cellWorldSize = mStorage->getCellWorldSize(); ViewData* vd = static_cast(view); vd->setViewPoint(viewPoint); vd->setActiveGrid(grid); - DefaultLodCallback lodCallback(mLodFactor, MIN_SIZE, mViewDistance, grid); - mRootNode->traverseNodes(vd, viewPoint, &lodCallback); - if (!progressTotal) - for (unsigned int i=0; igetNumEntries(); ++i) - progressTotal += vd->getEntry(i).mNode->getSize(); - - const float cellWorldSize = mStorage->getCellWorldSize(); - for (unsigned int i=0; igetNumEntries() && !abort; ++i) + for (unsigned int pass=0; pass<3; ++pass) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true); - progress += entry.mNode->getSize(); - } - vd->markUnchanged(); -} + unsigned int startEntry = vd->getNumEntries(); -bool QuadTreeWorld::storeView(const View* view, double referenceTime) -{ - return mViewDataMap->storeView(static_cast(view), referenceTime); + float distanceModifier=0.f; + if (pass == 1) + distanceModifier = 1024; + else if (pass == 2) + distanceModifier = -1024; + DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid, distanceModifier); + mRootNode->traverseNodes(vd, viewPoint, &lodCallback); + + if (pass==0) + { + std::size_t progressTotal = 0; + for (unsigned int i = 0, n = vd->getNumEntries(); i < n; ++i) + progressTotal += vd->getEntry(i).mNode->getSize(); + + reporter.addTotal(progressTotal); + } + + for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) + { + ViewDataEntry& entry = vd->getEntry(i); + + loadRenderingNode(entry, vd, cellWorldSize, grid, true); + if (pass==0) reporter.addProgress(entry.mNode->getSize()); + entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass + } + } } void QuadTreeWorld::reportStats(unsigned int frameNumber, osg::Stats *stats) { - stats->setAttribute(frameNumber, "Composite", mCompositeMapRenderer->getCompileSetSize()); + if (mCompositeMapRenderer) + stats->setAttribute(frameNumber, "Composite", mCompositeMapRenderer->getCompileSetSize()); } void QuadTreeWorld::loadCell(int x, int y) { // fallback behavior only for undefined cells (every other is already handled in quadtree) float dummy; - if (!mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy)) + if (mChunkManager && !mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy)) TerrainGrid::loadCell(x,y); else World::loadCell(x,y); @@ -532,7 +570,7 @@ void QuadTreeWorld::unloadCell(int x, int y) { // fallback behavior only for undefined cells (every other is already handled in quadtree) float dummy; - if (!mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy)) + if (mChunkManager && !mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy)) TerrainGrid::unloadCell(x,y); else World::unloadCell(x,y); @@ -542,6 +580,8 @@ void QuadTreeWorld::addChunkManager(QuadTreeWorld::ChunkManager* m) { mChunkManagers.push_back(m); mTerrainRoot->setNodeMask(mTerrainRoot->getNodeMask()|m->getNodeMask()); + if (m->getViewDistance()) + m->setMaxLodLevel(DefaultLodCallback::convertDistanceToLodLevel(m->getViewDistance() + mViewDataMap->getReuseDistance(), mMinSize, mLodFactor)); } void QuadTreeWorld::rebuildViews() @@ -549,4 +589,12 @@ void QuadTreeWorld::rebuildViews() mViewDataMap->rebuildViews(); } +void QuadTreeWorld::setViewDistance(float viewDistance) +{ + if (mViewDistance == viewDistance) + return; + mViewDistance = viewDistance; + mViewDataMap->rebuildViews(); +} + } diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 4c05efe646..d03ed613d4 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -1,26 +1,33 @@ #ifndef COMPONENTS_TERRAIN_QUADTREEWORLD_H #define COMPONENTS_TERRAIN_QUADTREEWORLD_H -#include "world.hpp" #include "terraingrid.hpp" #include +#include +#include namespace osg { class NodeVisitor; + class Group; + class Stats; } namespace Terrain { class RootNode; class ViewDataMap; + class ViewData; + struct ViewDataEntry; + + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. class QuadTreeWorld : public TerrainGrid // note: derived from TerrainGrid is only to render default cells (see loadCell) { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks); ~QuadTreeWorld(); @@ -28,7 +35,7 @@ namespace Terrain void enable(bool enabled) override; - void setViewDistance(float distance) override { mViewDistance = distance; } + void setViewDistance(float distance) override; void cacheCell(View *view, int x, int y) override {} /// @note Not thread safe. @@ -37,8 +44,7 @@ namespace Terrain void unloadCell(int x, int y) override; View* createView() override; - void preload(View* view, const osg::Vec3f& eyePoint, const osg::Vec4i &cellgrid, std::atomic& abort, std::atomic& progress, int& progressRange) override; - bool storeView(const View* view, double referenceTime) override; + void preload(View* view, const osg::Vec3f& eyePoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) override; void rebuildViews() override; void reportStats(unsigned int frameNumber, osg::Stats* stats) override; @@ -47,13 +53,24 @@ namespace Terrain { public: virtual ~ChunkManager(){} - virtual osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile) = 0; + virtual osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) = 0; virtual unsigned int getNodeMask() { return 0; } + + void setViewDistance(float viewDistance) { mViewDistance = viewDistance; } + float getViewDistance() const { return mViewDistance; } + + // Automatically set by addChunkManager based on getViewDistance() + unsigned int getMaxLodLevel() const { return mMaxLodLevel; } + void setMaxLodLevel(unsigned int level) { mMaxLodLevel = level; } + private: + float mViewDistance = 0.f; + unsigned int mMaxLodLevel = ~0u; }; void addChunkManager(ChunkManager*); private: void ensureQuadTreeBuilt(); + void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile); osg::ref_ptr mRootNode; @@ -66,6 +83,9 @@ namespace Terrain float mLodFactor; int mVertexLodMod; float mViewDistance; + float mMinSize; + bool mDebugTerrainChunks; + std::unique_ptr mDebugChunkManager; }; } diff --git a/components/terrain/terraindrawable.cpp b/components/terrain/terraindrawable.cpp index 0d82be4fff..231b6f4fed 100644 --- a/components/terrain/terraindrawable.cpp +++ b/components/terrain/terraindrawable.cpp @@ -78,7 +78,7 @@ void TerrainDrawable::cull(osgUtil::CullVisitor *cv) osg::RefMatrix& matrix = *cv->getModelViewMatrix(); - if (cv->getComputeNearFarMode() && bb.valid()) + if (cv->getComputeNearFarMode() != osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR && bb.valid()) { if (!cv->updateCalculatedNearFar(matrix, *this, false)) return; @@ -94,10 +94,10 @@ void TerrainDrawable::cull(osgUtil::CullVisitor *cv) return; } - if (mCompositeMap) + if (mCompositeMap && mCompositeMapRenderer) { mCompositeMapRenderer->setImmediate(mCompositeMap); - mCompositeMap = nullptr; + mCompositeMapRenderer = nullptr; } bool pushedLight = mLightListCallback && mLightListCallback->pushLightState(this, cv); diff --git a/components/terrain/terraindrawable.hpp b/components/terrain/terraindrawable.hpp index dbfdd3c80a..721abe7481 100644 --- a/components/terrain/terraindrawable.hpp +++ b/components/terrain/terraindrawable.hpp @@ -45,6 +45,7 @@ namespace Terrain typedef std::vector > PassVector; void setPasses (const PassVector& passes); + const PassVector& getPasses() const { return mPasses; } void setLightListCallback(SceneUtil::LightListCallback* lightListCallback); @@ -56,6 +57,7 @@ namespace Terrain const osg::BoundingBox& getWaterBoundingBox() const { return mWaterBoundingBox; } void setCompositeMap(CompositeMap* map) { mCompositeMap = map; } + CompositeMap* getCompositeMap() { return mCompositeMap; } void setCompositeMapRenderer(CompositeMapRenderer* renderer) { mCompositeMapRenderer = renderer; } private: diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index 679597971e..6467b549f1 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -8,7 +8,10 @@ #include #include "chunkmanager.hpp" #include "compositemaprenderer.hpp" +#include "view.hpp" #include "storage.hpp" +#include "heightcull.hpp" + namespace Terrain { @@ -20,12 +23,18 @@ public: void reset() override {} }; -TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) +TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask) : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mNumSplits(4) { } +TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, unsigned int nodeMask) + : Terrain::World(parent, storage, nodeMask) + , mNumSplits(4) +{ +} + TerrainGrid::~TerrainGrid() { while (!mGrid.empty()) @@ -107,6 +116,8 @@ void TerrainGrid::unloadCell(int x, int y) void TerrainGrid::updateWaterCulling() { + if (!mHeightCullCallback) return; + osg::ComputeBoundsVisitor computeBoundsVisitor; mTerrainRoot->accept(computeBoundsVisitor); float lowZ = computeBoundsVisitor.getBoundingBox()._min.z(); diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index f8b0fb2590..f09beb0cdc 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -7,14 +7,27 @@ #include "world.hpp" +namespace osg +{ + class Group; + class Stats; +} + +namespace Resource +{ + class ResourceSystem; +} + namespace Terrain { + class Storage; /// @brief Simple terrain implementation that loads cells in a grid, with no LOD. Only requested cells are loaded. class TerrainGrid : public Terrain::World { public: - TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); + TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask=~0u, unsigned int borderMask=0); + TerrainGrid(osg::Group* parent, Storage* storage, unsigned int nodeMask=~0u); ~TerrainGrid(); void cacheCell(View* view, int x, int y) override; diff --git a/components/terrain/view.hpp b/components/terrain/view.hpp new file mode 100644 index 0000000000..1ce0876603 --- /dev/null +++ b/components/terrain/view.hpp @@ -0,0 +1,25 @@ +#ifndef COMPONENTS_TERRAIN_VIEW_H +#define COMPONENTS_TERRAIN_VIEW_H + +#include +#include +#include + +namespace Terrain +{ + /** + * @brief A View is a collection of rendering objects that are visible from a given camera/intersection. + * The base View class is part of the interface for usage in conjunction with preload feature. + */ + class View : public osg::Referenced + { + public: + virtual ~View() {} + + /// Reset internal structure so that the next addition to the view will override the previous frame's contents. + virtual void reset() = 0; + }; + +} + +#endif diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index e4d043ffc4..033b5f5faa 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -12,12 +12,10 @@ ViewData::ViewData() , mHasViewPoint(false) , mWorldUpdateRevision(0) { - } ViewData::~ViewData() { - } void ViewData::copyFrom(const ViewData& other) @@ -38,42 +36,19 @@ void ViewData::add(QuadTreeNode *node) if (index+1 > mEntries.size()) mEntries.resize(index+1); - Entry& entry = mEntries[index]; + ViewDataEntry& entry = mEntries[index]; if (entry.set(node)) mChanged = true; } -unsigned int ViewData::getNumEntries() const -{ - return mNumEntries; -} - -ViewData::Entry &ViewData::getEntry(unsigned int i) -{ - return mEntries[i]; -} - -bool ViewData::hasChanged() const -{ - return mChanged; -} - -bool ViewData::hasViewPoint() const -{ - return mHasViewPoint; -} - void ViewData::setViewPoint(const osg::Vec3f &viewPoint) { mViewPoint = viewPoint; mHasViewPoint = true; } -const osg::Vec3f& ViewData::getViewPoint() const -{ - return mViewPoint; -} - +// NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here. +// If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset(). void ViewData::reset() { // clear any unused entries @@ -108,14 +83,13 @@ bool ViewData::contains(QuadTreeNode *node) const return false; } -ViewData::Entry::Entry() +ViewDataEntry::ViewDataEntry() : mNode(nullptr) , mLodFlags(0) { - } -bool ViewData::Entry::set(QuadTreeNode *node) +bool ViewDataEntry::set(QuadTreeNode *node) { if (node == mNode) return false; @@ -143,7 +117,7 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo if (!(vd->suitableToUse(activeGrid) && (vd->getViewPoint()-viewPoint).length2() < mReuseDistance*mReuseDistance && vd->getWorldUpdateRevision() >= mWorldUpdateRevision)) { - float shortestDist = std::numeric_limits::max(); + float shortestDist = viewer ? mReuseDistance*mReuseDistance : std::numeric_limits::max(); const ViewData* mostSuitableView = nullptr; for (const ViewData* other : mUsedViews) { @@ -162,26 +136,21 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo vd->copyFrom(*mostSuitableView); return vd; } - } - if (!vd->suitableToUse(activeGrid)) - { - vd->setViewPoint(viewPoint); - vd->setActiveGrid(activeGrid); - needsUpdate = true; + else if (!mostSuitableView) + { + if (vd->getWorldUpdateRevision() != mWorldUpdateRevision) + { + vd->setWorldUpdateRevision(mWorldUpdateRevision); + vd->clear(); + } + vd->setViewPoint(viewPoint); + vd->setActiveGrid(activeGrid); + needsUpdate = true; + } } return vd; } -bool ViewDataMap::storeView(const ViewData* view, double referenceTime) -{ - if (view->getWorldUpdateRevision() < mWorldUpdateRevision) - return false; - ViewData* store = createOrReuseView(); - store->copyFrom(*view); - store->setLastUsageTimeStamp(referenceTime); - return true; -} - ViewData *ViewDataMap::createOrReuseView() { ViewData* vd = nullptr; diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 0289352585..125ee7dfc0 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -6,13 +6,25 @@ #include -#include "world.hpp" +#include "view.hpp" namespace Terrain { class QuadTreeNode; + struct ViewDataEntry + { + ViewDataEntry(); + + bool set(QuadTreeNode* node); + + QuadTreeNode* mNode; + + unsigned int mLodFlags; + osg::ref_ptr mRenderingNode; + }; + class ViewData : public View { public: @@ -31,33 +43,22 @@ namespace Terrain void copyFrom(const ViewData& other); - struct Entry - { - Entry(); - - bool set(QuadTreeNode* node); - - QuadTreeNode* mNode; - - unsigned int mLodFlags; - osg::ref_ptr mRenderingNode; - }; - - unsigned int getNumEntries() const; - - Entry& getEntry(unsigned int i); + unsigned int getNumEntries() const { return mNumEntries; } + ViewDataEntry& getEntry(unsigned int i) { return mEntries[i]; } double getLastUsageTimeStamp() const { return mLastUsageTimeStamp; } void setLastUsageTimeStamp(double timeStamp) { mLastUsageTimeStamp = timeStamp; } - /// @return Have any nodes changed since the last frame - bool hasChanged() const; - void markUnchanged() { mChanged = false; } + /// Indicates at least one mNode of mEntries has changed. + /// @note Such changes may necessitate a revalidation of cached mRenderingNodes elsewhere depending + /// on the parameters that affect the creation of mRenderingNode. + bool hasChanged() const { return mChanged; } + void setChanged(bool changed) { mChanged = changed; } - bool hasViewPoint() const; + bool hasViewPoint() const { return mHasViewPoint; } void setViewPoint(const osg::Vec3f& viewPoint); - const osg::Vec3f& getViewPoint() const; + const osg::Vec3f& getViewPoint() const { return mViewPoint; } void setActiveGrid(const osg::Vec4i &grid) { if (grid != mActiveGrid) {mActiveGrid = grid;mEntries.clear();mNumEntries=0;} } const osg::Vec4i &getActiveGrid() const { return mActiveGrid;} @@ -66,7 +67,7 @@ namespace Terrain void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; } private: - std::vector mEntries; + std::vector mEntries; unsigned int mNumEntries; double mLastUsageTimeStamp; bool mChanged; @@ -93,7 +94,8 @@ namespace Terrain void clearUnusedViews(double referenceTime); void rebuildViews(); - bool storeView(const ViewData* view, double referenceTime); + + float getReuseDistance() const { return mReuseDistance; } private: std::list mViewVector; diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 5b4807b387..5798784e83 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -4,16 +4,18 @@ #include #include +#include #include "storage.hpp" #include "texturemanager.hpp" #include "chunkmanager.hpp" #include "compositemaprenderer.hpp" +#include "heightcull.hpp" namespace Terrain { -World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) +World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask) : mStorage(storage) , mParent(parent) , mResourceSystem(resourceSystem) @@ -40,31 +42,47 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mParent->addChild(mTerrainRoot); - mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); - mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); + mTextureManager = std::make_unique(mResourceSystem->getSceneManager()); + mChunkManager = std::make_unique(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer); mChunkManager->setNodeMask(nodeMask); - mCellBorder.reset(new CellBorder(this,mTerrainRoot.get(),borderMask)); + mCellBorder = std::make_unique(this,mTerrainRoot.get(),borderMask,mResourceSystem->getSceneManager()); mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); } -World::~World() +World::World(osg::Group* parent, Storage* storage, unsigned int nodeMask) + : mStorage(storage) + , mParent(parent) + , mCompositeMapCamera(nullptr) + , mCompositeMapRenderer(nullptr) + , mResourceSystem(nullptr) + , mTextureManager(nullptr) + , mChunkManager(nullptr) + , mCellBorder(nullptr) + , mBorderVisible(false) + , mHeightCullCallback(nullptr) { - mResourceSystem->removeResourceManager(mChunkManager.get()); - mResourceSystem->removeResourceManager(mTextureManager.get()); - - mParent->removeChild(mTerrainRoot); - - mCompositeMapCamera->removeChild(mCompositeMapRenderer); - mCompositeMapCamera->getParent(0)->removeChild(mCompositeMapCamera); + mTerrainRoot = new osg::Group; + mTerrainRoot->setNodeMask(nodeMask); - delete mStorage; + mParent->addChild(mTerrainRoot); } -void World::setWorkQueue(SceneUtil::WorkQueue* workQueue) +World::~World() { - mCompositeMapRenderer->setWorkQueue(workQueue); + if (mResourceSystem && mChunkManager) + mResourceSystem->removeResourceManager(mChunkManager.get()); + if (mResourceSystem && mTextureManager) + mResourceSystem->removeResourceManager(mTextureManager.get()); + + mParent->removeChild(mTerrainRoot); + + if (mCompositeMapCamera && mCompositeMapRenderer) + { + mCompositeMapCamera->removeChild(mCompositeMapRenderer); + mCompositeMapCamera->getParent(0)->removeChild(mCompositeMapCamera); + } } void World::setBordersVisible(bool visible) @@ -108,16 +126,20 @@ float World::getHeightAt(const osg::Vec3f &worldPos) void World::updateTextureFiltering() { - mTextureManager->updateTextureFiltering(); + if (mTextureManager) + mTextureManager->updateTextureFiltering(); } void World::clearAssociatedCaches() { - mChunkManager->clearCache(); + if (mChunkManager) + mChunkManager->clearCache(); } osg::Callback* World::getHeightCullCallback(float highz, unsigned int mask) { + if (!mHeightCullCallback) return nullptr; + mHeightCullCallback->setHighZ(highz); mHeightCullCallback->setCullMask(mask); return mHeightCullCallback; diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index d94125100e..85114c32d2 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -4,13 +4,12 @@ #include #include #include -#include -#include -#include #include #include +#include + #include "defs.hpp" #include "cellborder.hpp" @@ -18,8 +17,6 @@ namespace osg { class Group; class Stats; - class Node; - class Object; } namespace Resource @@ -27,9 +24,9 @@ namespace Resource class ResourceSystem; } -namespace SceneUtil +namespace Loading { - class WorkQueue; + class Reporter; } namespace Terrain @@ -39,61 +36,9 @@ namespace Terrain class TextureManager; class ChunkManager; class CompositeMapRenderer; - - class HeightCullCallback : public osg::NodeCallback - { - public: - void setLowZ(float z) - { - mLowZ = z; - } - float getLowZ() const - { - return mLowZ; - } - - void setHighZ(float highZ) - { - mHighZ = highZ; - } - float getHighZ() const - { - return mHighZ; - } - - void setCullMask(unsigned int mask) - { - mMask = mask; - } - unsigned int getCullMask() const - { - return mMask; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - if (mLowZ <= mHighZ) - traverse(node, nv); - } - private: - float mLowZ{-std::numeric_limits::max()}; - float mHighZ{std::numeric_limits::max()}; - unsigned int mMask{~0u}; - }; - - /** - * @brief A View is a collection of rendering objects that are visible from a given camera/intersection. - * The base View class is part of the interface for usage in conjunction with preload feature. - */ - class View : public osg::Referenced - { - public: - virtual ~View() {} - - /// Reset internal structure so that the next addition to the view will override the previous frame's contents. - virtual void reset() = 0; - }; - + class View; + class HeightCullCallback; + /** * @brief The basic interface for a terrain world. How the terrain chunks are paged and displayed * is up to the implementation. @@ -105,12 +50,10 @@ namespace Terrain /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param nodeMask mask for the terrain root /// @param preCompileMask mask for pre compiling textures - World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask); + World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask); + World(osg::Group* parent, Storage* storage, unsigned int nodeMask); virtual ~World(); - /// Set a WorkQueue to delete objects in the background thread. - void setWorkQueue(SceneUtil::WorkQueue* workQueue); - /// See CompositeMapRenderer::setTargetFrameRate void setTargetFrameRate(float rate); @@ -147,11 +90,7 @@ namespace Terrain /// @note Thread safe, as long as you do not attempt to load into the same view from multiple threads. - virtual void preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i &cellgrid, std::atomic& abort, std::atomic& progress, int& progressRange) {} - - /// Store a preloaded view into the cache with the intent that the next rendering traversal can use it. - /// @note Not thread safe. - virtual bool storeView(const View* view, double referenceTime) {return true;} + virtual void preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) {} virtual void rebuildViews() {} diff --git a/components/to_utf8/gen_iconv.cpp b/components/to_utf8/gen_iconv.cpp index 8198b305dd..75d83fb1a7 100644 --- a/components/to_utf8/gen_iconv.cpp +++ b/components/to_utf8/gen_iconv.cpp @@ -1,20 +1,19 @@ // This program generates the file tables_gen.hpp #include -using namespace std; #include #include -void tab() { cout << " "; } +void tab() { std::cout << " "; } // write one number with a space in front of it and a comma after it void num(char i, bool last) { // Convert i to its integer value, i.e. -128 to 127. Printing it directly // would result in non-printable characters in the source code, which is bad. - cout << " " << static_cast(i); - if(!last) cout << ","; + std::cout << " " << static_cast(i); + if(!last) std::cout << ","; } // Write one table entry (UTF8 value), 1-5 bytes @@ -27,9 +26,9 @@ void writeChar(char *value, int length, bool last, const std::string &comment="" num(value[i], last && i==4); if(comment != "") - cout << " // " << comment; + std::cout << " // " << comment; - cout << endl; + std::cout << std::endl; } // What to write on missing characters @@ -46,7 +45,7 @@ void writeMissing(bool last) int write_table(const std::string &charset, const std::string &tableName) { // Write table header - cout << "static signed char " << tableName << "[] =\n{\n"; + std::cout << "const static signed char " << tableName << "[] =\n{\n"; // Open conversion system iconv_t cd = iconv_open ("UTF-8", charset.c_str()); @@ -74,7 +73,7 @@ int write_table(const std::string &charset, const std::string &tableName) iconv_close (cd); // Finish table - cout << "};\n"; + std::cout << "};\n"; return 0; } @@ -82,37 +81,37 @@ int write_table(const std::string &charset, const std::string &tableName) int main() { // Write header guard - cout << "#ifndef COMPONENTS_TOUTF8_TABLE_GEN_H\n#define COMPONENTS_TOUTF8_TABLE_GEN_H\n\n"; + std::cout << "#ifndef COMPONENTS_TOUTF8_TABLE_GEN_H\n#define COMPONENTS_TOUTF8_TABLE_GEN_H\n\n"; // Write namespace - cout << "namespace ToUTF8\n{\n\n"; + std::cout << "namespace ToUTF8\n{\n\n"; // Central European and Eastern European languages that use Latin script, such as // Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian. - cout << "\n/// Central European and Eastern European languages that use Latin script," - "\n/// such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian," - "\n/// Serbian (Latin script), Romanian and Albanian." - "\n"; + std::cout << "\n/// Central European and Eastern European languages that use Latin script," + "\n/// such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian," + "\n/// Serbian (Latin script), Romanian and Albanian." + "\n"; write_table("WINDOWS-1250", "windows_1250"); // Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages - cout << "\n/// Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic" - "\n/// and other languages" - "\n"; + std::cout << "\n/// Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic" + "\n/// and other languages" + "\n"; write_table("WINDOWS-1251", "windows_1251"); // English - cout << "\n/// Latin alphabet used by English and some other Western languages" - "\n"; + std::cout << "\n/// Latin alphabet used by English and some other Western languages" + "\n"; write_table("WINDOWS-1252", "windows_1252"); write_table("CP437", "cp437"); // Close namespace - cout << "\n}\n\n"; + std::cout << "\n}\n\n"; // Close header guard - cout << "#endif\n\n"; + std::cout << "#endif\n\n"; return 0; } diff --git a/components/to_utf8/tables_gen.hpp b/components/to_utf8/tables_gen.hpp index 14e66eac17..b7659979eb 100644 --- a/components/to_utf8/tables_gen.hpp +++ b/components/to_utf8/tables_gen.hpp @@ -8,7 +8,7 @@ namespace ToUTF8 /// Central European and Eastern European languages that use Latin script, /// such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, /// Serbian (Latin script), Romanian and Albanian. -static signed char windows_1250[] = +const static signed char windows_1250[] = { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, @@ -270,7 +270,7 @@ static signed char windows_1250[] = /// Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic /// and other languages -static signed char windows_1251[] = +const static signed char windows_1251[] = { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, @@ -531,7 +531,7 @@ static signed char windows_1251[] = }; /// Latin alphabet used by English and some other Western languages -static signed char windows_1252[] = +const static signed char windows_1252[] = { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, @@ -790,7 +790,7 @@ static signed char windows_1252[] = 2, -61, -66, 0, 0, 0, 2, -61, -65, 0, 0, 0 }; -static signed char cp437[] = +const static signed char cp437[] = { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, diff --git a/components/to_utf8/tests/.gitignore b/components/to_utf8/tests/.gitignore deleted file mode 100644 index 8144904045..0000000000 --- a/components/to_utf8/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*_test diff --git a/components/to_utf8/tests/output/to_utf8_test.out b/components/to_utf8/tests/output/to_utf8_test.out deleted file mode 100644 index dcb32359ab..0000000000 --- a/components/to_utf8/tests/output/to_utf8_test.out +++ /dev/null @@ -1,4 +0,0 @@ -original: Без вопросов отдаете ему рулет, зная, что позже вы сможете привести с собой своих друзей и тогда он получит по заслугам? -converted: Без вопросов отдаете ему рулет, зная, что позже вы сможете привести с собой своих друзей и тогда он получит по заслугам? -original: Vous lui donnez le gâteau sans protester avant d’aller chercher tous vos amis et de revenir vous venger. -converted: Vous lui donnez le gâteau sans protester avant d’aller chercher tous vos amis et de revenir vous venger. diff --git a/components/to_utf8/tests/test.sh b/components/to_utf8/tests/test.sh deleted file mode 100755 index 2d07708adc..0000000000 --- a/components/to_utf8/tests/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -make || exit - -mkdir -p output - -PROGS=*_test - -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done diff --git a/components/to_utf8/tests/to_utf8_test.cpp b/components/to_utf8/tests/to_utf8_test.cpp deleted file mode 100644 index 3fcddd1581..0000000000 --- a/components/to_utf8/tests/to_utf8_test.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include -#include -#include - -#include "../to_utf8.hpp" - -std::string getFirstLine(const std::string &filename); -void testEncoder(ToUTF8::FromType encoding, const std::string &legacyEncFile, - const std::string &utf8File); - -/// Test character encoding conversion to and from UTF-8 -void testEncoder(ToUTF8::FromType encoding, const std::string &legacyEncFile, - const std::string &utf8File) -{ - // get some test data - std::string legacyEncLine = getFirstLine(legacyEncFile); - std::string utf8Line = getFirstLine(utf8File); - - // create an encoder for specified character encoding - ToUTF8::Utf8Encoder encoder (encoding); - - // convert text to UTF-8 - std::string convertedUtf8Line = encoder.getUtf8(legacyEncLine); - - std::cout << "original: " << utf8Line << std::endl; - std::cout << "converted: " << convertedUtf8Line << std::endl; - - // check correctness - assert(convertedUtf8Line == utf8Line); - - // convert UTF-8 text to legacy encoding - std::string convertedLegacyEncLine = encoder.getLegacyEnc(utf8Line); - // check correctness - assert(convertedLegacyEncLine == legacyEncLine); -} - -std::string getFirstLine(const std::string &filename) -{ - std::string line; - std::ifstream text (filename.c_str()); - - if (!text.is_open()) - { - throw std::runtime_error("Unable to open file " + filename); - } - - std::getline(text, line); - text.close(); - - return line; -} - -int main() -{ - testEncoder(ToUTF8::WINDOWS_1251, "test_data/russian-win1251.txt", "test_data/russian-utf8.txt"); - testEncoder(ToUTF8::WINDOWS_1252, "test_data/french-win1252.txt", "test_data/french-utf8.txt"); - return 0; -} diff --git a/components/to_utf8/to_utf8.cpp b/components/to_utf8/to_utf8.cpp index bcb174b7be..6e04db8db4 100644 --- a/components/to_utf8/to_utf8.cpp +++ b/components/to_utf8/to_utf8.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -44,45 +45,67 @@ using namespace ToUTF8; -Utf8Encoder::Utf8Encoder(const FromType sourceEncoding): - mOutput(50*1024) +namespace { - switch (sourceEncoding) + std::string_view::iterator skipAscii(std::string_view input) { - case ToUTF8::WINDOWS_1252: - { - translationArray = ToUTF8::windows_1252; - break; - } - case ToUTF8::WINDOWS_1250: - { - translationArray = ToUTF8::windows_1250; - break; - } - case ToUTF8::WINDOWS_1251: + return std::find_if(input.begin(), input.end(), [] (unsigned char v) { return v == 0 || v >= 128; }); + } + + std::basic_string_view getTranslationArray(FromType sourceEncoding) + { + switch (sourceEncoding) { - translationArray = ToUTF8::windows_1251; - break; + case ToUTF8::WINDOWS_1252: + return {ToUTF8::windows_1252, std::size(ToUTF8::windows_1252)}; + case ToUTF8::WINDOWS_1250: + return {ToUTF8::windows_1250, std::size(ToUTF8::windows_1250)}; + case ToUTF8::WINDOWS_1251: + return {ToUTF8::windows_1251, std::size(ToUTF8::windows_1251)}; + case ToUTF8::CP437: + return {ToUTF8::cp437, std::size(ToUTF8::cp437)}; } - case ToUTF8::CP437: + throw std::logic_error("Invalid source encoding: " + std::to_string(sourceEncoding)); + } + + // Make sure the output vector is large enough for 'size' bytes, + // including a terminating zero after it. + void resize(std::size_t size, BufferAllocationPolicy bufferAllocationPolicy, std::string& buffer) + { + if (buffer.size() > size) { - translationArray = ToUTF8::cp437; - break; + buffer[size] = 0; + return; } - default: + if (buffer.size() == size) + return; + + switch (bufferAllocationPolicy) { - assert(0); + case BufferAllocationPolicy::FitToRequiredSize: + buffer.resize(size); + break; + case BufferAllocationPolicy::UseGrowFactor: + // Add some extra padding to reduce the chance of having to resize + // again later. + buffer.resize(3 * size); + // And make sure the string is zero terminated + buffer[size] = 0; + break; } } } -std::string Utf8Encoder::getUtf8(const char* input, size_t size) +StatelessUtf8Encoder::StatelessUtf8Encoder(FromType sourceEncoding) + : mTranslationArray(getTranslationArray(sourceEncoding)) +{ +} + +std::string_view StatelessUtf8Encoder::getUtf8(std::string_view input, BufferAllocationPolicy bufferAllocationPolicy, std::string& buffer) const { - // Double check that the input string stops at some point (it might - // contain zero terminators before this, inside its own data, which - // is also ok.) - assert(input[size] == 0); + if (input.empty()) + return input; // Note: The rest of this function is designed for single-character // input encodings only. It also assumes that the input encoding @@ -92,38 +115,34 @@ std::string Utf8Encoder::getUtf8(const char* input, size_t size) // Compute output length, and check for pure ascii input at the same // time. - bool ascii; - size_t outlen = getLength(input, ascii); + const auto [outlen, ascii] = getLength(input); // If we're pure ascii, then don't bother converting anything. if(ascii) - return std::string(input, outlen); + return std::string_view(input.data(), outlen); // Make sure the output is large enough - resize(outlen); - char *out = &mOutput[0]; + resize(outlen, bufferAllocationPolicy, buffer); + char *out = buffer.data(); // Translate - while (*input) - copyFromArray(*(input++), out); + for (auto it = input.begin(); it != input.end() && *it != 0; ++it) + copyFromArray(*it, out); // Make sure that we wrote the correct number of bytes - assert((out-&mOutput[0]) == (int)outlen); + assert((out - buffer.data()) == (int)outlen); // And make extra sure the output is null terminated - assert(mOutput.size() > outlen); - assert(mOutput[outlen] == 0); + assert(buffer.size() >= outlen); + assert(buffer[outlen] == 0); - // Return a string - return std::string(&mOutput[0], outlen); + return std::string_view(buffer.data(), outlen); } -std::string Utf8Encoder::getLegacyEnc(const char *input, size_t size) +std::string_view StatelessUtf8Encoder::getLegacyEnc(std::string_view input, BufferAllocationPolicy bufferAllocationPolicy, std::string& buffer) const { - // Double check that the input string stops at some point (it might - // contain zero terminators before this, inside its own data, which - // is also ok.) - assert(input[size] == 0); + if (input.empty()) + return input; // TODO: The rest of this function is designed for single-character // input encodings only. It also assumes that the input the input @@ -133,43 +152,28 @@ std::string Utf8Encoder::getLegacyEnc(const char *input, size_t size) // Compute output length, and check for pure ascii input at the same // time. - bool ascii; - size_t outlen = getLength2(input, ascii); + const auto [outlen, ascii] = getLengthLegacyEnc(input); // If we're pure ascii, then don't bother converting anything. if(ascii) - return std::string(input, outlen); + return std::string_view(input.data(), outlen); // Make sure the output is large enough - resize(outlen); - char *out = &mOutput[0]; + resize(outlen, bufferAllocationPolicy, buffer); + char *out = buffer.data(); // Translate - while(*input) - copyFromArray2(input, out); + for (auto it = input.begin(); it != input.end() && *it != 0;) + copyFromArrayLegacyEnc(it, input.end(), out); // Make sure that we wrote the correct number of bytes - assert((out-&mOutput[0]) == (int)outlen); + assert((out - buffer.data()) == static_cast(outlen)); // And make extra sure the output is null terminated - assert(mOutput.size() > outlen); - assert(mOutput[outlen] == 0); + assert(buffer.size() >= outlen); + assert(buffer[outlen] == 0); - // Return a string - return std::string(&mOutput[0], outlen); -} - -// Make sure the output vector is large enough for 'size' bytes, -// including a terminating zero after it. -void Utf8Encoder::resize(size_t size) -{ - if (mOutput.size() <= size) - // Add some extra padding to reduce the chance of having to resize - // again later. - mOutput.resize(3*size); - - // And make sure the string is zero terminated - mOutput[size] = 0; + return std::string_view(buffer.data(), outlen); } /** Get the total length length needed to decode the given string with @@ -182,39 +186,35 @@ void Utf8Encoder::resize(size_t size) is the case, then the ascii parameter is set to true, and the caller can optimize for this case. */ -size_t Utf8Encoder::getLength(const char* input, bool &ascii) +std::pair StatelessUtf8Encoder::getLength(std::string_view input) const { - ascii = true; - size_t len = 0; - const char* ptr = input; - unsigned char inp = *ptr; - // Do away with the ascii part of the string first (this is almost // always the entire string.) - while (inp && inp < 128) - inp = *(++ptr); - len += (ptr-input); + auto it = skipAscii(input); // If we're not at the null terminator at this point, then there // were some non-ascii characters to deal with. Go to slow-mode for // the rest of the string. - if (inp) + if (it == input.end() || *it == 0) + return {it - input.begin(), true}; + + std::size_t len = it - input.begin(); + + do { - ascii = false; - while (inp) - { - // Find the translated length of this character in the - // lookup table. - len += translationArray[inp*6]; - inp = *(++ptr); - } + // Find the translated length of this character in the + // lookup table. + len += mTranslationArray[static_cast(*it) * 6]; + ++it; } - return len; + while (it != input.end() && *it != 0); + + return {len, false}; } // Translate one character 'ch' using the translation array 'arr', and // advance the output pointer accordingly. -void Utf8Encoder::copyFromArray(unsigned char ch, char* &out) +void StatelessUtf8Encoder::copyFromArray(unsigned char ch, char* &out) const { // Optimize for ASCII values if (ch < 128) @@ -223,57 +223,58 @@ void Utf8Encoder::copyFromArray(unsigned char ch, char* &out) return; } - const signed char *in = translationArray + ch*6; + const signed char *in = &mTranslationArray[ch * 6]; int len = *(in++); - for (int i=0; i StatelessUtf8Encoder::getLengthLegacyEnc(std::string_view input) const { - ascii = true; - size_t len = 0; - const char* ptr = input; - unsigned char inp = *ptr; - // Do away with the ascii part of the string first (this is almost // always the entire string.) - while (inp && inp < 128) - inp = *(++ptr); - len += (ptr-input); + auto it = skipAscii(input); // If we're not at the null terminator at this point, then there // were some non-ascii characters to deal with. Go to slow-mode for // the rest of the string. - if (inp) + if (it == input.end() || *it == 0) + return {it - input.begin(), true}; + + std::size_t len = it - input.begin(); + std::size_t symbolLen = 0; + + do { - ascii = false; - while(inp) + symbolLen += 1; + // Find the translated length of this character in the + // lookup table. + switch (static_cast(*it)) { - len += 1; - // Find the translated length of this character in the - // lookup table. - switch(inp) - { - case 0xe2: len -= 2; break; - case 0xc2: - case 0xcb: - case 0xc4: - case 0xc6: - case 0xc3: - case 0xd0: - case 0xd1: - case 0xd2: - case 0xc5: len -= 1; break; - } - - inp = *(++ptr); + case 0xe2: symbolLen -= 2; break; + case 0xc2: + case 0xcb: + case 0xc4: + case 0xc6: + case 0xc3: + case 0xd0: + case 0xd1: + case 0xd2: + case 0xc5: symbolLen -= 1; break; + default: + len += symbolLen; + symbolLen = 0; + break; } + + ++it; } - return len; + while (it != input.end() && *it != 0); + + return {len, false}; } -void Utf8Encoder::copyFromArray2(const char*& chp, char* &out) +void StatelessUtf8Encoder::copyFromArrayLegacyEnc(std::string_view::iterator& chp, std::string_view::iterator end, char* &out) const { unsigned char ch = *(chp++); // Optimize for ASCII values @@ -304,14 +305,21 @@ void Utf8Encoder::copyFromArray2(const char*& chp, char* &out) return; } + if (chp == end) + return; + unsigned char ch2 = *(chp++); unsigned char ch3 = '\0'; if (len == 3) + { + if (chp == end) + return; ch3 = *(chp++); + } for (int i = 128; i < 256; i++) { - unsigned char b1 = translationArray[i*6 + 1], b2 = translationArray[i*6 + 2], b3 = translationArray[i*6 + 3]; + unsigned char b1 = mTranslationArray[i*6 + 1], b2 = mTranslationArray[i*6 + 2], b3 = mTranslationArray[i*6 + 3]; if (b1 == ch && b2 == ch2 && (len != 3 || b3 == ch3)) { *(out++) = (char)i; @@ -324,7 +332,23 @@ void Utf8Encoder::copyFromArray2(const char*& chp, char* &out) *(out++) = ch; // Could not find glyph, just put whatever } -ToUTF8::FromType ToUTF8::calculateEncoding(const std::string& encodingName) +Utf8Encoder::Utf8Encoder(FromType sourceEncoding) + : mBuffer(50 * 1024, '\0') + , mImpl(sourceEncoding) +{ +} + +std::string_view Utf8Encoder::getUtf8(std::string_view input) +{ + return mImpl.getUtf8(input, BufferAllocationPolicy::UseGrowFactor, mBuffer); +} + +std::string_view Utf8Encoder::getLegacyEnc(std::string_view input) +{ + return mImpl.getLegacyEnc(input, BufferAllocationPolicy::UseGrowFactor, mBuffer); +} + +ToUTF8::FromType ToUTF8::calculateEncoding(std::string_view encodingName) { if (encodingName == "win1250") return ToUTF8::WINDOWS_1250; @@ -333,10 +357,10 @@ ToUTF8::FromType ToUTF8::calculateEncoding(const std::string& encodingName) else if (encodingName == "win1252") return ToUTF8::WINDOWS_1252; else - throw std::runtime_error(std::string("Unknown encoding '") + encodingName + std::string("', see openmw --help for available options.")); + throw std::runtime_error("Unknown encoding '" + std::string(encodingName) + "', see openmw --help for available options."); } -std::string ToUTF8::encodingUsingMessage(const std::string& encodingName) +std::string ToUTF8::encodingUsingMessage(std::string_view encodingName) { if (encodingName == "win1250") return "Using Central and Eastern European font encoding."; @@ -345,5 +369,5 @@ std::string ToUTF8::encodingUsingMessage(const std::string& encodingName) else if (encodingName == "win1252") return "Using default (English) font encoding."; else - throw std::runtime_error(std::string("Unknown encoding '") + encodingName + std::string("', see openmw --help for available options.")); + throw std::runtime_error("Unknown encoding '" + std::string(encodingName) + "', see openmw --help for available options."); } diff --git a/components/to_utf8/to_utf8.hpp b/components/to_utf8/to_utf8.hpp index 3f20a51f86..918f03aa9f 100644 --- a/components/to_utf8/to_utf8.hpp +++ b/components/to_utf8/to_utf8.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace ToUTF8 { @@ -17,38 +18,55 @@ namespace ToUTF8 CP437 // Used for fonts (*.fnt) if data files encoding is 1252. Otherwise, uses the same encoding as the data files. }; - FromType calculateEncoding(const std::string& encodingName); - std::string encodingUsingMessage(const std::string& encodingName); + enum class BufferAllocationPolicy + { + FitToRequiredSize, + UseGrowFactor, + }; + + FromType calculateEncoding(std::string_view encodingName); + std::string encodingUsingMessage(std::string_view encodingName); + + class StatelessUtf8Encoder + { + public: + explicit StatelessUtf8Encoder(FromType sourceEncoding); - // class + /// Convert to UTF8 from the previously given code page. + /// Returns a view to passed buffer that will be resized to fit output if it's too small. + std::string_view getUtf8(std::string_view input, BufferAllocationPolicy bufferAllocationPolicy, std::string& buffer) const; + + /// Convert from UTF-8 to sourceEncoding. + /// Returns a view to passed buffer that will be resized to fit output if it's too small. + std::string_view getLegacyEnc(std::string_view input, BufferAllocationPolicy bufferAllocationPolicy, std::string& buffer) const; + + private: + inline std::pair getLength(std::string_view input) const; + inline void copyFromArray(unsigned char chp, char* &out) const; + inline std::pair getLengthLegacyEnc(std::string_view input) const; + inline void copyFromArrayLegacyEnc(std::string_view::iterator& chp, std::string_view::iterator end, char* &out) const; + + const std::basic_string_view mTranslationArray; + }; class Utf8Encoder { public: - Utf8Encoder(FromType sourceEncoding); + explicit Utf8Encoder(FromType sourceEncoding); - // Convert to UTF8 from the previously given code page. - std::string getUtf8(const char *input, size_t size); - inline std::string getUtf8(const std::string &str) - { - return getUtf8(str.c_str(), str.size()); - } + /// Convert to UTF8 from the previously given code page. + /// Returns a view to internal buffer invalidate by next getUtf8 or getLegacyEnc call if input is not + /// ASCII-only string. Otherwise returns a view to the input. + std::string_view getUtf8(std::string_view input); - std::string getLegacyEnc(const char *input, size_t size); - inline std::string getLegacyEnc(const std::string &str) - { - return getLegacyEnc(str.c_str(), str.size()); - } + /// Convert from UTF-8 to sourceEncoding. + /// Returns a view to internal buffer invalidate by next getUtf8 or getLegacyEnc call if input is not + /// ASCII-only string. Otherwise returns a view to the input. + std::string_view getLegacyEnc(std::string_view input); private: - void resize(size_t size); - size_t getLength(const char* input, bool &ascii); - void copyFromArray(unsigned char chp, char* &out); - size_t getLength2(const char* input, bool &ascii); - void copyFromArray2(const char*& chp, char* &out); - - std::vector mOutput; - signed char* translationArray; + std::string mBuffer; + StatelessUtf8Encoder mImpl; }; } diff --git a/components/translation/translation.cpp b/components/translation/translation.cpp index ef0f432075..bc6ba1c44d 100644 --- a/components/translation/translation.cpp +++ b/components/translation/translation.cpp @@ -1,6 +1,6 @@ #include "translation.hpp" -#include +#include namespace Translation { @@ -32,8 +32,8 @@ namespace Translation if (dataFileCollections.getCollection (extension).doesExist (fileName)) { - boost::filesystem::ifstream stream ( - dataFileCollections.getCollection (extension).getPath (fileName)); + std::ifstream stream ( + dataFileCollections.getCollection (extension).getPath (fileName).c_str()); if (!stream.is_open()) throw std::runtime_error ("failed to open translation file: " + fileName); @@ -53,16 +53,16 @@ namespace Translation if (!line.empty()) { - line = mEncoder->getUtf8(line); + const std::string_view utf8 = mEncoder->getUtf8(line); - size_t tab_pos = line.find('\t'); - if (tab_pos != std::string::npos && tab_pos > 0 && tab_pos < line.size() - 1) + size_t tab_pos = utf8.find('\t'); + if (tab_pos != std::string::npos && tab_pos > 0 && tab_pos < utf8.size() - 1) { - std::string key = line.substr(0, tab_pos); - std::string value = line.substr(tab_pos + 1); + const std::string_view key = utf8.substr(0, tab_pos); + const std::string_view value = utf8.substr(tab_pos + 1); if (!key.empty() && !value.empty()) - container.insert(std::make_pair(key, value)); + container.emplace(key, value); } } } diff --git a/components/version/version.cpp b/components/version/version.cpp index fecbdcb3f9..32b8f44ae6 100644 --- a/components/version/version.cpp +++ b/components/version/version.cpp @@ -1,16 +1,16 @@ #include "version.hpp" -#include -#include +#include +#include namespace Version { Version getOpenmwVersion(const std::string &resourcePath) { - boost::filesystem::path path (resourcePath + "/version"); + std::filesystem::path path (resourcePath + "/version"); - boost::filesystem::ifstream stream (path); + std::ifstream stream (path); Version v; std::getline(stream, v.mVersion); diff --git a/components/vfs/archive.hpp b/components/vfs/archive.hpp index b36c7117b0..22df09e06c 100644 --- a/components/vfs/archive.hpp +++ b/components/vfs/archive.hpp @@ -3,7 +3,7 @@ #include -#include +#include namespace VFS { @@ -14,6 +14,8 @@ namespace VFS virtual ~File() {} virtual Files::IStreamPtr open() = 0; + + virtual std::string getPath() = 0; }; class Archive @@ -23,6 +25,11 @@ namespace VFS /// List all resources contained in this archive, and run the resource names through the given normalize function. virtual void listResources(std::map& out, char (*normalize_function) (char)) = 0; + + /// True if this archive contains the provided normalized file. + virtual bool contains(const std::string& file, char (*normalize_function) (char)) const = 0; + + virtual std::string getDescription() const = 0; }; } diff --git a/components/vfs/bsaarchive.cpp b/components/vfs/bsaarchive.cpp index ac65c58a1c..c0db4c071c 100644 --- a/components/vfs/bsaarchive.cpp +++ b/components/vfs/bsaarchive.cpp @@ -1,21 +1,15 @@ #include "bsaarchive.hpp" -#include + #include +#include +#include namespace VFS { BsaArchive::BsaArchive(const std::string &filename) { - Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(filename); - - if (bsaVersion == Bsa::BSAVER_COMPRESSED) { - mFile = std::make_unique(Bsa::CompressedBSAFile()); - } - else { - mFile = std::make_unique(Bsa::BSAFile()); - } - + mFile = std::make_unique(); mFile->open(filename); const Bsa::BSAFile::FileList &filelist = mFile->getList(); @@ -25,6 +19,10 @@ BsaArchive::BsaArchive(const std::string &filename) } } +BsaArchive::BsaArchive() +{ +} + BsaArchive::~BsaArchive() { } @@ -32,13 +30,30 @@ void BsaArchive::listResources(std::map &out, char (*normal { for (std::vector::iterator it = mResources.begin(); it != mResources.end(); ++it) { - std::string ent = it->mInfo->name; + std::string ent = it->mInfo->name(); std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); out[ent] = &*it; } } +bool BsaArchive::contains(const std::string& file, char (*normalize_function)(char)) const +{ + for (const auto& it : mResources) + { + std::string ent = it.mInfo->name(); + std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); + if(file == ent) + return true; + } + return false; +} + +std::string BsaArchive::getDescription() const +{ + return std::string{"BSA: "} + mFile->getFilename(); +} + // ------------------------------------------------------------------------------ BsaArchiveFile::BsaArchiveFile(const Bsa::BSAFile::FileStruct *info, Bsa::BSAFile* bsa) @@ -53,4 +68,58 @@ Files::IStreamPtr BsaArchiveFile::open() return mFile->getFile(mInfo); } +CompressedBsaArchive::CompressedBsaArchive(const std::string &filename) + : Archive() +{ + mCompressedFile = std::make_unique(); + mCompressedFile->open(filename); + + const Bsa::BSAFile::FileList &filelist = mCompressedFile->getList(); + for(Bsa::BSAFile::FileList::const_iterator it = filelist.begin();it != filelist.end();++it) + { + mCompressedResources.emplace_back(&*it, mCompressedFile.get()); + } +} + +void CompressedBsaArchive::listResources(std::map &out, char (*normalize_function)(char)) +{ + for (std::vector::iterator it = mCompressedResources.begin(); it != mCompressedResources.end(); ++it) + { + std::string ent = it->mInfo->name(); + std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); + + out[ent] = &*it; + } +} + +bool CompressedBsaArchive::contains(const std::string& file, char (*normalize_function)(char)) const +{ + for (const auto& it : mCompressedResources) + { + std::string ent = it.mInfo->name(); + std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); + if(file == ent) + return true; + } + return false; +} + +std::string CompressedBsaArchive::getDescription() const +{ + return std::string{"BSA: "} + mCompressedFile->getFilename(); +} + + +CompressedBsaArchiveFile::CompressedBsaArchiveFile(const Bsa::BSAFile::FileStruct *info, Bsa::CompressedBSAFile* bsa) + : mInfo(info) + , mCompressedFile(bsa) +{ + +} + +Files::IStreamPtr CompressedBsaArchiveFile::open() +{ + return mCompressedFile->getFile(mInfo); +} + } diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index 65a9db16c1..a52104efd7 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -4,6 +4,7 @@ #include "archive.hpp" #include +#include namespace VFS { @@ -14,21 +15,55 @@ namespace VFS Files::IStreamPtr open() override; + std::string getPath() override { return mInfo->name(); } + const Bsa::BSAFile::FileStruct* mInfo; Bsa::BSAFile* mFile; }; + class CompressedBsaArchiveFile : public File + { + public: + CompressedBsaArchiveFile(const Bsa::BSAFile::FileStruct* info, Bsa::CompressedBSAFile* bsa); + + Files::IStreamPtr open() override; + + std::string getPath() override { return mInfo->name(); } + + const Bsa::BSAFile::FileStruct* mInfo; + Bsa::CompressedBSAFile* mCompressedFile; + }; + + class BsaArchive : public Archive { public: BsaArchive(const std::string& filename); + BsaArchive(); virtual ~BsaArchive(); void listResources(std::map& out, char (*normalize_function) (char)) override; + bool contains(const std::string& file, char (*normalize_function) (char)) const override; + std::string getDescription() const override; - private: + protected: std::unique_ptr mFile; std::vector mResources; }; + + class CompressedBsaArchive : public Archive + { + public: + CompressedBsaArchive(const std::string& filename); + virtual ~CompressedBsaArchive() {} + void listResources(std::map& out, char (*normalize_function) (char)) override; + bool contains(const std::string& file, char (*normalize_function) (char)) const override; + std::string getDescription() const override; + + private: + std::unique_ptr mCompressedFile; + std::vector mCompressedResources; + }; + } #endif diff --git a/components/vfs/filesystemarchive.cpp b/components/vfs/filesystemarchive.cpp index ce4ff020ef..7b57c93944 100644 --- a/components/vfs/filesystemarchive.cpp +++ b/components/vfs/filesystemarchive.cpp @@ -1,8 +1,11 @@ #include "filesystemarchive.hpp" -#include +#include + +#include #include +#include namespace VFS { @@ -18,7 +21,7 @@ namespace VFS { if (!mBuiltIndex) { - typedef boost::filesystem::recursive_directory_iterator directory_iterator; + typedef std::filesystem::recursive_directory_iterator directory_iterator; directory_iterator end; @@ -27,32 +30,46 @@ namespace VFS if (mPath.size () > 0 && mPath [prefix - 1] != '\\' && mPath [prefix - 1] != '/') ++prefix; - for (directory_iterator i (mPath); i != end; ++i) + for (directory_iterator i (std::filesystem::u8path(mPath)); i != end; ++i) { - if(boost::filesystem::is_directory (*i)) + if(std::filesystem::is_directory (*i)) continue; - std::string proper = i->path ().string (); + auto proper = i->path ().u8string (); - FileSystemArchiveFile file(proper); + FileSystemArchiveFile file(std::string((char*)proper.c_str(), proper.size())); std::string searchable; std::transform(proper.begin() + prefix, proper.end(), std::back_inserter(searchable), normalize_function); - if (!mIndex.insert (std::make_pair (searchable, file)).second) - Log(Debug::Warning) << "Warning: found duplicate file for '" << proper << "', please check your file system for two files with the same name in different cases."; + const auto inserted = mIndex.insert(std::make_pair(searchable, file)); + if (!inserted.second) + Log(Debug::Warning) << "Warning: found duplicate file for '" << std::string((char*)proper.c_str(), proper.size()) << "', please check your file system for two files with the same name in different cases."; + else + out[inserted.first->first] = &inserted.first->second; } - mBuiltIndex = true; } - - for (index::iterator it = mIndex.begin(); it != mIndex.end(); ++it) + else { - out[it->first] = &it->second; + for (index::iterator it = mIndex.begin(); it != mIndex.end(); ++it) + { + out[it->first] = &it->second; + } } } + bool FileSystemArchive::contains(const std::string& file, char (*normalize_function)(char)) const + { + return mIndex.find(file) != mIndex.end(); + } + + std::string FileSystemArchive::getDescription() const + { + return std::string{"DIR: "} + mPath; + } + // ---------------------------------------------------------------------------------- FileSystemArchiveFile::FileSystemArchiveFile(const std::string &path) @@ -62,7 +79,7 @@ namespace VFS Files::IStreamPtr FileSystemArchiveFile::open() { - return Files::openConstrainedFileStream(mPath.c_str()); + return Files::openConstrainedFileStream(mPath); } } diff --git a/components/vfs/filesystemarchive.hpp b/components/vfs/filesystemarchive.hpp index d228ba87c5..2c50e5300b 100644 --- a/components/vfs/filesystemarchive.hpp +++ b/components/vfs/filesystemarchive.hpp @@ -3,6 +3,8 @@ #include "archive.hpp" +#include + namespace VFS { @@ -13,6 +15,8 @@ namespace VFS Files::IStreamPtr open() override; + std::string getPath() override { return mPath; } + private: std::string mPath; @@ -25,6 +29,9 @@ namespace VFS void listResources(std::map& out, char (*normalize_function) (char)) override; + bool contains(const std::string& file, char (*normalize_function) (char)) const override; + + std::string getDescription() const override; private: typedef std::map index; diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index c198823811..13fea30f59 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -1,6 +1,7 @@ #include "manager.hpp" #include +#include #include @@ -36,35 +37,30 @@ namespace VFS } - Manager::~Manager() - { - reset(); - } + Manager::~Manager() {} void Manager::reset() { mIndex.clear(); - for (std::vector::iterator it = mArchives.begin(); it != mArchives.end(); ++it) - delete *it; mArchives.clear(); } - void Manager::addArchive(Archive *archive) + void Manager::addArchive(std::unique_ptr&& archive) { - mArchives.push_back(archive); + mArchives.push_back(std::move(archive)); } void Manager::buildIndex() { mIndex.clear(); - for (std::vector::const_iterator it = mArchives.begin(); it != mArchives.end(); ++it) - (*it)->listResources(mIndex, mStrict ? &strict_normalize_char : &nonstrict_normalize_char); + for (const auto& archive : mArchives) + archive->listResources(mIndex, mStrict ? &strict_normalize_char : &nonstrict_normalize_char); } - Files::IStreamPtr Manager::get(const std::string &name) const + Files::IStreamPtr Manager::get(std::string_view name) const { - std::string normalized = name; + std::string normalized(name); normalize_path(normalized, mStrict); return getNormalized(normalized); @@ -78,22 +74,61 @@ namespace VFS return found->second->open(); } - bool Manager::exists(const std::string &name) const + bool Manager::exists(std::string_view name) const { - std::string normalized = name; + std::string normalized(name); normalize_path(normalized, mStrict); return mIndex.find(normalized) != mIndex.end(); } - const std::map& Manager::getIndex() const + std::string Manager::normalizeFilename(std::string_view name) const + { + std::string result(name); + normalize_path(result, mStrict); + return result; + } + + std::string Manager::getArchive(std::string_view name) const + { + std::string normalized(name); + normalize_path(normalized, mStrict); + for(auto it = mArchives.rbegin(); it != mArchives.rend(); ++it) + { + if((*it)->contains(normalized, mStrict ? &strict_normalize_char : &nonstrict_normalize_char)) + return (*it)->getDescription(); + } + return {}; + } + + std::string Manager::getAbsoluteFileName(std::string_view name) const { - return mIndex; + std::string normalized(name); + normalize_path(normalized, mStrict); + + std::map::const_iterator found = mIndex.find(normalized); + if (found == mIndex.end()) + throw std::runtime_error("Resource '" + normalized + "' not found"); + return found->second->getPath(); } - void Manager::normalizeFilename(std::string &name) const + namespace { - normalize_path(name, mStrict); + bool startsWith(std::string_view text, std::string_view start) + { + return text.rfind(start, 0) == 0; + } } + Manager::RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(std::string_view path) const + { + if (path.empty()) + return { mIndex.begin(), mIndex.end() }; + auto normalized = normalizeFilename(path); + const auto it = mIndex.lower_bound(normalized); + if (it == mIndex.end() || !startsWith(it->first, normalized)) + return { it, it }; + ++normalized.back(); + return { it, mIndex.lower_bound(normalized) }; + } } diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index c5f0a8fec3..ab42a643a5 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -1,10 +1,12 @@ #ifndef OPENMW_COMPONENTS_RESOURCEMANAGER_H #define OPENMW_COMPONENTS_RESOURCEMANAGER_H -#include +#include #include #include +#include +#include namespace VFS { @@ -12,6 +14,19 @@ namespace VFS class Archive; class File; + template + class IteratorPair + { + public: + IteratorPair(Iterator first, Iterator last) : mFirst(first), mLast(last) {} + Iterator begin() const { return mFirst; } + Iterator end() const { return mLast; } + + private: + Iterator mFirst; + Iterator mLast; + }; + /// @brief The main class responsible for loading files from a virtual file system. /// @par Various archive types (e.g. directories on the filesystem, or compressed archives) /// can be registered, and will be merged into a single file tree. If the same filename is @@ -19,6 +34,21 @@ namespace VFS /// @par Most of the methods in this class are considered thread-safe, see each method documentation for details. class Manager { + class RecursiveDirectoryIterator + { + public: + RecursiveDirectoryIterator(std::map::const_iterator it) : mIt(it) {} + const std::string& operator*() const { return mIt->first; } + const std::string* operator->() const { return &mIt->first; } + bool operator!=(const RecursiveDirectoryIterator& other) { return mIt != other.mIt; } + RecursiveDirectoryIterator& operator++() { ++mIt; return *this; } + + private: + std::map::const_iterator mIt; + }; + + using RecursiveDirectoryRange = IteratorPair; + public: /// @param strict Use strict path handling? If enabled, no case folding will /// be done, but slash/backslash conversions are always done. @@ -30,38 +60,46 @@ namespace VFS void reset(); /// Register the given archive. All files contained in it will be added to the index on the next buildIndex() call. - /// @note Takes ownership of the given pointer. - void addArchive(Archive* archive); + void addArchive(std::unique_ptr&& archive); /// Build the file index. Should be called when all archives have been registered. void buildIndex(); /// Does a file with this name exist? /// @note May be called from any thread once the index has been built. - bool exists(const std::string& name) const; - - /// Get a complete list of files from all archives - /// @note May be called from any thread once the index has been built. - const std::map& getIndex() const; + bool exists(std::string_view name) const; /// Normalize the given filename, making slashes/backslashes consistent, and lower-casing if mStrict is false. /// @note May be called from any thread once the index has been built. - void normalizeFilename(std::string& name) const; + [[nodiscard]] std::string normalizeFilename(std::string_view name) const; /// Retrieve a file by name. /// @note Throws an exception if the file can not be found. /// @note May be called from any thread once the index has been built. - Files::IStreamPtr get(const std::string& name) const; + Files::IStreamPtr get(std::string_view name) const; /// Retrieve a file by name (name is already normalized). /// @note Throws an exception if the file can not be found. /// @note May be called from any thread once the index has been built. Files::IStreamPtr getNormalized(const std::string& normalizedName) const; + std::string getArchive(std::string_view name) const; + + /// Recursivly iterate over the elements of the given path + /// In practice it return all files of the VFS starting with the given path + /// @note the path is normalized + /// @note May be called from any thread once the index has been built. + RecursiveDirectoryRange getRecursiveDirectoryIterator(std::string_view path) const; + + /// Retrieve the absolute path to the file + /// @note Throws an exception if the file can not be found. + /// @note May be called from any thread once the index has been built. + std::string getAbsoluteFileName(std::string_view name) const; + private: bool mStrict; - std::vector mArchives; + std::vector> mArchives; std::map mIndex; }; diff --git a/components/vfs/registerarchives.cpp b/components/vfs/registerarchives.cpp index 80e639f350..25ee9e1f62 100644 --- a/components/vfs/registerarchives.cpp +++ b/components/vfs/registerarchives.cpp @@ -1,7 +1,8 @@ #include "registerarchives.hpp" #include -#include +#include +#include #include @@ -23,27 +24,30 @@ namespace VFS // Last BSA has the highest priority const std::string archivePath = collections.getPath(*archive).string(); Log(Debug::Info) << "Adding BSA archive " << archivePath; + Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(archivePath); - vfs->addArchive(new BsaArchive(archivePath)); + if (bsaVersion == Bsa::BSAVER_COMPRESSED) + vfs->addArchive(std::make_unique(archivePath)); + else + vfs->addArchive(std::make_unique(archivePath)); } else { - std::stringstream message; - message << "Archive '" << *archive << "' not found"; - throw std::runtime_error(message.str()); + throw std::runtime_error("Archive '" + *archive + "' not found"); } } if (useLooseFiles) { - std::set seen; + std::set seen; for (Files::PathContainer::const_iterator iter = dataDirs.begin(); iter != dataDirs.end(); ++iter) { - if (seen.insert(*iter).second) + // TODO(jvoisin) Get rid of `->native()` when we move PathContainer from boost::filesystem to std::filesystem. + if (seen.insert(iter->native()).second) { Log(Debug::Info) << "Adding data directory " << iter->string(); // Last data dir has the highest priority - vfs->addArchive(new FileSystemArchive(iter->string())); + vfs->addArchive(std::make_unique(iter->string())); } else Log(Debug::Info) << "Ignoring duplicate data directory " << iter->string(); diff --git a/components/widgets/box.cpp b/components/widgets/box.cpp index 1db1933202..f9a2e26142 100644 --- a/components/widgets/box.cpp +++ b/components/widgets/box.cpp @@ -1,14 +1,33 @@ #include "box.hpp" #include +#include namespace Gui { + // TODO: Since 3.4.2 MyGUI is supposed to automatically translate tags + // If the 3.4.2 become a required minimum version, the ComboBox class may be removed. + void ComboBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { +#if MYGUI_VERSION >= MYGUI_DEFINE_VERSION(3,4,2) + MyGUI::ComboBox::setPropertyOverride (_key, _value); +#else + if (_key == "AddItem") + { + const std::string value = MyGUI::LanguageManager::getInstance().replaceTags(_value); + MyGUI::ComboBox::setPropertyOverride (_key, value); + } + else + { + MyGUI::ComboBox::setPropertyOverride (_key, _value); + } +#endif + } void AutoSizedWidget::notifySizeChange (MyGUI::Widget* w) { MyGUI::Widget * parent = w->getParent(); - if (parent != 0) + if (parent != nullptr) { if (mExpandDirection.isLeft()) { @@ -17,7 +36,7 @@ namespace Gui } w->setSize(getRequestedSize ()); - while (parent != 0) + while (parent != nullptr) { Box * b = dynamic_cast(parent); if (b) @@ -32,7 +51,7 @@ namespace Gui MyGUI::IntSize AutoSizedTextBox::getRequestedSize() { - return getTextSize(); + return getCaption().empty() ? MyGUI::IntSize{0, 0} : getTextSize(); } void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) @@ -280,7 +299,7 @@ namespace Gui void HBox::initialiseOverride() { Base::initialiseOverride(); - MyGUI::Widget* client = 0; + MyGUI::Widget* client = nullptr; assignWidget(client, "Client"); setWidgetClient(client); } @@ -435,7 +454,7 @@ namespace Gui void VBox::initialiseOverride() { Base::initialiseOverride(); - MyGUI::Widget* client = 0; + MyGUI::Widget* client = nullptr; assignWidget(client, "Client"); setWidgetClient(client); } diff --git a/components/widgets/box.hpp b/components/widgets/box.hpp index 60d0ea67a4..1b1b71f156 100644 --- a/components/widgets/box.hpp +++ b/components/widgets/box.hpp @@ -4,13 +4,21 @@ #include #include #include -#include #include +#include #include "fontwrapper.hpp" namespace Gui { + class ComboBox : public FontWrapper + { + MYGUI_RTTI_DERIVED( ComboBox ) + + protected: + void setPropertyOverride(const std::string& _key, const std::string& _value) override; + }; + class Button : public FontWrapper { MYGUI_RTTI_DERIVED( Button ) diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp index daa69f9202..16ebba3587 100644 --- a/components/widgets/fontwrapper.hpp +++ b/components/widgets/fontwrapper.hpp @@ -31,15 +31,11 @@ namespace Gui } private: - static int clamp(const int& value, const int& lowBound, const int& highBound) - { - return std::min(std::max(lowBound, value), highBound); - } std::string getFontSize() { // Note: we can not use the FontLoader here, so there is a code duplication a bit. - static const std::string fontSize = std::to_string(clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); + static const std::string fontSize = std::to_string(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); return fontSize; } }; diff --git a/components/widgets/imagebutton.cpp b/components/widgets/imagebutton.cpp index 0d1f798da5..6d71588830 100644 --- a/components/widgets/imagebutton.cpp +++ b/components/widgets/imagebutton.cpp @@ -1,5 +1,7 @@ #include "imagebutton.hpp" +#include + #include #include @@ -88,13 +90,17 @@ namespace Gui if (!mUseWholeTexture) { - int scale = 1.f; + float scale = 1.f; MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(textureName); if (texture && getHeight() != 0) - scale = texture->getHeight() / getHeight(); - - setImageTile(MyGUI::IntSize(mTextureRect.width * scale, mTextureRect.height * scale)); - MyGUI::IntCoord scaledSize(mTextureRect.left * scale, mTextureRect.top * scale, mTextureRect.width * scale, mTextureRect.height * scale); + scale = static_cast(texture->getHeight()) / getHeight(); + + const int width = static_cast(std::round(mTextureRect.width * scale)); + const int height = static_cast(std::round(mTextureRect.height * scale)); + setImageTile(MyGUI::IntSize(width, height)); + MyGUI::IntCoord scaledSize(static_cast(std::round(mTextureRect.left * scale)), + static_cast(std::round(mTextureRect.top * scale)), + width, height); setImageCoord(scaledSize); } @@ -118,7 +124,7 @@ namespace Gui void ImageButton::setImage(const std::string &image) { - size_t extpos = image.find_last_of("."); + size_t extpos = image.find_last_of('.'); std::string imageNoExt = image.substr(0, extpos); std::string ext = image.substr(extpos); diff --git a/components/widgets/list.cpp b/components/widgets/list.cpp index bf24acf7e1..e961b04cb0 100644 --- a/components/widgets/list.cpp +++ b/components/widgets/list.cpp @@ -7,9 +7,9 @@ namespace Gui { - MWList::MWList() : - mScrollView(0) - ,mClient(0) + MWList::MWList() + : mScrollView(nullptr) + , mClient(nullptr) , mItemHeight(0) { } @@ -19,7 +19,7 @@ namespace Gui Base::initialiseOverride(); assignWidget(mClient, "Client"); - if (mClient == 0) + if (mClient == nullptr) mClient = this; mScrollView = mClient->createWidgetReal( @@ -115,7 +115,7 @@ namespace Gui unsigned int MWList::getItemCount() { - return mItems.size(); + return static_cast(mItems.size()); } std::string MWList::getItemNameAt(unsigned int at) diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp index e8ba226f70..c6ff9628ee 100644 --- a/components/widgets/numericeditbox.cpp +++ b/components/widgets/numericeditbox.cpp @@ -31,7 +31,7 @@ namespace Gui try { mValue = std::stoi(newCaption); - int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + int capped = std::clamp(mValue, mMinValue, mMaxValue); if (capped != mValue) { mValue = capped; diff --git a/components/widgets/sharedstatebutton.cpp b/components/widgets/sharedstatebutton.cpp index f4456275bd..91436a1b96 100644 --- a/components/widgets/sharedstatebutton.cpp +++ b/components/widgets/sharedstatebutton.cpp @@ -18,7 +18,7 @@ namespace Gui } } - void SharedStateButton::shareStateWith(ButtonGroup shared) + void SharedStateButton::shareStateWith(const ButtonGroup &shared) { mSharedWith = shared; } diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp index 42c6424b2c..d643bf2430 100644 --- a/components/widgets/sharedstatebutton.hpp +++ b/components/widgets/sharedstatebutton.hpp @@ -34,7 +34,7 @@ namespace Gui bool _setState(const std::string &_value); public: - void shareStateWith(ButtonGroup shared); + void shareStateWith(const ButtonGroup &shared); /// @note The ButtonGroup connection will be destroyed when any widget in the group gets destroyed. static void createButtonGroup(ButtonGroup group); diff --git a/components/widgets/widgets.cpp b/components/widgets/widgets.cpp index c1a9a50531..a5ff0b677d 100644 --- a/components/widgets/widgets.cpp +++ b/components/widgets/widgets.cpp @@ -28,6 +28,7 @@ namespace Gui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } } diff --git a/components/windows.hpp b/components/windows.hpp new file mode 100644 index 0000000000..003c7bcaae --- /dev/null +++ b/components/windows.hpp @@ -0,0 +1,9 @@ +#ifndef OPENMW_COMPONENTS_WINDOWS_H +#define OPENMW_COMPONENTS_WINDOWS_H + +#include + +#undef far +#undef near + +#endif diff --git a/docker/Dockerfile.ubuntu b/docker/Dockerfile.ubuntu new file mode 100644 index 0000000000..b5fadff3e3 --- /dev/null +++ b/docker/Dockerfile.ubuntu @@ -0,0 +1,23 @@ +FROM ubuntu +LABEL maintainer="Wassim DHIF " + +ENV NPROC=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends software-properties-common apt-utils \ + && add-apt-repository ppa:openmw/openmw \ + && apt-get update \ + && apt-get install -y --no-install-recommends openmw openmw-launcher \ + && apt-get install -y --no-install-recommends git build-essential cmake \ + libopenal-dev libopenscenegraph-dev libbullet-dev libsdl2-dev \ + libmygui-dev libunshield-dev liblz4-dev libtinyxml-dev libqt5opengl5-dev \ + libboost-filesystem-dev libboost-program-options-dev libboost-iostreams-dev \ + libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev \ + librecast-dev libsqlite3-dev libluajit-5.1-dev + +COPY build.sh /build.sh + +RUN mkdir /openmw +WORKDIR /openmw + +ENTRYPOINT ["/build.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000000..4c4131c235 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,17 @@ +# Build OpenMW using Docker + +## Build Docker image + +Replace `LINUX_VERSION` with the Linux distribution you wish to use. +``` +docker build -f Dockerfile.LINUX_VERSION -t openmw.LINUX_VERSION . +``` + +## Build OpenMW using Docker + +Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. +Without a label, the security system might prevent the processes running inside the container from using the content. +The Z option tells Docker to label the content with a private unshared label. +``` +docker run -v /path/to/openmw:/openmw:Z -e NPROC=2 -it openmw.LINUX_VERSION +``` diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000000..0f79161379 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -xe + +# Creating build directory... +mkdir -p build +cd build + +# Running CMake... +cmake ../ + +# Building with $NPROC CPU... +make -j $NPROC diff --git a/docs/requirements.txt b/docs/requirements.txt index 288d462d0d..ac82149f5d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ parse_cmake -sphinx>=1.7.0 +sphinx==1.8.5 +docutils==0.17.1 diff --git a/docs/source/_static/luadoc.css b/docs/source/_static/luadoc.css new file mode 100644 index 0000000000..aa83013def --- /dev/null +++ b/docs/source/_static/luadoc.css @@ -0,0 +1,113 @@ +#luadoc tt { font-family: monospace; } + +#luadoc p, +#luadoc td, +#luadoc th { font-size: .95em; line-height: 1.2em;} + +#luadoc p, +#luadoc ul +{ margin: 10px 0 0 10px;} + +#luadoc strong { font-weight: bold;} + +#luadoc em { font-style: italic;} + +#luadoc h1 { + font-size: 1.5em; + margin: 25px 0 20px 0; +} +#luadoc h2, +#luadoc h3, +#luadoc h4 { margin: 15px 0 10px 0; } +#luadoc h2 { font-size: 1.25em; } +#luadoc h3 { font-size: 1.15em; } +#luadoc h4 { font-size: 1.06em; } + +#luadoc hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +#luadoc blockquote { margin-left: 3em; } + +#luadoc ul { list-style-type: disc; } + +#luadoc p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +#luadoc p:first-child { + margin-top: 0px; +} + +#luadoc table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +#luadoc table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} + +#luadoc table.function_list td.name { background-color: #f0f0f0; } +#luadoc table.function_list td.summary { width: 100%; } + +#luadoc dl.table dt, +#luadoc dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +#luadoc dl.table dd, +#luadoc dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +#luadoc dl.table h3, +#luadoc dl.function h3 {font-size: .95em;} + + + +#luadoc pre.example { + background-color: #eeffcc; + border: 1px solid #e1e4e5; + padding: 10px; + margin: 10px 0 10px 0; + overflow-x: auto; +} + +#luadoc code { + background-color: inherit; + color: inherit; + border: none; + font-family: monospace; +} + +#luadoc pre.example code { + color: #404040; + background-color: #eeffcc; + border: none; + white-space: pre; + padding: 0px; +} + +#luadoc dt { + background: inherit; + color: inherit; + width: 100%; + padding: 0px; +} + +#luadoc a:not(:link) { + font-weight: bold; + color: #000; + text-decoration: none; + cursor: inherit; +} +#luadoc a:link { font-weight: bold; color: #004080; text-decoration: none; } +#luadoc a:visited { font-weight: bold; color: #006699; text-decoration: none; } +#luadoc a:link:hover { text-decoration: underline; } + +#luadoc dl, +#luadoc dd {margin: 0px; line-height: 1.2em;} +#luadoc li {list-style: bullet;} + diff --git a/docs/source/conf.py b/docs/source/conf.py index 7653b94edf..7f2affbb75 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,6 +13,7 @@ # serve to show the default. import os import sys +import subprocess # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -148,7 +149,11 @@ html_theme = 'sphinx_rtd_theme' def setup(app): app.add_stylesheet('figures.css') - + app.add_stylesheet('luadoc.css') + try: + subprocess.call(project_root + '/docs/source/generate_luadoc.sh') + except Exception as e: + print('Can\'t generate Lua API documentation:', e) # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh new file mode 100755 index 0000000000..f71e3110cb --- /dev/null +++ b/docs/source/generate_luadoc.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# How to install openmwluadocumentor: + +# sudo apt install luarocks +# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git +# cd openmw-luadocumentor/luarocks +# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec +# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock + +# How to install on Windows: + +# install LuaRocks (heavily recommended to use the standalone package) +# https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows +# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git +# cd openmw-luadocumentor/luarocks +# open "Developer Command Prompt for VS <2017/2019>" in this directory and run: +# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec +# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +# open "Git Bash" in the same directory and run script: +# ./generate_luadoc.sh + +if [ -f /.dockerenv ]; then + # We are inside readthedocs pipeline + echo "Install lua 5.1" + cd ~ + curl -R -O https://www.lua.org/ftp/lua-5.1.5.tar.gz + tar -zxf lua-5.1.5.tar.gz + cd lua-5.1.5/ + make linux + PATH=$PATH:~/lua-5.1.5/src + + echo "Install luarocks" + cd ~ + wget https://luarocks.org/releases/luarocks-2.4.2.tar.gz + tar zxpf luarocks-2.4.2.tar.gz + cd luarocks-2.4.2/ + ./configure --with-lua-bin=$HOME/lua-5.1.5/src --with-lua-include=$HOME/lua-5.1.5/src --prefix=$HOME/luarocks + make build + make install + PATH=$PATH:~/luarocks/bin + + echo "Install openmwluadocumentor" + cd ~ + git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git + cd openmw-luadocumentor/luarocks + luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec + luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +fi + +DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +FILES_DIR=$DOCS_SOURCE_DIR/../../files +OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html +DOCUMENTOR_PATH=~/.luarocks/bin/openmwluadocumentor + +if [ ! -x $DOCUMENTOR_PATH ]; then + # running on Windows? + DOCUMENTOR_PATH="$APPDATA/LuaRocks/bin/openmwluadocumentor.bat" +fi + +rm -f $OUTPUT_DIR/*.html + +cd $FILES_DIR/lua_api +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua + +cd $FILES_DIR/data +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/ai.lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera/camera.lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/mwui/init.lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/settings/player.lua diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 538cfd4c6d..57460c4983 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -80,6 +80,18 @@ Afterwards, you can point OpenMW to the Steam install location at ``C:\Program Files\Steam\SteamApps\common\Morrowind\Data Files\`` and find ``Morrowind.esm`` there. +XBox Game Pass for PC +--------------------- + +Default Morrowind Game Pass files are in a restricted WindowsApps folder +that OpenMW cannot scan for content files in the usual way. +However, in the Xbox App for PC, inside the Manage game settings +(three-dots menu-> Manage), there is an option to enable advanced +management options (or similar phrasing) for Morrowind. Choose this +option and the app will prompt you to move the Morrowind files to a +new folder. Once done you can find ``Morrowind.esm`` in the folder you +chose. + macOS ----- @@ -105,10 +117,10 @@ If you are running macOS, you can also download Morrowind through Steam: #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` -Linux ----- -Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". ----- +Linux +----- +Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". +--------------------------------------------------------- #. Install Steam from "Ubuntu Software" Center #. Enable Proton (basically WINE under the hood). This is done in the Steam client menu drop down. Select, "Steam | Settings" then in the "SteamPlay" section check the box next to "enable steam play for all other titles" #. Now Morrowind should be selectable in your game list (as long as you own it). You can install it like any other game, choose to install it and remember the directory path of the location you pick. diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index fab00ab9b7..50efe7542d 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -61,3 +61,10 @@ However, it depends on several packages which are not in stable, so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. This is not recommended or supported. +The Flatpak Way +=============== + +OpenMW is available as a flatpak. With flatpak installed, run the command below. It should show up on your desktop. +:: + + # flatpak install openmw diff --git a/docs/source/manuals/openmw-cs/index.rst b/docs/source/manuals/openmw-cs/index.rst index ee82907119..5a0b9c99e0 100644 --- a/docs/source/manuals/openmw-cs/index.rst +++ b/docs/source/manuals/openmw-cs/index.rst @@ -22,5 +22,10 @@ few chapters to familiarise yourself with the new interface. files-and-directories starting-dialog tables + tables-file + tables-world + tables-mechanics + tables-characters + tables-assets record-types record-filters diff --git a/docs/source/manuals/openmw-cs/record-types.rst b/docs/source/manuals/openmw-cs/record-types.rst index 3742cc9e86..cd83555a4c 100644 --- a/docs/source/manuals/openmw-cs/record-types.rst +++ b/docs/source/manuals/openmw-cs/record-types.rst @@ -1,25 +1,20 @@ -Record Types -############ +################### +Object Record Types +################### -A game world contains many items, such as chests, weapons and monsters. All -these items are merely instances of templates we call *Objects*. The OpenMW CS -*Objects* table contains information about each of these template objects, such +A game world contains many items, such as chests, weapons and monsters. All of +these items are merely instances of templates we call Objects. The OpenMW-CS +Objects table contains information about each of these template objects, such as its value and weight in the case of items, or an aggression level in the case of NPCs. -The following is a list of all Record Types and what you can tell OpenMW CS +The following is a list of all Record Types and what you can tell OpenMW-CS about each of them. Activator Activators can have a script attached to them. As long as the cell this object is in is active the script will be run once per frame. -Potion - This is a potion which is not self-made. It has an Icon for your inventory, - weight, coin value, and an attribute called *Auto Calc* set to ``False``. - This means that the effects of this potion are pre-configured. This does not - happen when the player makes their own potion. - Apparatus This is a tool to make potions. Again there’s an icon for your inventory as well as a weight and a coin value. It also has a *Quality* value attached to @@ -54,9 +49,89 @@ Container container, however, will just refuse to take the item in question when it gets "over-encumbered". Organic Containers are containers such as plants. Containers that respawn are not safe to store stuff in. After a certain - amount of time they will reset to their default contents, meaning that + amount of time, they will reset to their default contents, meaning that everything in them is gone forever. Creature These can be monsters, animals and the like. +Creature Levelled List + A list of creatures that can periodically spawn at a particular location. + The strength, type, and number of creatures depends on the player's level. + The list accepts entries of individual Creatures or also of other Creature + Levelled Lists. Each entry has a corresponding level value that determines + the lowest player level the creature can be spawned at. + + When ``Calculate all levels <= player`` is enabled, all the creatures with + a level value lower or equal to the player can be spawned. Otherwise, + the creature levelled list will only resolve a creature that matches + the player's level + + ``Chance None`` is the percentage of possibility that no creatures will spawn. + +Door + Objects in the environment that can be opened or closed when activated. Can + also be locked and trapped. + +Ingredient + Objects required to create potions in an apparatus. Each ingredient can hold + up to four alchemical effects. + +Item Levelled List + A list of items that can spawn in a container when the player opens it. + The quality, type, and number of spawned items depends on the player's level. + The list accepts entries of individual items or also of other Item + Levelled Lists. Each entry has a corresponding level value that determines + the lowest player level the item can be spawned at. + + When ``Calculate all levels <= player`` is enabled, all the items with + a level value lower or equal to the player can be spawned. Otherwise, + the item levelled list will only resolve an item that matches the + player's level. + + ``Chance None`` is the percentage of possibility that no creatures will spawn. + +Light + An object that illuminates the environment around itself, depending on the + light's strength, range, and colour. Can be a static object in the environment, + or when configured, can be picked up and carried by NPCs. + +Lockpick + Tool required to open locks without having the proper key or using a spell. + Locks are found on various in-game objects, usually doors, cabinets, drawers, + chests, and so on. + +Miscellaneous + This is a category of objects with various in-game functions. + + * Soul Gems, used to hold trapped souls and enchant items. + * Gold piles that add their value directly to the player's gold amount when picked up. + * Keys to open locked doors. + * Certain quest items. + * Environment props such as plates, bowls, pots, tankards, and so on. Unlike environment objects of the Static type, these can be picked up. + +NPC + Player character and non-player characters. Their attributes and skills + follow the game's character system, and they are made from multiple body parts. + +Potion + This is a potion which is not self-made. It has an Icon for your inventory, + weight, coin value, and an attribute called *Auto Calc* set to ``False``. + This means that the effects of this potion are pre-configured. This does not + happen when the player makes their own potion. + +Probe + Tool required to disarm trapped doors, chests, or any other object that has + a trap assigned. + +Repair + Tool required by the player to repair damaged objects of Armor and Weapon types. + +Static + Objects from which the world is made. Walls, furniture, foliage, statues, + signs, rocks and rock formations, etc. + +Weapon + An object which the player or NPCs can carry and use it to deal damage + to their opponents in combat. Swords, spears, axes, maces, staves, bows, + crossbows, ammunition, and so on. diff --git a/docs/source/manuals/openmw-cs/tables-assets.rst b/docs/source/manuals/openmw-cs/tables-assets.rst new file mode 100644 index 0000000000..e06213ea60 --- /dev/null +++ b/docs/source/manuals/openmw-cs/tables-assets.rst @@ -0,0 +1,108 @@ +############# +Assets Tables +############# + + +Sounds +****** + +Sounds are Sound Files wrapped by OpenMW, allowing you to adjust how they behave +once they are played in-game. They are used for many audio events including +spells, NPCs commenting, environment ambients, doors, UI events, when moving items +in the inventory, and so on. + +Volume + More is louder, less is quieter. Maximum is 255. +Min Range + When the player is within Min Range distance from the Sound, the Volume will + always be at its maximum defined value. +Max Range + When the player is beyond Max Range distance from the Sound, the volume will + fade to 0. +Sound File + Source sound file on the hard-drive that holds all the actual audio information. + All available records are visible in the Sound Files table. + + +Sound Generators +**************** + +Sound generators are a way to play Sounds driven by animation events. Animated +creatures or NPCs always require to be paired with a corresponding textkeys file. +This textkeys file defines the extents of each animation, and relevant in this +case, events and triggers occuring at particular animation frames. + +For example, a typical textkey entry intended for a Sound Generator would be +named `SoundGen: Left` and be hand placed by an animator whenever the left leg +of the creature touches the ground. In OpenMW-CS, the appropriate Sound +Generator of an appropriate type would then be connected to the animated creature. +In this case the type would be `Left Foot`. Once in-game, OpenMW will play the +sound whenever its textkeys occurs in the currently playing animation. + +Creature + Which creature uses this effect. +Sound + Which record of the Sound type is used as the audio source +Sound Generator Type + Type of the sound generator that is matched to corresponding textkeys. + + * Land + * Left Foot + * Moan + * Right Foot + * Roar + * Scream + * Swim Left + * Swim Right + + +Meshes +****** + +Meshes are 3D assets that need to be assigned to objects to render them in the +game world. Entries in this table are based on contents of ``data/meshes`` +folder and cannot be edited from OpenMW-CS. + + +Icons +***** + +Icons are images used in the user interface to represent inventory items, +spells, and attributes. They can be assigned to relevant records through other +dialogues in the editor. Entries in this table are based on contents of +``data/icons`` folder and cannot be edited from OpenMW-CS. + + +Music Files +*********** + +Music is played in the background during the game and at special events such as +intro, death, or level up. Entries in this table are based on contents of +``data/music`` folder and cannot be edited from OpenMW-CS. + + +Sound Files +*********** + +Sound files are the source audio files on the hard-drive and are used by other +records related to sound. Entries in this table are based on contents of +``data/sounds`` folder and cannot be edited from OpenMW-CS. + + +Textures +******** + +Textures are images used by 3D objects, particle effects, weather system, user +interface and more. Definitions which mesh uses which texture are included in +the mesh files and cannot be assigned through the editor. To use a texture to +paint the terrain, a separate entry is needed in the Land Textures table. +Entries in this table are based on contents of ``data/textures`` folder and +cannot be edited from OpenMW-CS. + + +Videos +****** + +Videos can be shown at various points in the game, depending on where they are +called. Entries in this table are based on contents of ``data/videos`` folder +and cannot be edited from OpenMW-CS. diff --git a/docs/source/manuals/openmw-cs/tables-characters.rst b/docs/source/manuals/openmw-cs/tables-characters.rst new file mode 100644 index 0000000000..9d6f290937 --- /dev/null +++ b/docs/source/manuals/openmw-cs/tables-characters.rst @@ -0,0 +1,300 @@ +################# +Characters Tables +################# + +These tables deal with records that make up the player and NPCs. Together we'll +refer to them as characters. + + +Skills +****** + +Characters in OpenMW can perform various actions and skills define how successful +a character is in performing these actions. The skills themselves are hardcoded +and cannot be added through the editor but can have some of their parameters +adjusted. + +Attribute + Each skill is governed by an attribute. The value of the attribute will + contribute or hinder your success in using a skill. + +Specialization + A skill can belong to combat, magic, or stealth category. If the player + specializes in the same category, they get a +5 starting bonus to the skill. + +Use Value 1, 2, 3, 4 + Use Values are the amount of experience points the player will be awarded + for successful use of this skill. Skills can have one or multiple in-game + actions associated with them and each action is tied to one of the Use Values. + For example, Block skill has Use Value 1 = 2,5. This means that on each + successful blocked attack, the player's block skill will increase by 2,5 + experience points. Athletics have Use Value 1 = 0,02, and Use Value 2 = 0,03. + For each second of running the player's athletics skill will be raised by 0,02 + experience points, while for each second of swimming by 0,03 points. Note that not + all of the skills use all of the Use Values and OpenMW-CS currently doesn't tell + what action a skill's Use Value is associated with. + +Description + Flavour text that appears in the character information window. + + +Classes +******* + +All characters in OpenMW need to be of a certain class. This table list existing +classes and allows you to create, delete or modify them. + +Name + How this class is called. + +Attribute 1 and 2 + Characters of this class receive a +10 starting bonus on the two chosen + atributes. + +Specialization + Characters can specialize in combat, magic, or stealth. Each skill that + belongs to the chosen specialization receives a +5 starting bonus and is + easier to train. + +Major Skill 1 to 5 + Training these skills contributes to the player leveling up. Each of these + skills receives a +25 starting bonus and is easier to train. + +Minor Skill 1 to 5 + Training these skills contributes to the player leveling up. Each of these + skills receives a +10 starting bonus and is easier to train. + +Playable + When enabled, the player can choose to play as this class at character creation. + If disabled, only NPCs can be of this class. + +Description + Flavour text that appears in the character information window. + + +Faction +******* + +Characters in OpenMW can belong to different factions that represent interest +groups within the game's society. The most obvious example of factions are +guilds which the player can join, perform quests for and rise through its ranks. +Membership in factions can be a good source of interesting situations and quests. +If nothing else, membership affects NPC disposition towards the player. + +Name + How this faction is called. + +Attribute 1 and 2 + Two attributes valued by the faction and required to rise through its ranks. + +Hidden + When enabled, the player's membership in this faction will not be displayed + in the character window. + +Skill 1 to 7 + Skills valued by the faction and required to rise through its ranks. Not all + of the skill slots need to be filled. + +Reactions + Change of NPC disposition towards the player when joining this faction. + Disposition change depends on which factions the NPCs belong to. + +Ranks + Positions in the hierarchy of this faction. Every rank has a requirement in + attributes and skills before the player is promoted. Every rank gives the + player an NPC disposition bonus within the faction. + + +Races +***** + +All characters in OpenMW need to be of a certain race. This table list existing +races and allows you to create, delete or modify them. After a race is created, +it can be assigned to an NPC in the objects table. Race affects character's +starting attributes and skill bonuses, appearance, and voice lines. In addition, +a race assigned to the player can affect NPC disposition, quests, dialogues and +other things. + +Name + How this race is called. + +Description + Flavour text that appears in the character information window. + +Playable + When enabled, the player can choose to play as this race. If disabled, only + the NPCs can belong to it. + +Beast Race + Use models and animations for humanoids of different proportions and + movement styles. In addition, beast races can't wear closed helmets + or boots. + +Male Weight + Scaling factor of the default body type. Values above 1.0 will make members + of this race wider. Values below 1.0 will make them thinner. Applies to males. + +Male Height + Scaling factor of the default body type. Values above 1.0 will make members + of this race taller. Values below 1.0 will make them shorter. Applies to males. + +Female Weight + Scaling factor of the default body type. Values above 1.0 will make members + of this race wider. Values below 1.0 will make them thinner. Applies to females. + +Female Height + Scaling factor of the default body type. Values above 1.0 will make members + of this race taller. Values below 1.0 will make them shorter. Applies to females. + + +Birthsigns +********** + +Birthsigns permanently modify player's attributes, skills, or other abilities. +The player can have a single birthsign which is usually picked during character creation. +Modifications to the player are done through one or more spells added +to a birthsign. Spells with constant effects modify skills and attributes. +Spells that are cast are given to the player in the form of powers. + +Name + Name of the birthsign that will be displayed in the interface. + +Texture + An image that will be displayed in the birthsigns selection window. + +Description + Flavour text about the birthsign. + +Powers + A list of spells that are given to the player. When spells are added by a + birthsign, they cannot be removed in-game by the player. + + +Body Parts +********** + +Characters are made from separate parts. Together they form the whole body. +Allows customization of the outer look. Includes heads, arms, legs, torso, hand, +armor, pauldrons, chestpiece, helmets, wearables, etc. + + +Topics +****** + +Topics are, in a broader meaning, dialogue triggers. They can take the form of +clickable keywords in the dialogue, combat events, persuasion events, and other. +What response they produce and under what conditions is then defined by topic infos. +A single topic can be used by unlimited number of topic infos. There are four +different types of topics. + +Greeting 0 to 9 + Initial text that appears in the dialogue window when talking to an NPC. Hardcoded. + +Persuasion + Persuasion entries produce a dialogue response when using persuasion actions on NPCs. Hardcoded. + + * Admire, Bribe, Intimidate Taunt Fail - Conversation text that appears when the player fails a persuasion action. + * Admire, Bribe, Intimidate Taunt Succeed - Conversation text that appears when the player succeeds a persuasion action. + * Info Refusal - Conversation text that appears when the player wishes to talk about a certain topic and the conditions are not met. For example, NPC disposition is too low, the player is not a faction member, etc. + * Service Refusal - Conversation text that appears when the player wishes a service from the NPC but the conditions are not met. + +Topic + A keyword in the dialogue window that leads to further dialogue text, + similar to wiki links. These are the foundation to create dialogues from. + Entires can be freely added, edited, or removed. + +Voice + Voice entries are specific in-game events used to play a sound. Hardcoded. + + * Alarm - NPC enters combat state + * Attack - NPC performs an attack + * Flee - NPC loses their motivation to fight + * Hello - NPC addressing the player when near enough + * Hit - When NPCs are hit and injured. + * Idle - Random things NPCs say. + * Intruder + * Thief - When an NPC detects the player steal something. + + +Topic Infos +*********** + +Topic infos take topics as their triggers and define responses. Through their +many parameters they can be assigned to one or more NPCs. Important to note is +that each topic info can take a combination of parameters to accurately define +which NPCs will produce a particular response. These parameters are as follow. + +Actor + A specific NPC. + +Race + All members of a race. + +Class + NPCs of a chosen class. + +Faction + NPCs belonging to a faction. + +Cell + NPCs located in a particular cell. + +Disposition + NPC disposition towards the player. This is the 0-100 bar visible in the + conversation window and tells how much an NPC likes the player. + +Rank + NPC rank within a faction. + +Gender + NPC gender. + +PC Faction + Player's faction. + +PC Rank + Player's rank within a faction. + +Topic infos when triggered provide a response when the correct conditions are met. + +Sound File + Sound file to play when the topic info is triggered + +Response + Dialogue text that appears in a dialogue window when clicking on a keyword. + Dialogue text that appears near the bottom of the screen when a voice topic + is triggered. + +Script + Script to define further effects or branching dialogue choices when this + topic info is triggered. + +Info Conditions. + Conditions required for this topic info to be active. + + +Journals +******** + +Journals are records that define questlines. Entries can be added or removed. +When adding a new entry, you give it a unique ID which cannot be edited afterwards. +Also to remember is that journal IDs are not the actual keywords appearing in +the in-game journal. + + +Journal Infos +************* + +Journal infos are stages of a particular quest. Entries appear in the player's +journal once they are called by a script. The script can be a standalone record +or a part of a topic info. The current command is ``Journal, "Journal ID", "Quest Index"`` + +Quest Status + Finished, Name, None, Restart. No need to use them. + +Quest Index + A quest can write multiple entries into the player's journal and each of + these entries is identified by its index. + +Quest Description + Text that appears in the journal for this particular stage of the quest. diff --git a/docs/source/manuals/openmw-cs/tables-file.rst b/docs/source/manuals/openmw-cs/tables-file.rst new file mode 100644 index 0000000000..f6b5be0bfd --- /dev/null +++ b/docs/source/manuals/openmw-cs/tables-file.rst @@ -0,0 +1,37 @@ +########### +File Tables +########### + +Tables found in the File menu. + + +Verification Results +******************** + +This table shows reports created by the verify command found in the file menu. +Verify will go over the whole project and output errors / warnings when records +don't conform to the requirements of the engine. The offending records can be +accessed directly from the verification results table. The table will not update +on its own once an error / warning is fixed. Instead, use the *Refresh* option +found in the right click menu in this table. + +Type + The type of record that is causing the error / warning. + +ID + ID value of the offending record. + +Severity + Whether the entry is an error or merely a warning. + The game can still run even if not all errors are fixed. + +Description + Information on what exactly is causing the error / warning. + + +Error Log +********* + +The Error Log table shows any errors that occured while loading the game files +into OpenMW-CS. These are the files that come in ``.omwgame``, ``.omwaddon``, +``.esm``, and ``.esp`` formats. diff --git a/docs/source/manuals/openmw-cs/tables-mechanics.rst b/docs/source/manuals/openmw-cs/tables-mechanics.rst new file mode 100644 index 0000000000..8438be69b6 --- /dev/null +++ b/docs/source/manuals/openmw-cs/tables-mechanics.rst @@ -0,0 +1,176 @@ +################ +Mechanics Tables +################ + +Tables that belong into the mechanics category. + + +Scripts +******* + +Scripts are useful to expand the base functionality offered by the engine +or to create complex quest lines. Entries can be freely added or removed +through OpenMW-CS. When creating a new script, it can be defined as local +or global. + + +Start Scripts +************* + +A list of scripts that are automatically started as global scripts on game startup. +The script ``main`` is an exception, because it will be automatically started +even without being in the start script list. + + +Global Variables +**************** + +Global variables are needed to keep track of the the game's state. They can be +accessed from anywhere in the game (in contrast to local variables) and can be +altered at runtime (in contrast to GMST records). Some example of global +variables are current day, month and year, rats killed, player's crime penalty, +is player a werewolf, is player a vampire, and so on. + + +Game Settings (GMST) +******************** + +GMST are variables needed throughout the game. They can be either a float, +integer, boolean, or string. Float and integer variables affect all sorts of +behaviours of the game. String entries are the text that appears in the +user interface, dialogues, tooltips, buttons and so on. GMST records cannot +be altered at runtime. + + +Spells +****** + +Spells are combinations of magic effects with additional properties and are used +in different ways depending on their type. Some spells are the usual abracadabra +that characters cast. Other spells define immunities, diseases, modify +attributes, or give special abilities. Entries in this table can be freely +added, edited, or removed through the editor. + +Name + Name of the spell that will appear in the user interface. + +Spell Type + * Ability - Constant effect which does not need to be cast. Commonly used for racial or birthsign bonuses to attributes and skills. + * Blight - Can be contracted in-game and treated with blight disease cures (common disease cures will not work). Applies a constant effect to the recepient. + * Curse + * Disease - Can be contracted in-game and treated with common disease cures. Applies a constant effect to the recepient. + * Power - May be cast once per day at no magicka cost. Usually a racial or birthsign bonus. + * Spell - Can be cast and costs magicka. The chance to successfully cast a spell depends on the caster's skill. + +Cost + The amount of magicka spent when casting this spell. + +Auto Calc + Automatically calculate the spell's magicka cost. + +Starter Spell + Starting spells are added to the player on character creation when certain + criteria are fulfilled. The player must be able to cast spells, there is a + certain chance for that spell to be added, and there is a maximum number + of starter spells the player can have. + + +Always Succeeds + When enabled, it will ensure this spell will always be cast regardless of + the caster's skill. + +Effects + A table containing magic effects of this spell and their properties. + New entries can be added and removed through the right click menu. + + +Enchantments +************ + +Enchantments are a way for magic effects to be assigned to in-game items. +Each enchantment can hold multiple magic effects along with other properties. +Entries can be freely added or removed through the editor. + +Enchantment Type + The way to use this enchantment. + * Cast once - the enchantment is cast like a regular spell and afterwards the item is gone. Used to make scrolls. + * Constant effect - the effects of the enchantment will always apply as long as the enchanted item is equiped. + * When Strikes - the effect will apply to whatever is hit by the weapon with the enchantment. + * When Used - the enchantment is cast like a regular spell. Instead of spending character's magicka, it uses the item's available charges. + +Cost + How many points from the available charges are spent each time the + enchantment is used. In-game the cost will also depend on character's + enchanting skill. + +Charges + Total supply of points needed to use the enchantment. When there are + less charges than the cost, the enchantment cannot be used and + the item needs to be refilled. + +Auto Calc + Automatically calculate the enchantment's cost to cast. + +Effects + A table containing magic effects of this enchantment and their properties. + New entries can be added and removed through the right click menu. + + +Magic Efects +************ + +Magic effects define how in-game entities are affected when using magic. +They are required in spells, enchantments, and potions. The core gameplay +functionality of these effects is hardcoded and it's not possible to add +new entries through the editor. The existing entries can nonetheless have +their various parameters adjusted. + +School + Category this magic effect belongs to. + +Base Cost + Used when automatically calculating the spell's cost with "Auto Calc" feature. + +Icon + Which icon will be displayed in the user interface for this effect. Can only + insert records available from the icons table. + +Particle + Texture used by the particle system of this magic effect. + +Casting Object + Which object is displayed when this magic effect is cast. + +Hit Object + Which object is displayed when this magic effect hits a target. + +Area Object + Which object is displayed when this magic effect affects an area. + +Bolt Object + Which object is displayed as the projectile for this magic effect. + +Casting Sound + Sound played when this magic effect is cast. + +Hit Sound + Sound played when this magic effect hits a target. + +Area Sound + Sound played when this magic effect affects an area. + +Bolt Sound + Sound played by this magic effect's projectile. + +Allow Spellmaking + When enabled, this magic effect can be used to create spells. + +Allow Enchanting + When enabled, this magic effect can be used to create enchantments. + +Negative Light + This is a flag present in Morrowind, but is not actually used. + It doesn’t do anything in OpenMW either. + +Description + Flavour text that appears in the user interface. diff --git a/docs/source/manuals/openmw-cs/tables-world.rst b/docs/source/manuals/openmw-cs/tables-world.rst new file mode 100644 index 0000000000..91f1b744a5 --- /dev/null +++ b/docs/source/manuals/openmw-cs/tables-world.rst @@ -0,0 +1,275 @@ +############ +World Tables +############ + +These are the tables in the World menu category. The contents of the game world +can be changed by choosing one of the options in the appropriate menu at the top +of the screen. + + +Objects +******* + +This is a library of all the items, triggers, containers, NPCs, etc. in the game. +There are several kinds of Record Types. Depending on which type a record +is, it will need specific information to function. For example, an NPC needs a +value attached to its aggression level. A chest, of course, does not. All Record +Types contain at least a 3D model or else the player would not see them. Usually +they also have a *Name*, which is what the players sees when they hover their +crosshair over the object during the game. + +Please refer to the :doc:`record-types` chapter for an overview of what each +object type represents in the game's world. + + +Instances +********* + +An instance is created every time an object is placed into a cell. While the +object defines its own fundamental properties, an instance defines how and where +this object appears in the world. When the object is modified, all of its +instances will be modified as well. + +Cell + Which cell contains this instance. Is assigned automatically based on the + edit you make in the 3D view. + +Original Cell + If an object has been moved in-game this field keeps a track of the original + cell as defined through the editor. Is assigned automatically based on the edit + you make in the 3D view. + +Object ID + ID of the object from which this instance is created. + +Pos X, Y, Z + Position coordinates in 3D space relative to the parent cell. + +Rot X, Y, Z + Rotation in 3D space. + +Scale + Size factor applied to this instance. It scales the instance uniformly on + all axes. + +Owner + NPC the instance belongs to. Picking up the instance by the player is + regarded as stealing. + +Soul + This field takes the object of a *Creature* type. Option applies only to + soul gems which will contain the creature's soul and allow enchanting. + +Faction + Faction the instance belongs to. Picking up the instance without joining + this faction is regarded as stealing. + +Faction Index + The player's required rank in a faction to pick up this instance without it + seen as stealing. It allows a reward mechanic where the higher the player + is in a faction, the more of its items and resources are freely + available for use. + +Charges + How many times can this item be used. Applies to lockpicks, probes, and + repair items. Typically used to add a "used" version of the object to the + in-game world. + +Enchantment + Doesn't appear to do anything for instances. An identical field for Objects + takes an ID of an enchantment. + +Coin Value + This works only for instances created from objects with IDs ``gold_001``, + ``gold_005``, ``gold_010``, ``gold_025``, and ``gold_100``. Coin Value tells how + much gold is added to player's inventory when this instance is picked up. The + names and corresponding functionality are hardcoded into the engine. + + For all other instances this value does nothing and their price when buying + or selling is determined by the Coin Value of their Object. + +Teleport + When enabled, this instance acts as a teleport to other locations in the world. + Teleportation occurs when the player activates the instance. + +Teleport Cell + Destination cell where the player will appear. + +Teleport Pos X, Y, Z + Location coordinates where the player will appear relative to the + destination cell. + +Teleport Rot X, Y, Z + Initial orientation of the player after being teleported. + +Lock Level + Is there a lock on this instance and how difficult it is to pick. + +Key + Which key is needed to unlock the lock on this instance. + +Trap + What spell will be cast on the player if the trap is triggered. The spell + has an on touch magic effect. + +Owner Global + A global variable that lets you override ownership. This is used in original + Morrowind to make beds rentable. + + +Cells +***** + +Cells are the basic world-building units that together make up the game's world. +Each of these basic building blocks is a container for other objects to exist in. +Dividing an expansive world into smaller units is neccessary to be able to +efficiently render and process it. Cells can be one of two types: + +Exterior cells + These represent the outside world. They all fit on a grid where cells have + unique coordinates and border one another. Each exterior cell contains a part of + the terrain and together they form a seamless, continuous landmass. Entering and + leaving these cells is as simple as walking beyond their boundary after which we + enter its neighbouring cell. It is also possible to move into another interior + or exterior cell through door objects. + +Interior cells + These represent enclosed spaces such as houses, dungeons, mines, etc. They + don't have a terrain, instead their whole environment is made from objects. + Interior cells only load when the player is in them. Entering and leaving these + cells is possible through door objects or teleportation abilities. + +The Cells table provides you with a list of cells in the game and exposes +their various parameters to edit. + +Sleep Forbidden + In most cities it is forbidden to sleep outside. Sleeping in the wilderness + carries its own risks of attack, though. This entry lets you decide if a + player should be allowed to sleep on the floor in this cell or not. + +Interior Water + Setting the cell’s Interior Water to ``true`` tells the game that there needs + to be water at height 0 in this cell. This is useful for dungeons or mines + that have water in them. + + Setting the cell’s Interior Water to ``false`` tells the game that the water + at height 0 should not be used. This flag is useless for outside cells. + +Interior Sky + Should this interior cell have a sky? This is a rather unique case. The + Tribunal expansion took place in a city on the mainland. Normally this would + require the city to be composed of exterior cells so it has a sky, weather + and the like. But if the player is in an exterior cell and were to look at + their in-game map, they would see Vvardenfell with an overview of all + exterior cells. The player would have to see the city’s very own map, as if + they were walking around in an interior cell. + + So the developers decided to create a workaround and take a bit of both: The + whole city would technically work exactly like an interior cell, but it + would need a sky as if it was an exterior cell. That is what this is. This + is why the vast majority of the cells you will find in this screen will have + this option set to false: It is only meant for these "fake exteriors". + +Region + To which Region does this cell belong? This has an impact on the way the + game handles weather and encounters in this area. It is also possible for a + cell not to belong to any region. + +Interior + When enabled, it allows to manually set *Ambient*, *Sunlight*, *Fog*, + and *Fog Density* values regardless of the main sky system. + +Ambient + Colour of the secondary light, that contributes to an overall shading of the + scene. + +Sunlight + Colour of the primary light that lights the scene. + +Fog + Colour of the distant fog effect. + +Fog Density + How quickly do objects start fading into the fog. + +Water Level + Height of the water plane. Only applies to interior cells + when *Interior Water* is enabled. + +Map Color + This is a property present in Morrowind, but is not actually used. + It doesn’t do anything in OpenMW either. + + +Lands +***** + +Lands are records needed by exterior cells to show the terrain. Each exterior +cell needs its own land record and they are paired by matching IDs. Land records +can be created manually in this table, but a better approach is to simply shape +the terrain in the 3D view and the land record of affected cells will be +created automatically. + + +Land Textures +************* + +This is a list of textures that are specifically used to paint the terrain of +exterior cells. By default, the terrain shows the ``_land_default.dds`` texture +found in ``data/textures`` folder. Land texture entries can be added, edited or +removed. + +Texture Nickname + Name of this land texture. + +Texture Index + Assigned automatically and cannot be edited. + +Texture + Texture image file that is used for this land texture. + + +Pathgrids +********* + +Pathgrids allow NPCs to navigate and move along complicated paths in their surroundings. +A pathgrid contains a list of *points* connected by *edges*. NPCs will +find their way from one point to another as long as there is a path of +connecting edges between them. One pathgrid is used per cell. When recast +navigation is enabled, the pathgrids are not used. + + +Regions +******* + +Regions describe general areas of the exterior game world and define rules for +random enemy encounters, ambient sounds, and weather. Regions can be assigned +one per cell and the cells will inherit their rules. + +Name + This is how the game will show the player's location in-game. + +MapColour + This is a colour used to identify the region when viewed in *World* → *Region Map*. + +Sleep Encounter + This field takes an object of the *Creature Levelled List* type. This object + defines what kinds of enemies the player might encounter when sleeping outside + in the wilderness. + +Weather + A table listing all available weather types and their chance to occur while + the player is in this region. Entries cannot be added or removed. + +Sounds + A table listing ambient sounds that will randomly play while the player is + in this region. Entries can be freely added or removed. + + +Region Map +********** + +The region map shows a grid of exterior cells, their relative positions to one +another, and regions they belong to. In summary, it shows the world map. +Compared to the cells table which is a list, this view helps vizualize the world. +Region map does not show interior cells. diff --git a/docs/source/manuals/openmw-cs/tables.rst b/docs/source/manuals/openmw-cs/tables.rst index 43da03f079..2d63439683 100644 --- a/docs/source/manuals/openmw-cs/tables.rst +++ b/docs/source/manuals/openmw-cs/tables.rst @@ -67,102 +67,3 @@ Modified delete that instance yourself or make sure that that object is replaced by something that still exists otherwise the player will get crashes in the worst case scenario. - - - -World Screens -************* - -The contents of the game world can be changed by choosing one of the options in -the appropriate menu at the top of the screen. - - -Regions -======= - -This describes the general areas of Vvardenfell. Each of these areas has -different rules about things such as encounters and weather. - -Name - This is how the game will show the player's location in-game. - -MapColour - This is a six-digit hexadecimal representation of the colour used to - identify the region on the map available in *World* → *Region Map*. - -Sleep Encounter - These are the rules for what kinds of enemies the player might encounter - when sleeping outside in the wilderness. - - -Cells -===== - -Expansive worlds such as Vvardenfell, with all its items, NPCs, etc. have a lot -going on simultaneously. But if the player is in Balmora, why would the -computer need to keep track the exact locations of NPCs walking through the -corridors in a Vivec canton? All that work would be quite useless and bring -the player's system down to its knees! So the world has been divided up into -squares we call *cells*. Once your character enters a cell, the game will load -everything that is going on in that cell so the player can interact with it. - -In the original Morrowind this could be seen when a small loading bar would -appear near the bottom of the screen while travelling; the player had just -entered a new cell and the game had to load all the items and NPCs. The *Cells* -screen in OpenMW CS provides you with a list of cells in the game, both the -interior cells (houses, dungeons, mines, etc.) and the exterior cells (the -outside world). - -Sleep Forbidden - Can the player sleep on the floor? In most cities it is forbidden to sleep - outside. Sleeping in the wilderness carries its own risks of attack, though, - and this entry lets you decide if a player should be allowed to sleep on the - floor in this cell or not. - -Interior Water - Should water be rendered in this interior cell? The game world consists of - an endless ocean at height 0, then the landscape is added. If part of the - landscape goes below height 0, the player will see water. - - Setting the cell’s Interior Water to true tells the game that this cell that - there needs to be water at height 0. This is useful for dungeons or mines - that have water in them. - - Setting the cell’s Interior Water to ``false`` tells the game that the water - at height 0 should not be used. This flag is useless for outside cells. - -Interior Sky - Should this interior cell have a sky? This is a rather unique case. The - Tribunal expansion took place in a city on the mainland. Normally this would - require the city to be composed of exterior cells so it has a sky, weather - and the like. But if the player is in an exterior cell and were to look at - their in-game map, they would see Vvardenfell with an overview of all - exterior cells. The player would have to see the city’s very own map, as if - they were walking around in an interior cell. - - So the developers decided to create a workaround and take a bit of both: The - whole city would technically work exactly like an interior cell, but it - would need a sky as if it was an exterior cell. That is what this is. This - is why the vast majority of the cells you will find in this screen will have - this option set to false: It is only meant for these "fake exteriors". - -Region - To which Region does this cell belong? This has an impact on the way the - game handles weather and encounters in this area. It is also possible for a - cell not to belong to any region. - - -Objects -======= - -This is a library of all the items, triggers, containers, NPCs, etc. in the -game. There are several kinds of Record Types. Depending on which type a record -is, it will need specific information to function. For example, an NPC needs a -value attached to its aggression level. A chest, of course, does not. All -Record Types contain at least a 3D model or else the player would not see them. -Usually they also have a *Name*, which is what the players sees when they hover -their reticle over the object during the game. - -Please refer to the Record Types chapter for an overview of what each type of -object does and what you can tell OpenMW CS about these objects. - diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 75dbe8dca2..d2b67d02ca 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -154,9 +154,9 @@ A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" - and merge the changes you've made into the project master repository. One of the project maintainers will probably ask - you to make some corrections or clarifications. Go back and repeat this process to make those changes, - and repeat until they're good enough to get merged. +and merge the changes you've made into the project master repository. One of the project maintainers will probably ask +you to make some corrections or clarifications. Go back and repeat this process to make those changes, +and repeat until they're good enough to get merged. So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index cd947745e1..9aa409f784 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -6,4 +6,6 @@ Reference Material :maxdepth: 2 modding/index - documentationHowTo \ No newline at end of file + lua-scripting/index + postprocessing/index + documentationHowTo diff --git a/docs/source/reference/lua-scripting/aipackages.rst b/docs/source/reference/lua-scripting/aipackages.rst new file mode 100644 index 0000000000..4570be712e --- /dev/null +++ b/docs/source/reference/lua-scripting/aipackages.rst @@ -0,0 +1,145 @@ +Built-in AI packages +==================== + +Combat +------ + +Attack another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - target + - `GameObject `_ [required] + - the actor to attack + +**Examples** + +.. code-block:: Lua + + -- from local script add package to self + local AI = require('openmw.interfaces').AI + AI.startPackage({type='Combat', target=anotherActor}) + + -- via event to any actor + actor:sendEvent('StartAIPackage', {type='Combat', target=anotherActor}) + +Pursue +------ + +Pursue another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - target + - `GameObject `_ [required] + - the actor to pursue + +Follow +------ + +Follow another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - target + - `GameObject `_ [required] + - the actor to follow + +Escort +------ + +Escort another actor to the given location. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - target + - `GameObject `_ [required] + - the actor to follow + * - destPosition + - `3d vector `_ [required] + - the destination point + * - destCell + - Cell [optional] + - the destination cell + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + +**Example** + +.. code-block:: Lua + + actor:sendEvent('StartAIPackage', { + type = 'Escort', + target = object.self, + destPosition = util.vector3(x, y, z), + duration = 3 * time.hour, + }) + +Wander +------ + +Wander nearby current position. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - distance + - float [default=0] + - the actor to follow + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + +Travel +------ + +Go to given location. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - destPosition + - `3d vector `_ [required] + - the point to travel to + diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst new file mode 100644 index 0000000000..23bf9cb3a2 --- /dev/null +++ b/docs/source/reference/lua-scripting/api.rst @@ -0,0 +1,81 @@ +################# +Lua API reference +################# + +.. toctree:: + :hidden: + + engine_handlers + user_interface + aipackages + setting_renderers + events + openmw_util + openmw_storage + openmw_core + openmw_types + openmw_async + openmw_world + openmw_self + openmw_nearby + openmw_input + openmw_ui + openmw_camera + openmw_postprocessing + openmw_debug + openmw_aux_calendar + openmw_aux_util + openmw_aux_time + openmw_aux_ui + interface_ai + interface_camera + interface_mwui + interface_settings + iterables + + +- :ref:`Engine handlers reference` +- :ref:`User interface reference ` +- `Game object reference `_ +- `Cell reference `_ +- :ref:`Built-in AI packages` +- :ref:`Built-in events` + +**API packages** + +API packages provide functions that can be called by scripts. I.e. it is a script-to-engine interaction. +A package can be loaded with ``require('')``. +It can not be overloaded even if there is a lua file with the same name. +The list of available packages is different for global and for local scripts. +Player scripts are local scripts that are attached to a player. + +.. include:: tables/packages.rst + +**openmw_aux** + +``openmw_aux.*`` are built-in libraries that are itself implemented in Lua. They can not do anything that is not possible with the basic API, they only make it more convenient. +Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can override them, but it is not recommended. + +.. include:: tables/aux_packages.rst + +**Interfaces of built-in scripts** + +.. list-table:: + :widths: 20 20 60 + + * - Interface + - Can be used + - Description + * - :ref:`AI ` + - by local scripts + - Control basic AI of NPCs and creatures. + * - :ref:`Camera ` + - by player scripts + - | Allows to alter behavior of the built-in camera script + | without overriding the script completely. + * - :ref:`Settings ` + - by player and global scripts + - Save, display and track changes of setting values. + * - :ref:`MWUI ` + - by player scripts + - Morrowind-style UI templates. diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst new file mode 100644 index 0000000000..a731ceef6f --- /dev/null +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -0,0 +1,117 @@ +Engine handlers reference +========================= + +Engine handler is a function defined by a script, that can be called by the engine. + + + +**Can be defined by any script** + +.. list-table:: + :widths: 20 80 + + * - onInit(initData) + - | Called once when the script is created (not loaded). `InitData can be` + | `assigned to a script in openmw-cs (not yet implemented).` + | ``onInterfaceOverride`` can be called before ``onInit``. + * - onUpdate(dt) + - | Called every frame if the game is not paused. `dt` is + | the simulation time from the last update in seconds. + * - onSave() -> savedData + - | Called when the game is saving. May be called in inactive state, + | so it shouldn't use `openmw.nearby`. + * - onLoad(savedData, initData) + - | Called on loading with the data previosly returned by + | ``onSave``. During loading the object is always inactive. ``initData`` is + | the same as in ``onInit``. + | Note that ``onLoad`` means loading a script rather than loading a game. + | If a script did not exist when a game was saved onLoad will not be + | called, but ``onInit`` will. + * - onInterfaceOverride(base) + - | Called if the current script has an interface and overrides an interface + | (``base``) of another script. + +**Only for global scripts** + +.. list-table:: + :widths: 20 80 + + * - onNewGame() + - New game is started + * - onPlayerAdded(player) + - Player added to the game world. The argument is a `Game object`. + * - onObjectActive(object) + - Object becomes active. + * - onActorActive(actor) + - Actor (NPC or Creature) becomes active. + * - onItemActive(item) + - | Item (Weapon, Potion, ...) becomes active in a cell. + | Does not apply to items in inventories or containers. + +**Only for local scripts** + +.. list-table:: + :widths: 20 80 + + * - onActive() + - | Called when the object becomes active + | (either a player came to this cell again, or a save was loaded). + * - onInactive() + - | Object became inactive. Since it is inactive the handler + | can not access anything nearby, but it is possible to send + | an event to global scripts. + * - onActivated(actor) + - | Called on an object when an actor activates it. Note that picking + | up an item is also an activation and works this way: (1) a copy of + | the item is placed to the actor's inventory, (2) count of + | the original item is set to zero, (3) and only then onActivated is + | called on the original item, so self.count is already zero. + * - onConsume(item) + - | Called on an actor when they consume an item (e.g. a potion). + | Similarly to onActivated, the item has already been removed + | from the actor's inventory, and the count was set to zero. + +**Only for local scripts attached to a player** + +.. list-table:: + :widths: 20 80 + + * - onFrame(dt) + - | Called every frame (even if the game is paused) right after + | processing user input. Use it only for latency-critical stuff + | and for UI that should work on pause. + | `dt` is simulation time delta (0 when on pause). + * - onKeyPress(key) + - | `Key `_ is pressed. + | Usage example: + | ``if key.symbol == 'z' and key.withShift then ...`` + * - onKeyRelease(key) + - | `Key `_ is released. + | Usage example: + | ``if key.symbol == 'z' and key.withShift then ...`` + * - onControllerButtonPress(id) + - | A `button `_ on a game controller is pressed. + | Usage example: + | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` + * - onControllerButtonRelease(id) + - | A `button `_ on a game controller is released. + | Usage example: + | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` + * - onInputAction(id) + - | `Game control `_ is pressed. + | Usage example: + | ``if id == input.ACTION.ToggleWeapon then ...`` + * - onTouchPress(touchEvent) + - | A finger pressed on a touch device. + | `Touch event `_. + * - onTouchRelease(touchEvent) + - | A finger released a touch device. + | `Touch event `_. + * - onTouchMove(touchEvent) + - | A finger moved on a touch device. + | `Touch event `_. + * - | onConsoleCommand( + | mode, command, selectedObject) + - | User entered `command` in in-game console. Called if either + | `mode` is not default or `command` starts with prefix `lua`. + diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst new file mode 100644 index 0000000000..3d443b2811 --- /dev/null +++ b/docs/source/reference/lua-scripting/events.rst @@ -0,0 +1,13 @@ +Built-in events +=============== + +Any script can send to any actor (except player, for player will be ignored) events ``StartAIPackage`` and ``RemoveAIPackages``. +The effect is equivalent to calling ``interfaces.AI.startPackage`` or ``interfaces.AI.removePackages`` in a local script on this actor. + +Examples: + +.. code-block:: Lua + + actor:sendEvent('StartAIPackage', {type='Combat', target=self.object}) + actor:sendEvent('RemoveAIPackages', 'Pursue') + diff --git a/docs/source/reference/lua-scripting/index.rst b/docs/source/reference/lua-scripting/index.rst new file mode 100644 index 0000000000..75157982c9 --- /dev/null +++ b/docs/source/reference/lua-scripting/index.rst @@ -0,0 +1,15 @@ +#################### +OpenMW Lua scripting +#################### + +.. warning:: + OpenMW Lua scripting is in early stage of development. Also note that OpenMW Lua is not compatible with MWSE. + +.. toctree:: + :caption: Table of Contents + :includehidden: + :maxdepth: 2 + + overview + api + diff --git a/docs/source/reference/lua-scripting/interface_ai.rst b/docs/source/reference/lua-scripting/interface_ai.rst new file mode 100644 index 0000000000..ec79b50d5d --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_ai.rst @@ -0,0 +1,6 @@ +Interface AI +============ + +.. raw:: html + :file: generated_html/scripts_omw_ai.html + diff --git a/docs/source/reference/lua-scripting/interface_camera.rst b/docs/source/reference/lua-scripting/interface_camera.rst new file mode 100644 index 0000000000..6a0f19d47b --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_camera.rst @@ -0,0 +1,6 @@ +Interface Camera +================ + +.. raw:: html + :file: generated_html/scripts_omw_camera_camera.html + diff --git a/docs/source/reference/lua-scripting/interface_mwui.rst b/docs/source/reference/lua-scripting/interface_mwui.rst new file mode 100644 index 0000000000..cd9b00cb87 --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_mwui.rst @@ -0,0 +1,6 @@ +Interface MWUI +============== + +.. raw:: html + :file: generated_html/scripts_omw_mwui_init.html + diff --git a/docs/source/reference/lua-scripting/interface_settings.rst b/docs/source/reference/lua-scripting/interface_settings.rst new file mode 100644 index 0000000000..cd1994ccfa --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_settings.rst @@ -0,0 +1,6 @@ +Interface Settings +================== + +.. raw:: html + :file: generated_html/scripts_omw_settings_player.html + diff --git a/docs/source/reference/lua-scripting/iterables.rst b/docs/source/reference/lua-scripting/iterables.rst new file mode 100644 index 0000000000..208b7f1c92 --- /dev/null +++ b/docs/source/reference/lua-scripting/iterables.rst @@ -0,0 +1,50 @@ +Iterable types +============== + +List Iterable +------------- + +An iterable with defined size and order. + +.. code-block:: Lua + + -- can iterate over the list with pairs + for i, v in pairs(list) do + -- ... + end + +.. code-block:: Lua + + -- can iterate over the list with ipairs + for i, v in ipairs(list) do + -- ... + end + +.. code-block:: Lua + + -- can get total size with the size # operator + local length = #list + +.. code-block:: Lua + + -- can index the list with numbers + for i = 1, length do + list[i] + end + +Map Iterable +------------ + +An iterable with undefined order. + +.. code-block:: Lua + + -- can iterate over the map with pairs + for k, v in pairs(map) do + -- ... + end + +.. code-block:: Lua + + -- can index the map by key + map[key] diff --git a/docs/source/reference/lua-scripting/openmw_async.rst b/docs/source/reference/lua-scripting/openmw_async.rst new file mode 100644 index 0000000000..9f24f63a4f --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_async.rst @@ -0,0 +1,5 @@ +Package openmw.async +==================== + +.. raw:: html + :file: generated_html/openmw_async.html diff --git a/docs/source/reference/lua-scripting/openmw_aux_calendar.rst b/docs/source/reference/lua-scripting/openmw_aux_calendar.rst new file mode 100644 index 0000000000..ea60b62852 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_aux_calendar.rst @@ -0,0 +1,5 @@ +Package openmw_aux.calendar +=========================== + +.. raw:: html + :file: generated_html/openmw_aux_calendar.html diff --git a/docs/source/reference/lua-scripting/openmw_aux_time.rst b/docs/source/reference/lua-scripting/openmw_aux_time.rst new file mode 100644 index 0000000000..120d888a01 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_aux_time.rst @@ -0,0 +1,5 @@ +Package openmw_aux.time +======================= + +.. raw:: html + :file: generated_html/openmw_aux_time.html diff --git a/docs/source/reference/lua-scripting/openmw_aux_ui.rst b/docs/source/reference/lua-scripting/openmw_aux_ui.rst new file mode 100644 index 0000000000..18c0926c03 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_aux_ui.rst @@ -0,0 +1,5 @@ +Package openmw_aux.ui +======================= + +.. raw:: html + :file: generated_html/openmw_aux_ui.html diff --git a/docs/source/reference/lua-scripting/openmw_aux_util.rst b/docs/source/reference/lua-scripting/openmw_aux_util.rst new file mode 100644 index 0000000000..01b200bfc2 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_aux_util.rst @@ -0,0 +1,5 @@ +Package openmw_aux.util +======================= + +.. raw:: html + :file: generated_html/openmw_aux_util.html diff --git a/docs/source/reference/lua-scripting/openmw_camera.rst b/docs/source/reference/lua-scripting/openmw_camera.rst new file mode 100644 index 0000000000..4090843920 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_camera.rst @@ -0,0 +1,5 @@ +Package openmw.camera +===================== + +.. raw:: html + :file: generated_html/openmw_camera.html diff --git a/docs/source/reference/lua-scripting/openmw_core.rst b/docs/source/reference/lua-scripting/openmw_core.rst new file mode 100644 index 0000000000..bde93fb9c9 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_core.rst @@ -0,0 +1,5 @@ +Package openmw.core +=================== + +.. raw:: html + :file: generated_html/openmw_core.html diff --git a/docs/source/reference/lua-scripting/openmw_debug.rst b/docs/source/reference/lua-scripting/openmw_debug.rst new file mode 100644 index 0000000000..5be48f6558 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_debug.rst @@ -0,0 +1,5 @@ +Package openmw.debug +==================== + +.. raw:: html + :file: generated_html/openmw_debug.html diff --git a/docs/source/reference/lua-scripting/openmw_input.rst b/docs/source/reference/lua-scripting/openmw_input.rst new file mode 100644 index 0000000000..39136dc4c2 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_input.rst @@ -0,0 +1,6 @@ +Package openmw.input +==================== + +.. raw:: html + :file: generated_html/openmw_input.html + diff --git a/docs/source/reference/lua-scripting/openmw_nearby.rst b/docs/source/reference/lua-scripting/openmw_nearby.rst new file mode 100644 index 0000000000..ca0a7d5d00 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_nearby.rst @@ -0,0 +1,5 @@ +Package openmw.nearby +===================== + +.. raw:: html + :file: generated_html/openmw_nearby.html diff --git a/docs/source/reference/lua-scripting/openmw_postprocessing.rst b/docs/source/reference/lua-scripting/openmw_postprocessing.rst new file mode 100644 index 0000000000..ec2f025049 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_postprocessing.rst @@ -0,0 +1,5 @@ +Package openmw.postprocessing +============================= + +.. raw:: html + :file: generated_html/openmw_postprocessing.html diff --git a/docs/source/reference/lua-scripting/openmw_self.rst b/docs/source/reference/lua-scripting/openmw_self.rst new file mode 100644 index 0000000000..71a2cb04ae --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_self.rst @@ -0,0 +1,5 @@ +Package openmw.self +=================== + +.. raw:: html + :file: generated_html/openmw_self.html diff --git a/docs/source/reference/lua-scripting/openmw_storage.rst b/docs/source/reference/lua-scripting/openmw_storage.rst new file mode 100644 index 0000000000..5abf664e1a --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_storage.rst @@ -0,0 +1,5 @@ +Package openmw.storage +====================== + +.. raw:: html + :file: generated_html/openmw_storage.html diff --git a/docs/source/reference/lua-scripting/openmw_types.rst b/docs/source/reference/lua-scripting/openmw_types.rst new file mode 100644 index 0000000000..1819b1a6ce --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_types.rst @@ -0,0 +1,5 @@ +Package openmw.types +==================== + +.. raw:: html + :file: generated_html/openmw_types.html diff --git a/docs/source/reference/lua-scripting/openmw_ui.rst b/docs/source/reference/lua-scripting/openmw_ui.rst new file mode 100644 index 0000000000..1b7b57c359 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_ui.rst @@ -0,0 +1,5 @@ +Package openmw.ui +================= + +.. raw:: html + :file: generated_html/openmw_ui.html diff --git a/docs/source/reference/lua-scripting/openmw_util.rst b/docs/source/reference/lua-scripting/openmw_util.rst new file mode 100644 index 0000000000..f054d7b44d --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_util.rst @@ -0,0 +1,5 @@ +Package openmw.util +=================== + +.. raw:: html + :file: generated_html/openmw_util.html diff --git a/docs/source/reference/lua-scripting/openmw_world.rst b/docs/source/reference/lua-scripting/openmw_world.rst new file mode 100644 index 0000000000..b8818d4027 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_world.rst @@ -0,0 +1,5 @@ +Package openmw.world +==================== + +.. raw:: html + :file: generated_html/openmw_world.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst new file mode 100644 index 0000000000..4cceb26088 --- /dev/null +++ b/docs/source/reference/lua-scripting/overview.rst @@ -0,0 +1,686 @@ +Overview of Lua scripting +######################### + +Language and sandboxing +======================= + +OpenMW supports scripts written in Lua 5.1 with some extensions (see below) from Lua 5.2. +There are no plans to switch to any newer version of the language, because newer versions are not supported by LuaJIT. + +Here are starting points for learning Lua: + +- `Programing in Lua `__ (first edition, aimed at Lua 5.0) +- `Lua 5.1 Reference Manual `__ + +Each script works in a separate sandbox and doesn't have any access to the underlying operating system. +Only a limited list of allowed standard libraries can be used: +`coroutine `__, +`math `__ (except `math.randomseed` -- it is called by the engine on startup and not available from scripts), +`string `__, +`table `__, +`os `__ (only `os.date`, `os.difftime`, `os.time`). +These libraries are loaded automatically and are always available. + +Allowed `basic functions `__: +``assert``, ``error``, ``ipairs``, ``next``, ``pairs``, ``pcall``, ``print``, ``select``, ``tonumber``, ``tostring``, ``type``, ``unpack``, ``xpcall``, ``rawequal``, ``rawget``, ``rawset``, ``getmetatable``, ``setmetatable``. + +Supported Lua 5.2 features: + +- ``goto`` and ``::labels::``; +- hex escapes ``\x3F`` and ``\*`` escape in strings; +- ``math.log(x [,base])``; +- ``string.rep(s, n [,sep])``; +- in ``string.format()``: ``%q`` is reversible, ``%s`` uses ``__tostring``, ``%a`` and ``%A`` are added; +- String matching pattern ``%g``; +- ``__pairs`` and ``__ipairs`` metamethods; +- Function ``table.unpack`` (alias to Lua 5.1 ``unpack``). + +Loading libraries with ``require('library_name')`` is allowed, but limited. It works this way: + +1. If `library_name` is one of the standard libraries, then return the library. +2. If `library_name` is one of the built-in `API packages`_, then return the package. +3. Otherwise search for a Lua source file with such name in :ref:`data folders `. For example ``require('my_lua_library.something')`` will try to open one of the files ``my_lua_library/something.lua`` or ``my_lua_library/something/init.lua``. + +Loading DLLs and precompiled Lua files is intentionally prohibited for compatibility and security reasons. + +Basic concepts +============== + +Game object + Any object that exists in the game world and has a specific location. Player, actors, items, and statics are game objects. + +Record + Persistent information about an object. Includes starting stats and links to assets, but doesn't have a location. Game objects are instances of records. Some records (e.g. a unique NPC) have a single instance, some (e.g. a specific potion) may correspond to multiple objects. + +.. note:: + Don't be confused with MWSE terminology. In MWSE game objects are "references" and records are "objects". + +Cell + An area of the game world. A position in the world is a link to a cell and X, Y, Z coordinates in the cell. At a specific moment in time each cell can be active or inactive. Inactive cells don't perform physics updates. + +Global scripts + Lua scripts that are not attached to any game object and are always active. Global scripts can not be started or stopped during a game session. Lists of global scripts are defined by `omwscripts` files, which should be :ref:`registered ` in `openmw.cfg`. + +Local scripts + Lua scripts that are attached to some game object. A local script is active only if the object it is attached to is in an active cell. There are no limitations to the number of local scripts on one object. Local scripts can be attached to (or detached from) any object at any moment by a global script. In some cases inactive local scripts still can run code (for example during saving and loading), but while inactive they can not see nearby objects. + +.. note:: + Currently scripts on objects in a container or an inventory are considered inactive. Probably later this behaviour will be changed. + +Player scripts + A specific kind of local scripts; *player script* is a local script that is attached to a player. It can do everything that a normal local script can do, plus some player-specific functionality (e.g. control UI and camera). + +This scripting API was developed to be conceptually compatible with `multiplayer `__. In multiplayer the server is lightweight and delegates most of the work to clients. Each client processes some part of the game world. Global scripts are server-side and local scripts are client-side. Because of this, there are several rules for the Lua scripting API: + +1. A local script can see only some area of the game world (cells that are active on a specific client). Any data from inactive cells can't be used, as they are not synchronized and could be already changed on another client. +2. A local script can only modify the object it is attached to. Other objects can theoretically be processed by another client. To prevent synchronization problems the access to them is read-only. +3. Global scripts can access and modify the whole game world including unloaded areas, but the global scripts API is different from the local scripts API and in some aspects limited, because it is not always possible to have all game assets in memory at the same time. +4. Though the scripting system doesn't actually work with multiplayer yet, the API assumes that there can be several players. That's why any command related to UI, camera, and everything else that is player-specific can be used only by player scripts. + + +How to run a script +=================== + +Let's write a simple example of a `Player script`: + +.. code-block:: Lua + + -- Save to my_lua_mod/example/player.lua + + local ui = require('openmw.ui') + + return { + engineHandlers = { + onKeyPress = function(key) + if key.symbol == 'x' then + ui.showMessage('You have pressed "X"') + end + end + } + } + +The script will be used only if it is specified in one of content files. +OpenMW Lua is an inclusive OpenMW feature, so it can not be controlled by ESP/ESM. +The options are: + + +1. Create text file "my_lua_mod.omwscripts" with the following line: +:: + + PLAYER: example/player.lua + +2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon". + + +Enable it in ``openmw.cfg`` the same way as any other mod: + +:: + + data=path/to/my_lua_mod + content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon + +Now every time the player presses "X" on a keyboard, a message is shown. + + +Format of ``.omwscripts`` +========================= + +:: + + # Lines starting with '#' are comments + + GLOBAL: my_mod/some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER: my_mod/player.lua + + # Local script that will be automatically attached to every NPC and every creature in the game + NPC, CREATURE: my_mod/some_other_script.lua + + # Local script that can be attached to any object by a global script + CUSTOM: my_mod/something.lua + + # Local script that will be automatically attached to any Container AND can be + # attached to any other object by a global script. + CONTAINER, CUSTOM: my_mod/container.lua + +Each script is described by one line: +``: ``. +The order of lines determines the script load order (i.e. script priorities). + +Possible flags are: + +- ``GLOBAL`` - a global script; always active, can not be stopped; +- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script; +- ``PLAYER`` - an auto started player script; +- ``ACTIVATOR`` - a local script that will be automatically attached to any activator; +- ``ARMOR`` - a local script that will be automatically attached to any armor; +- ``BOOK`` - a local script that will be automatically attached to any book; +- ``CLOTHING`` - a local script that will be automatically attached to any clothing; +- ``CONTAINER`` - a local script that will be automatically attached to any container; +- ``CREATURE`` - a local script that will be automatically attached to any creature; +- ``DOOR`` - a local script that will be automatically attached to any door; +- ``INGREDIENT`` - a local script that will be automatically attached to any ingredient; +- ``LIGHT`` - a local script that will be automatically attached to any light; +- ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item; +- ``NPC`` - a local script that will be automatically attached to any NPC; +- ``POTION`` - a local script that will be automatically attached to any potion; +- ``WEAPON`` - a local script that will be automatically attached to any weapon; +- ``APPARATUS`` - a local script that will be automatically attached to any apparatus; +- ``LOCKPICK`` - a local script that will be automatically attached to any lockpick; +- ``PROBE`` - a local script that will be automatically attached to any probe tool; +- ``REPAIR`` - a local script that will be automatically attached to any repair tool. + +Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator. + +Hot reloading +============= + +It is possible to modify a script without restarting OpenMW. To apply changes, open the in-game console and run the command: ``reloadlua``. +This will restart all Lua scripts using the `onSave and onLoad`_ handlers the same way as if the game was saved or loaded. +It reloads all ``.omwscripts`` files and ``.lua`` files that are not packed to any archives. ``.omwaddon`` files and scripts packed to BSA can not be changed without restarting the game. + +Lua console +=========== + +It is also possible to run Lua commands directly from the in-game console. + +To enter the Lua mode run one of the commands: + +- ``lua player`` or ``luap`` - enter player context +- ``lua global`` or ``luag`` - enter global context +- ``lua selected`` or ``luas`` - enter local context on the selected object + +Script structure +================ + +Each script is a separate file in the game assets. +`Starting a script` means that the engine runs the file, parses the table it returns, and registers its interface, event handlers, and engine handlers. The handlers are permanent and exist until the script is stopped (if it is a local script, because global scripts can not be stopped). + +Here is an example of a basic script structure: + +.. code-block:: Lua + + local util = require('openmw.util') + + local function onUpdate(dt) + ... + end + + local function onSave() + ... + return data + end + + local function onLoad(data) + ... + end + + local function myEventHandler(eventData) + ... + end + + local function somePublicFunction(params, ...) + ... + end + + return { + interfaceName = 'MyScriptInterface', + interface = { + somePublicFunction = somePublicFunction, + }, + + eventHandlers = { MyEvent = myEventHandler }, + + engineHandlers = { + onUpdate = onUpdate, + onSave = onSave, + onLoad = onLoad, + } + } + + +.. note:: + Every instance of every script works in a separate enviroment, so it is not necessary + to make everything local. It's local just because it makes the code a bit faster. + +All sections in the returned table are optional. +If you just want to do something every frame, it is enough to write the following: + +.. code-block:: Lua + + return { + engineHandlers = { + onUpdate = function() + print('Hello, World!') + end + } + } + + +Engine handlers +=============== + +An engine handler is a function defined by a script, that can be called by the engine. I.e. it is an engine-to-script interaction. +Not visible to other scripts. If several scripts register an engine handler with the same name, +the engine calls all of them according to the load order (i.e. the order of ``content=`` entries in ``openmw.cfg``) and the order of scripts in ``omwaddon/omwscripts``. + +Some engine handlers are allowed only for global, or only for local/player scripts. Some are universal. +See :ref:`Engine handlers reference`. + + +onSave and onLoad +================= + +When a game is saved or loaded, the engine calls the engine handlers `onSave` or `onLoad` to save or load each script. +The value that `onSave` returns will be passed to `onLoad` when the game is loaded. +It is the only way to save the internal state of a script. All other script variables will be lost after closing the game. +The saved state must be :ref:`serializable `. + +Note that `onLoad` means loading a script rather than loading a game. +If a script did not exist when a game was saved then `onLoad` will not be called, but `onInit` will. + +`onSave` and `onLoad` can be called even for objects in inactive state, so it shouldn't use `openmw.nearby`. + +An example: + +.. code-block:: Lua + + ... + + local scriptVersion = 3 -- increase it every time when `onSave` is changed + + local function onSave() + return { + version = scriptVersion + some = someVariable, + someOther = someOtherVariable + } + end + + local function onLoad(data) + if not data or not data.version or data.version < 2 then + print('Was saved with an old version of the script, initializing to default') + someVariable = 'some value' + someOtherVariable = 42 + return + end + if data.version > scriptVersion then + error('Required update to a new version of the script') + end + someVariable = data.some + if data.version == scriptVersion then + someOtherVariable = data.someOther + else + print(string.format('Updating from version %d to %d', data.version, scriptVersion)) + someOtherVariable = 42 + end + end + + return { + engineHandlers = { + onUpdate = update, + onSave = onSave, + onLoad = onLoad, + } + } + +Serializable data +----------------- + +`Serializable` value means that OpenMW is able to convert it to a sequence of bytes and then (probably on a different computer and with different OpenMW version) restore it back to the same form. + +Serializable value is one of: + +- `nil` value +- a number +- a string +- a game object +- a value of a type, defined by :ref:`openmw.util ` +- a table whith serializable keys and values + +Serializable data can not contain: + +- Functions +- Tables with custom metatables +- Several references to the same table. For example ``{ x = some_table, y = some_table }`` is not allowed. +- Circular references (i.e. when some table contains itself). + +API packages +============ + +API packages provide functions that can be called by scripts. I.e. it is a script-to-engine interaction. +A package can be loaded with ``require('')``. +It can not be overloaded even if there is a lua file with the same name. +The list of available packages is different for global and for local scripts. +Player scripts are local scripts that are attached to a player. + +.. include:: tables/packages.rst + +openmw_aux +---------- + +``openmw_aux.*`` are built-in libraries that are themselves implemented in Lua. They can not do anything that is not possible with the basic API, they only make it more convenient. +Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can override them, but it is not recommended. + +.. include:: tables/aux_packages.rst + +They can be loaded with ``require`` the same as API packages. For example: + +.. code-block:: Lua + + local time = require('openmw_aux.time') + time.runRepeatedly(doSomething, 15 * time.second) -- run `doSomething()` every 15 seconds + + +Script interfaces +================= + +Each script can provide a named interface for other scripts. +It is a script-to-script interaction. This mechanism is not used by the engine itself. + +A script can use an interface of another script either if both are global scripts, or both are local scripts on the same object. +In other cases events should be used. + +Defining an interface: + +.. code-block:: Lua + + return { + interfaceName = "SomeUtils" + interface = { + version = 1, + doSomething = function(x, y) ... end, + } + } + +Overriding the interface and adding a debug output: + +.. code-block:: Lua + + local baseInterface = nil -- will be assigned by `onInterfaceOverride` + interface = { + version = 1, + doSomething = function(x, y) + print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) + baseInterface.doSomething(x, y) -- calls the original `doSomething` + + -- WRONG! Would lead to an infinite recursion. + -- local interfaces = require('openmw.interfaces') + -- interfaces.SomeUtils.doSomething(x, y) + end, + } + + return { + interfaceName = "SomeUtils", + interface = interface, + engineHandlers = { + onInterfaceOverride = function(base) baseInterface = base end, + }, + } + +A general recommendation about overriding is that the new interface should be fully compatible with the old one. +So it is fine to change the behaviour of `SomeUtils.doSomething`, but if you want to add a completely new function, it would be +better to create a new interface for it. For example `SomeUtilsExtended` with an additional function `doSomethingElse`. + +Using the interface: + +.. code-block:: Lua + + local interfaces = require('openmw.interfaces') + + local function onUpdate() + interfaces.SomeUtils.doSomething(2, 3) + end + + return { engineHandlers = {onUpdate = onUpdate} } + +The order in which the scripts are started is important. So if one mod should override an interface provided by another mod, make sure that load order (i.e. the sequence of `lua-scripts=...` in `openmw.cfg`) is correct. + +**Interfaces of built-in scripts** + +.. list-table:: + :widths: 20 20 60 + + * - Interface + - Can be used + - Description + * - :ref:`AI ` + - by local scripts + - Control basic AI of NPCs and creatures. + * - :ref:`Camera ` + - by player scripts + - | Allows to alter behavior of the built-in camera script + | without overriding the script completely. + * - :ref:`Settings ` + - by player and global scripts + - Save, display and track changes of setting values. + * - :ref:`MWUI ` + - by player scripts + - Morrowind-style UI templates. + +Event system +============ + +This is another kind of script-to-script interactions. The differences: + +- Any script can send an event to any object or a global event to global scripts. +- Events are delivered with a small delay (in single player the delay is always one frame). +- Event handlers can not return any data to the sender. +- Event handlers have a single argument `eventData` (must be :ref:`serializable `) + +Events are the main way of interacting between local and global scripts. +They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient. + +If several scripts register handlers for the same event, the handlers will be called in reverse order (opposite to engine handlers). +I.e. the handler from the last script in the load order will be called first. +Return value 'false' means "skip all other handlers for this event". +Any other return value (including nil) means nothing. + +An example. Imagine we are working on a mod that adds some "dark power" with special effects. +We attach a local script to an item that can explode. +At some moment it will send the 'DamagedByDarkPower' event to all nearby actors: + +.. code-block:: Lua + + local self = require('openmw.self') + local nearby = require('openmw.nearby') + + local function onActivated() + for i, actor in ipairs(nearby.actors) do + local dist = (self.position - actor.position):length() + if dist < 500 then + local damage = (1 - dist / 500) * 200 + actor:sendEvent('DamagedByDarkPower', {source=self.object, damage=damage}) + end + end + end + + return { engineHandlers = { onActivated = onActivated } } + +And every actor should have a local script that processes this event: + +.. code-block:: Lua + + local function damagedByDarkPower(data) + ... -- apply `data.damage` to stats / run custom animation / etc + end + + return { + eventHandlers = { DamagedByDarkPower = damagedByDarkPower }, + } + +Someone may create an additional mod that adds a protection from the dark power. +The protection mod attaches an additional local script to every actor. The script intercepts and modifies the event: + +.. code-block:: Lua + + local protectionLevel = ... + + local function reduceDarkDamage(data) + data.damage = data.damage - protectionLevel -- reduce the damage + return data.damage > 0 -- it skips the original handler if the damage becomes <= 0 + end + + return { + eventHandlers = { DamagedByDarkPower = reduceDarkDamage }, + } + +In order to be able to intercept the event, the protection script should be placed in the load order below the original script. + +See :ref:`the list of events ` that are used by built-in scripts. + + +Timers +====== + +Timers are in the :ref:`openmw.async ` package. +They can be set either in simulation time or in game time. + +- `Simulation time`: the number of seconds in the game world (i.e. seconds when the game is not paused), passed from starting a new game. +- `Game time`: current time of the game world in seconds. Note that game time generally goes faster than the simulation time. + +When the game is paused, all timers are paused as well. + +When an object becomes inactive, timers on this object are not paused, but callbacks are called only when the object becomes active again. +For example if there were 3 timers with delays 30, 50, 90 seconds, and from the 15-th to the 65-th second the object was inactive, then the first two callbacks are both evaluated on the 65-th second and the third one -- on the 90-th second. + +There are two types: *reliable* and *unsavable* timers. + +Reliable timer +-------------- + +Reliable timers are automatically saved and restored when the game is saved or loaded. +When the game is saved each timer record contains only name of a callback, the time when the callback should be called, and an argument that should be passed to the callback. +The callback itself is not stored. That's why callbacks must be registered when the script is initialized with a function ``async:registerTimerCallback(name, func)``. +`Name` is an arbitrary string. + +An example: + +.. code-block:: Lua + + local async = require('openmw.async') + + local teleportWithDelayCallback = async:registerTimerCallback('teleport', + function(data) + data.actor:teleport(data.destCellName, data.destPos) + end) + + local function teleportWithDelay(delay, actor, cellName, pos) + async:newSimulationTimer(delay, teleportWithDelayCallback, { + actor = actor, + destCellName = cellName, + destPos = pos, + }) + end + +Unsavable timer +--------------- + +Unsavable timers can be created from any function without registering a callback in advance, but they can not be saved. +If the player saves the game when an unsavable timer is running, then the timer will be lost after reloading. +So be careful with unsavable timers and don't use them if there is a risk of leaving the game world in an inconsistent state. + +An example: + +.. code-block:: Lua + + local async = require('openmw.async') + local ui = require('openmw.ui') + + return { + engineHandlers = { + onKeyPress = function(key) + if key.symbol == 'x' then + async:newUnsavableSimulationTimer( + 10, + function() + ui.showMessage('You have pressed "X" 10 seconds ago') + end) + end + end, + } + } + +Also in `openmw_aux`_ is the helper function ``runRepeatedly``, it is implemented on top of unsavable timers: + +.. code-block:: Lua + + local core = require('openmw.core') + local time = require('openmw_aux.time') + + -- call `doSomething()` at the end of every game day. + -- the second argument (`time.day`) is the interval. + -- the periodical evaluation can be stopped at any moment by calling `stopFn()` + local timeBeforeMidnight = time.day - core.getGameTime() % time.day + local stopFn = time.runRepeatedly(doSomething, time.day, { + initialDelay = timeBeforeMidnight, + type = time.GameTime, + }) + + +Using IDE for Lua scripting +=========================== + +Find the directory ``resources/lua_api`` in your installation of OpenMW. +It describes OpenMW LuaAPI in +`LDT Documentation Language `__. +It is the source from which the :ref:`API reference ` is generated. + +If you write scripts using `Lua Development Tools `__ (eclipse-based IDE), +you can import these files to get code autocompletion and integrated OpenMW API reference. Here are the steps: + +- Install and run `LDT `__. +- Press `File` / `New` / `Lua Project` in menu. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-create-project.png + +- Specify project name (for example the title of your omwaddon) +- Set `Targeted Execution Environment` to `No Execution Environment`, and `Target Grammar` to `lua-5.1`. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-project-settings.png + +- Press `Next`, choose the `Libraries` tab, and click `Add External Source Folder`. +- Specify there paths to ``resources/lua_api`` and ``resources/vfs`` in your OpenMW installation. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-import-api.png + +- Press `Finish`. Create a new Lua file. +- Now you have code completion! Press ``Ctrl+Space`` in any place to see the variants. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion1.png + +In some cases LDT can deduce types automatically, but it is not always possible. +You can add special hints to give LDT more information: + +- Before function definition: ``--- @param TYPE argName`` +- Before variable definition: ``--- @field TYPE variableName`` + +.. code-block:: Lua + + --- @param openmw.core#GameObject obj + local function doSomething(obj) + -- autocompletion now works with `obj` + end + + --- @field openmw.util#Vector3 c + local c + + -- autocompletion now works with `c` + +.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion2.png + +In order to have autocompletion for script interfaces the information where to find these interfaces should be provided. +For example for the camera interface (defined in ``resources/vfs/scripts/omw/camera.lua``): + +.. code-block:: Lua + + --- @type Interfaces + -- @field scripts.omw.camera#Interface Camera + -- ... other interfaces here + --- @field #Interfaces I + local I = require('openmw.interfaces') + + I.Camera.disableZoom() + +See `LDT Documentation Language `__ for more details. diff --git a/docs/source/reference/lua-scripting/setting_renderers.rst b/docs/source/reference/lua-scripting/setting_renderers.rst new file mode 100644 index 0000000000..a0ce9e99f9 --- /dev/null +++ b/docs/source/reference/lua-scripting/setting_renderers.rst @@ -0,0 +1,126 @@ +Built-in Setting Renderers +========================== + +textLine +-------- + +Single line text input + +**Argument** + +Table with the following optional fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - disabled + - bool (false) + - Disables changing the setting from the UI + +checkbox +-------- + +True / false (yes/no) toggle + +**Argument** + +Table with the following optional fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - disabled + - bool (false) + - Disables changing the setting from the UI + * - l10n + - string ('Interface') + - Localization context with display values for the true/false values + * - trueLabel + - string ('Yes') + - Localization key to display for the true value + * - falseLabel + - string ('No') + - Localization key to display for the false value + +number +------ + +Numeric input + +**Argument** + +Table with the following optional fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - disabled + - bool (false) + - Disables changing the setting from the UI + * - integer + - bool (false) + - Only allow integer values + * - min + - number (nil) + - If set, restricts setting values to numbers larger than min + * - max + - number (nil) + - If set, restricts setting values to numbers smaller than max + +select +------ + +A small selection box with two next / previous arrows on the sides + +**Argument** + +Table with the following optional fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - disabled + - bool (false) + - Disables changing the setting from the UI + * - l10n + - string (required) + - Localization context with display values for items + * - items + - #list ({}) + - List of options to choose from, all the viable values of the setting + +color +----- + +Hex-code color input with a preview + +**Argument** + +Table with the following optional fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - disabled + - bool (false) + - Disables changing the setting from the UI diff --git a/docs/source/reference/lua-scripting/tables/aux_packages.rst b/docs/source/reference/lua-scripting/tables/aux_packages.rst new file mode 100644 index 0000000000..928e5821de --- /dev/null +++ b/docs/source/reference/lua-scripting/tables/aux_packages.rst @@ -0,0 +1,11 @@ ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +| Built-in library | Can be used | Description | ++=========================================================+====================+===============================================================+ +|:ref:`openmw_aux.calendar ` | everywhere | | Game time calendar | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw_aux.time ` | everywhere | | Timers and game time utils | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw_aux.ui ` | by player scripts | | User interface utils | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst new file mode 100644 index 0000000000..27f5c0ff6e --- /dev/null +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -0,0 +1,33 @@ ++------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +| Package | Can be used | Description | ++============================================================+====================+===============================================================+ +|:ref:`openmw.interfaces + diff --git a/files/openmw.appdata.xml b/files/openmw.appdata.xml index f4bdb5c919..1c22876bc8 100644 --- a/files/openmw.appdata.xml +++ b/files/openmw.appdata.xml @@ -5,8 +5,8 @@ Copyright 2020 Bret Curtis --> org.openmw.launcher.desktop - GPL-3+ - GPL-3+ + CC0-1.0 + GPL-3.0-or-later OpenMW Unofficial open source engine re-implementation of the game Morrowind @@ -23,19 +23,19 @@ Copyright 2020 Bret Curtis org.openmw.launcher.desktop - https://wiki.openmw.org/images/b/b2/Openmw_0.11.1_launcher_1.png + https://wiki.openmw.org/images/Openmw_0.11.1_launcher_1.png The OpenMW launcher - https://wiki.openmw.org/images/f/f1/Screenshot_mournhold_plaza_0.35.png + https://wiki.openmw.org/images/0.40_Screenshot-Balmora_3.png The Mournhold's plaza on OpenMW - https://wiki.openmw.org/images/5/5b/Screenshot_Vivec_seen_from_Ebonheart_0.35.png + https://wiki.openmw.org/images/Screenshot_mournhold_plaza_0.35.png Vivec seen from Ebonheart on OpenMW - http://wiki.openmw.org/images/a/a3/0.40_Screenshot-Balmora_3.png + https://wiki.openmw.org/images/Screenshot_Vivec_seen_from_Ebonheart_0.35.png Balmora at morning on OpenMW diff --git a/files/openmw.cfg b/files/openmw.cfg index afbf0810cc..00c1435212 100644 --- a/files/openmw.cfg +++ b/files/openmw.cfg @@ -2,8 +2,10 @@ # Modifications should be done on the user openmw.cfg file instead # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) -data=${MORROWIND_DATA_FILES} +content=builtin.omwscripts data-local="?userdata?data" +user-data="?userdata?" +config="?userconfig?" resources=${OPENMW_RESOURCE_FILES} script-blacklist=Museum script-blacklist=MockChangeScript @@ -69,9 +71,9 @@ fallback=Water_UnderwaterColor,012,030,037 fallback=Water_UnderwaterColorWeight,0.85 # fonts -fallback=Fonts_Font_0,magic_cards_regular -fallback=Fonts_Font_1,century_gothic_font_regular -fallback=Fonts_Font_2,daedric_font +fallback=Fonts_Font_0,pelagiad +fallback=Fonts_Font_1,dejavusansmono +fallback=Fonts_Font_2,ayembedt fallback=FontColor_color_normal,202,165,96 fallback=FontColor_color_normal_over,223,201,159 fallback=FontColor_color_normal_pressed,243,237,221 @@ -498,3 +500,28 @@ fallback=Weather_Blizzard_Cloud_Speed,7.5 fallback=Weather_Blizzard_Glare_View,0 fallback=Weather_Blizzard_Ambient_Loop_Sound_ID, Blizzard fallback=Weather_Blizzard_Storm_Threshold,.50 + +# moons +fallback=Moons_Secunda_Size,20 +fallback=Moons_Secunda_Axis_Offset,50 +fallback=Moons_Secunda_Speed,.6 +fallback=Moons_Secunda_Daily_Increment,1.2 +fallback=Moons_Secunda_Moon_Shadow_Early_Fade_Angle,0.5 +fallback=Moons_Secunda_Fade_Start_Angle,50 +fallback=Moons_Secunda_Fade_End_Angle,30 +fallback=Moons_Secunda_Fade_In_Start,14 +fallback=Moons_Secunda_Fade_In_Finish,15 +fallback=Moons_Secunda_Fade_Out_Start,7 +fallback=Moons_Secunda_Fade_Out_Finish,10 +fallback=Moons_Masser_Size,55 +fallback=Moons_Masser_Axis_Offset,35 +fallback=Moons_Masser_Speed,.5 +fallback=Moons_Masser_Daily_Increment,1 +fallback=Moons_Masser_Moon_Shadow_Early_Fade_Angle,0.5 +fallback=Moons_Masser_Fade_Start_Angle,50 +fallback=Moons_Masser_Fade_End_Angle,40 +fallback=Moons_Masser_Fade_In_Start,14 +fallback=Moons_Masser_Fade_In_Finish,15 +fallback=Moons_Masser_Fade_Out_Start,7 +fallback=Moons_Masser_Fade_Out_Finish,10 +fallback=Moons_Script_Color,255,20,20 diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local index 76f829379b..44525dc486 100644 --- a/files/openmw.cfg.local +++ b/files/openmw.cfg.local @@ -2,9 +2,10 @@ # Modifications should be done on the user openmw.cfg file instead # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) -data="?global?data" -data=./data +content=builtin.omwscripts data-local="?userdata?data" +user-data="?userdata?" +config="?userconfig?" resources=./resources script-blacklist=Museum script-blacklist=MockChangeScript @@ -70,9 +71,9 @@ fallback=Water_UnderwaterColor,012,030,037 fallback=Water_UnderwaterColorWeight,0.85 # fonts -fallback=Fonts_Font_0,magic_cards_regular -fallback=Fonts_Font_1,century_gothic_font_regular -fallback=Fonts_Font_2,daedric_font +fallback=Fonts_Font_0,pelagiad +fallback=Fonts_Font_1,dejavusansmono +fallback=Fonts_Font_2,ayembedt fallback=FontColor_color_normal,202,165,96 fallback=FontColor_color_normal_over,223,201,159 fallback=FontColor_color_normal_pressed,243,237,221 @@ -499,3 +500,28 @@ fallback=Weather_Blizzard_Cloud_Speed,7.5 fallback=Weather_Blizzard_Glare_View,0 fallback=Weather_Blizzard_Ambient_Loop_Sound_ID, Blizzard fallback=Weather_Blizzard_Storm_Threshold,.50 + +# moons +fallback=Moons_Secunda_Size,20 +fallback=Moons_Secunda_Axis_Offset,50 +fallback=Moons_Secunda_Speed,.6 +fallback=Moons_Secunda_Daily_Increment,1.2 +fallback=Moons_Secunda_Moon_Shadow_Early_Fade_Angle,0.5 +fallback=Moons_Secunda_Fade_Start_Angle,50 +fallback=Moons_Secunda_Fade_End_Angle,30 +fallback=Moons_Secunda_Fade_In_Start,14 +fallback=Moons_Secunda_Fade_In_Finish,15 +fallback=Moons_Secunda_Fade_Out_Start,7 +fallback=Moons_Secunda_Fade_Out_Finish,10 +fallback=Moons_Masser_Size,55 +fallback=Moons_Masser_Axis_Offset,35 +fallback=Moons_Masser_Speed,.5 +fallback=Moons_Masser_Daily_Increment,1 +fallback=Moons_Masser_Moon_Shadow_Early_Fade_Angle,0.5 +fallback=Moons_Masser_Fade_Start_Angle,50 +fallback=Moons_Masser_Fade_End_Angle,40 +fallback=Moons_Masser_Fade_In_Start,14 +fallback=Moons_Masser_Fade_In_Finish,15 +fallback=Moons_Masser_Fade_Out_Start,7 +fallback=Moons_Masser_Fade_Out_Finish,10 +fallback=Moons_Script_Color,255,20,20 diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 0b307ba09d..2675004907 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1,14 +1,7 @@ -# WARNING: If this file is named settings-default.cfg, then editing -# this file might have no effect, as these settings may be overwritten -# by your user settings.cfg file (see documentation for its location). +# WARNING: Users should NOT edit this file. Users should add their personal preferences to the settings.cfg file overriding this file. +# For the location of the settings.cfg file, as well as more detailed settings documentation, refer to: # -# This file provides minimal documentation for each setting, and -# ranges of recommended values. For detailed explanations of the -# significance of each setting, interaction with other settings, hard -# limits on value ranges and more information in general, please read -# the detailed documentation at: -# -# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html +# https://openmw.readthedocs.io/en/latest/reference/modding/settings/index.html # [Camera] @@ -23,7 +16,7 @@ small feature culling pixel size = 2.0 # Maximum visible distance. Caution: this setting # can dramatically affect performance, see documentation for details. -viewing distance = 6656.0 +viewing distance = 7168.0 # Camera field of view in degrees (e.g. 30.0 to 110.0). # Does not affect the player's hands in the first person camera. @@ -33,38 +26,8 @@ field of view = 60.0 # Best to leave this at the default since vanilla assets are not complete enough to adapt to high FoV's. Too low FoV would clip the hands off screen. first person field of view = 60.0 -# Distance from the camera to the character in third person mode. -third person camera distance = 192 - -# If enabled then third person camera is positioned above character's shoulder and crosshair is visible. -view over shoulder = false - -# Makes sense only if 'view over shoulder' is true. First number is horizontal offset (negative value means offset to the left), second number is vertical offset. -view over shoulder offset = 30 -10 - -# Switch shoulder automatically when player is close to an obstacle. -auto switch shoulder = true - -# Slightly pulls camera away when the character moves. Works only in 'view over shoulder' mode. Set to 0 to disable. -zoom out when move coef = 20 - -# Automatically enable preview mode when player doesn't move. -preview if stand still = false - -# Rotate the character to the view direction after exiting preview mode. -deferred preview rotation = true - -# Enables head bobbing in first person mode -head bobbing = false - -# Length of each step -head bobbing step = 90.0 - -# Amplitude of the bobbing effect -head bobbing height = 3.0 - -# Maximum camera roll angle (degrees) -head bobbing roll = 0.2 +# Reverse the depth range, reduces z-fighting of distant objects and terrain +reverse z = true [Cells] @@ -135,11 +98,14 @@ composite map resolution = 512 # Controls the maximum size of composite geometry, should be >= 1.0. With low values there will be many small chunks, with high values - lesser count of bigger chunks. max composite geometry size = 4.0 +# Draw lines arround chunks. +debug chunks = false + # Use object paging for non active cells object paging = true # Use object paging for active cells grid -object paging active grid = false +object paging active grid = true # Affects the likelyhood of objects being merged. A higher value means merging is more likely and may improve FPS at the cost of memory. object paging merge factor = 250 @@ -153,9 +119,6 @@ object paging min size merge factor = 0.3 # Controls how inexpensive an object needs to be to utilize 'min size merge factor'. object paging min size cost multiplier = 25 -# Assign a random color to merged batches. -object paging debug batches = false - [Fog] # If true, use extended fog parameters for distant terrain not controlled by @@ -174,6 +137,23 @@ distant interior fog start = 0 distant interior fog end = 16384 +# Determine fog intensity based on the distance from the eye point. +# This makes fogging independent from the viewing angle. Shaders will be used to render all objects. +radial fog = false + +# Whether to use exponential formula for fog. +exponential fog = false + +# Whether to hide the clipping plane by blending with sky. +sky blending = false + +# Fraction of the maximum distance at which blending with the sky starts. +sky blending start = 0.8 + +# The sky RTT texture size, used only for sky blending. Smaller values +# reduce quality of the sky blending, but can have slightly better performance. +sky rtt resolution = 512 256 + [Map] # Size of each exterior cell in pixels in the world map. (e.g. 12 to 24). @@ -198,6 +178,12 @@ local map widget size = 512 # If true, map in world mode, otherwise in local mode global = false +# If true, allow zoom on local and global maps +allow zooming = false + +# The local view distance in number of cells (up to the view distance) +max local viewing distance = 10 + [GUI] # Scales GUI window and widget size. (<1.0 is smaller, >1.0 is larger). @@ -361,6 +347,19 @@ trainers training skills based on base skill = false # Make stealing items from NPCs that were knocked down possible during combat. always allow stealing from knocked out actors = false +# Enables visually harvesting plants for models that support it. +graphic herbalism = true + +# Give actors an ability to swim over water surface when they follow other actor independently from their ability to swim +# (true, false) +allow actors to follow over water surface = true + +# Default size of actor for navmesh generation +default actor pathfind half extents = 29.27999496459961 28.479997634887695 66.5 + +# Enables use of day/night switch nodes +day night switches = true + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). @@ -378,6 +377,16 @@ texture min filter = linear # Texture mipmap type. (none, nearest, or linear). texture mipmap = nearest +# Show message box when screenshot is saved to a file. +notify on saved screenshot = false + +# List of the preferred languages separated by comma. +# For example "de,en" means German as the first prority and English as a fallback. +preferred locales = en + +# Buffer size for the in-game log viewer (press F10 to toggle). Zero disables the log viewer. +log buffer size = 65536 + [Shaders] # Force rendering with shaders. By default, only bump-mapped objects will use shaders. @@ -435,9 +444,45 @@ terrain specular map pattern = _diffusespec # Affected objects use shaders. apply lighting to environment maps = false -# Determine fog intensity based on the distance from the eye point. -# This makes fogging independent from the viewing angle. Shaders will be used to render all objects. -radial fog = false +# Internal handling of lights, ignored if 'force shaders' is off. "legacy" +# provides fixed function pipeline emulation."shaders compatibility" (default) +# uncaps the light limit, enables groundcover lighting, and uses a modified +# attenuation formula to reduce popping and light seams. "shaders" comes with +# all these benefits and is meant for larger light limits, but may not be +# supported on older hardware and may be slower on weaker hardware when +# 'force per pixel lighting' is enabled. +lighting method = shaders compatibility + +# Sets the bounding sphere multiplier of light sources if 'lighting method' is +# not 'legacy'. These are used to determine if an object should receive +# lighting. Higher values will allow for smoother transitions of light sources, +# but may carry a performance cost and requires a higher number of 'max lights' +# set. +light bounds multiplier = 1.65 + +# The distance from the camera at which lights fade away completely. +# Set to 0 to disable fading. +maximum light distance = 8192 + +# Fraction of the maximum distance at which lights begin to gradually fade away. +light fade start = 0.85 + +# Set maximum number of lights per object. +# When 'lighting method' is set to 'legacy', this setting will have no effect. +max lights = 8 + +# Sets minimum ambient brightness of interior cells. Levels below this threshold will have their +# ambient values adjusted to balance the darker interiors. +# When 'lighting method' is set to 'legacy', this setting will have no effect. +minimum interior brightness = 0.08 + +# Convert the alpha test (cutout/punchthrough alpha) to alpha-to-coverage. +# This allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. +# When MSAA is off, this setting will have no visible effect, but might have a performance cost. +antialias alpha test = false + +# Soften intersection of blended particle systems with opaque geometry +soft particles = false [Input] @@ -482,7 +527,7 @@ gyro horizontal axis = -x gyro vertical axis = y # The minimum gyroscope movement that is able to rotate the camera. -gyro input threshold = 0.01 +gyro input threshold = 0 # Horizontal camera axis sensitivity to gyroscope movement. gyro horizontal sensitivity = 1.0 @@ -549,8 +594,9 @@ hrtf = resolution x = 800 resolution y = 600 -# OpenMW takes complete control of the screen. -fullscreen = false +# Specify the window mode. +# 0 = Fullscreen, 1 = Windowed Fullscreen, 2 = Windowed +window mode = 2 # Determines which screen OpenMW is on. (>=0). screen = 0 @@ -594,6 +640,10 @@ refraction = false # Draw objects on water reflections. reflection detail = 2 +# Whether to use fully detailed raindrop ripples. (0, 1, 2). +# 0 = rings only; 1 = sparse, high detail; 2 = dense, high detail +rain ripple detail = 1 + # Overrides the value in '[Camera] small feature culling pixel size' specifically for water reflection/refraction textures. small feature culling pixel size = 20.0 @@ -610,10 +660,10 @@ stats x = 0.015 stats y = 0.015 stats w = 0.4275 stats h = 0.45 -stats maximized x = 0.0 -stats maximized y = 0.0 -stats maximized w = 1.0 -stats maximized h = 1.0 +stats maximized x = 0.255 +stats maximized y = 0.275 +stats maximized w = 0.4275 +stats maximized h = 0.45 stats pin = false stats hidden = false stats maximized = false @@ -623,10 +673,10 @@ spells x = 0.63 spells y = 0.39 spells w = 0.36 spells h = 0.51 -spells maximized x = 0.0 -spells maximized y = 0.0 -spells maximized w = 1.0 -spells maximized h = 1.0 +spells maximized x = 0.32 +spells maximized y = 0.02 +spells maximized w = 0.36 +spells maximized h = 0.88 spells pin = false spells hidden = false spells maximized = false @@ -636,23 +686,23 @@ map x = 0.63 map y = 0.015 map w = 0.36 map h = 0.37 -map maximized x = 0.0 -map maximized y = 0.0 -map maximized w = 1.0 -map maximized h = 1.0 +map maximized x = 0.015 +map maximized y = 0.02 +map maximized w = 0.97 +map maximized h = 0.875 map pin = false map hidden = false map maximized = false # Player inventory window when explicitly opened. -inventory x = 0.0 +inventory x = 0.015 inventory y = 0.54 inventory w = 0.45 inventory h = 0.38 -inventory maximized x = 0.0 -inventory maximized y = 0.0 -inventory maximized w = 1.0 -inventory maximized h = 1.0 +inventory maximized x = 0.015 +inventory maximized y = 0.02 +inventory maximized w = 0.97 +inventory maximized h = 0.875 inventory pin = false inventory hidden = false inventory maximized = false @@ -662,10 +712,10 @@ inventory container x = 0.015 inventory container y = 0.54 inventory container w = 0.45 inventory container h = 0.38 -inventory container maximized x = 0.0 -inventory container maximized y = 0.5 -inventory container maximized w = 1.0 -inventory container maximized h = 0.5 +inventory container maximized x = 0.015 +inventory container maximized y = 0.02 +inventory container maximized w = 0.97 +inventory container maximized h = 0.875 inventory container maximized = false # Player inventory window when bartering with a shopkeeper. @@ -673,10 +723,10 @@ inventory barter x = 0.015 inventory barter y = 0.54 inventory barter w = 0.45 inventory barter h = 0.38 -inventory barter maximized x = 0.0 -inventory barter maximized y = 0.5 -inventory barter maximized w = 1.0 -inventory barter maximized h = 0.5 +inventory barter maximized x = 0.015 +inventory barter maximized y = 0.02 +inventory barter maximized w = 0.97 +inventory barter maximized h = 0.875 inventory barter maximized = false # Player inventory window when trading with a companion. @@ -684,10 +734,10 @@ inventory companion x = 0.015 inventory companion y = 0.54 inventory companion w = 0.45 inventory companion h = 0.38 -inventory companion maximized x = 0.0 -inventory companion maximized y = 0.5 -inventory companion maximized w = 1.0 -inventory companion maximized h = 0.5 +inventory companion maximized x = 0.015 +inventory companion maximized y = 0.02 +inventory companion maximized w = 0.97 +inventory companion maximized h = 0.875 inventory companion maximized = false # Dialog window for talking with NPCs. @@ -695,10 +745,10 @@ dialogue x = 0.15 dialogue y = 0.5 dialogue w = 0.7 dialogue h = 0.45 -dialogue maximized x = 0.0 -dialogue maximized y = 0.0 -dialogue maximized w = 1.0 -dialogue maximized h = 1.0 +dialogue maximized x = 0.015 +dialogue maximized y = 0.02 +dialogue maximized w = 0.97 +dialogue maximized h = 0.875 dialogue maximized = false # Alchemy window for crafting potions. @@ -706,21 +756,21 @@ alchemy x = 0.25 alchemy y = 0.25 alchemy w = 0.5 alchemy h = 0.5 -alchemy maximized x = 0.0 -alchemy maximized y = 0.0 -alchemy maximized w = 1.0 -alchemy maximized h = 1.0 +alchemy maximized x = 0.015 +alchemy maximized y = 0.02 +alchemy maximized w = 0.97 +alchemy maximized h = 0.875 alchemy maximized = false # Console command window for debugging commands. -console x = 0.015 -console y = 0.015 -console w = 1.0 -console h = 0.5 -console maximized x = 0.0 -console maximized y = 0.0 -console maximized w = 1.0 -console maximized h = 1.0 +console x = 0.255 +console y = 0.215 +console w = 0.49 +console h = 0.3125 +console maximized x = 0.015 +console maximized y = 0.02 +console maximized w = 0.97 +console maximized h = 0.875 console maximized = false # Container inventory when searching a container. @@ -728,10 +778,10 @@ container x = 0.49 container y = 0.54 container w = 0.39 container h = 0.38 -container maximized x = 0.0 -container maximized y = 0.0 -container maximized w = 1.0 -container maximized h = 0.5 +container maximized x = 0.015 +container maximized y = 0.02 +container maximized w = 0.97 +container maximized h = 0.875 container maximized = false # NPC inventory window when bartering with a shopkeeper. @@ -739,10 +789,10 @@ barter x = 0.6 barter y = 0.27 barter w = 0.38 barter h = 0.63 -barter maximized x = 0.0 -barter maximized y = 0.0 -barter maximized w = 1.0 -barter maximized h = 0.5 +barter maximized x = 0.015 +barter maximized y = 0.02 +barter maximized w = 0.97 +barter maximized h = 0.875 barter maximized = false # NPC inventory window when trading with a companion. @@ -750,12 +800,34 @@ companion x = 0.6 companion y = 0.27 companion w = 0.38 companion h = 0.63 -companion maximized x = 0.0 -companion maximized y = 0.0 -companion maximized w = 1.0 -companion maximized h = 0.5 +companion maximized x = 0.015 +companion maximized y = 0.02 +companion maximized w = 0.97 +companion maximized h = 0.875 companion maximized = false +# Settings menu +settings x = 0.1 +settings y = 0.1 +settings w = 0.8 +settings h = 0.8 +settings maximized x = 0.015 +settings maximized y = 0.02 +settings maximized w = 0.97 +settings maximized h = 0.875 +settings maximized = false + +# Postprocessor configuration window for controlling shaders. +postprocessor h = 0.95 +postprocessor w = 0.44 +postprocessor x = 0.01 +postprocessor y = 0.02 +postprocessor maximized x = 0.015 +postprocessor maximized y = 0.02 +postprocessor maximized w = 0.97 +postprocessor maximized h = 0.875 +postprocessor maximized = false + [Navigator] # Enable navigator (true, false). When enabled background threads are started to build navmesh for world geometry. @@ -814,10 +886,10 @@ max polygons per tile = 4096 max verts per poly = 6 # Any regions with a span count smaller than this value will, if possible, be merged with larger regions. (value >= 0) -region merge size = 20 +region merge area = 400 # The minimum number of cells allowed to form isolated island areas. (value >= 0) -region min size = 8 +region min area = 64 # Number of background threads to update nav mesh (value >= 1) async nav mesh updater threads = 1 @@ -831,9 +903,6 @@ max polygon path size = 1024 # Maximum size of smoothed path (value > 0) max smooth path size = 1024 -# Maximum number of triangles in each node of mesh AABB tree (value > 0) -triangles per chunk = 256 - # Write recast mesh to file in .obj format for each use to update nav mesh (true, false) enable write recast mesh to file = false @@ -855,6 +924,9 @@ nav mesh path prefix = # Render nav mesh (true, false) enable nav mesh render = false +# Navigation mesh rendering mode (default, update frequency) +nav mesh render mode = area type + # Render agents paths (true, false) enable agents paths render = false @@ -867,6 +939,19 @@ max tiles number = 512 # Min time duration for the same tile update in milliseconds (value >= 0) min update interval ms = 250 +# Keep loading screen until navmesh is generated around the player for all tiles within manhattan distance (value >= 0). +# Distance is measured in the number of tiles and can be only an integer value. +wait until min distance to player = 5 + +# Use navigation mesh cache stored on disk (true, false) +enable nav mesh disk cache = true + +# Cache navigation mesh tiles to disk (true, false) +write to navmeshdb = true + +# Approximate maximum file size of navigation mesh cache stored on disk in bytes (value > 0) +max navmeshdb file size = 2147483648 + [Shadows] # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true. @@ -936,17 +1021,190 @@ enable indoor shadows = true # Set the number of background threads used for physics. # If no background threads are used, physics calculations are processed in the main thread # and the settings below have no effect. -async num threads = 0 +async num threads = 1 # Set the number of frames an inactive line-of-sight request will be kept # refreshed in the background physics thread cache. -# If this is set to -1, line-of-sight requests are never cached. lineofsight keep inactive cache = 0 -# Defer bounding boxes update until collision detection. -defer aabb update = true - [Models] + # Attempt to load any valid NIF file regardless of its version and track the progress. # Loading arbitrary meshes is not advised and may cause instability. load unsupported nif files = false + +# 3rd person base animation model that looks also for the corresponding kf-file +xbaseanim = meshes/xbase_anim.nif + +# 3rd person base model with textkeys-data +baseanim = meshes/base_anim.nif + +# 1st person base animation model that looks also for corresponding kf-file +xbaseanim1st = meshes/xbase_anim.1st.nif + +# 3rd person beast race base model with textkeys-data +baseanimkna = meshes/base_animkna.nif + +# 1st person beast race base animation model +baseanimkna1st = meshes/base_animkna.1st.nif + +# 3rd person female base animation model +xbaseanimfemale = meshes/xbase_anim_female.nif + +# 3rd person female base model with textkeys-data +baseanimfemale = meshes/base_anim_female.nif + +# 1st person female base model with textkeys-data +baseanimfemale1st = meshes/base_anim_female.1st.nif + +# 3rd person werewolf skin +wolfskin = meshes/wolf/skin.nif + +# 1st person werewolf skin +wolfskin1st = meshes/wolf/skin.1st.nif + +# Argonian smimkna +xargonianswimkna = meshes/xargonian_swimkna.nif + +# File to load xbaseanim 3rd person animations +xbaseanimkf = meshes/xbase_anim.kf + +# File to load xbaseanim 3rd person animations +xbaseanim1stkf = meshes/xbase_anim.1st.kf + +# File to load xbaseanim animations from +xbaseanimfemalekf = meshes/xbase_anim_female.kf + +# File to load xargonianswimkna animations from +xargonianswimknakf = meshes/xargonian_swimkna.kf + +# Sky atmosphere mesh +skyatmosphere = meshes/sky_atmosphere.nif + +# Sky clouds mesh +skyclouds = meshes/sky_clouds_01.nif + +# Sky stars mesh 01 +skynight01 = meshes/sky_night_01.nif + +# Sky stars mesh 02 +skynight02 = meshes/sky_night_02.nif + +# Ash clouds weather effect +weatherashcloud = meshes/ashcloud.nif + +# Blight clouds weather effect +weatherblightcloud = meshes/blightcloud.nif + +# Snow falling weather effect +weathersnow = meshes/snow.nif + +# Blizzard weather effect +weatherblizzard = meshes/blizzard.nif + +[Groundcover] + +# enable separate groundcover handling +enabled = false + +# A groundcover density (0.0 <= value <= 1.0) +# 1.0 means 100% density +density = 1.0 + +# A maximum distance in game units on which groundcover is rendered. +rendering distance = 6144.0 + +# Whether grass should respond to the player treading on it. +# 0 - Grass cannot be trampled. +# 1 - The player's XY position is taken into account. +# 2 - The player's height above the ground is taken into account, too. +stomp mode = 2 + +# How far away from the player grass can be before it's unaffected by being trod on, and how far it moves when it is. +# 2 - MGE XE levels. Generally excessive, but what existing mods were made with in mind +# 1 - Reduced levels. +# 0 - Gentle levels. +stomp intensity = 1 + +[Lua] + +# Enable performance-heavy debug features +lua debug = false + +# Set the maximum number of threads used for Lua scripts. +# If zero, Lua scripts are processed in the main thread. +lua num threads = 1 + +[Stereo] +# Enable/disable stereo view. This setting is ignored in VR. +stereo enabled = false + +# If enabled, OpenMW will use the GL_OVR_MultiView and GL_OVR_MultiView2 extensions where possible. +multiview = false + +# May accelerate the BruteForce method when shadows are enabled +shared shadow maps = true + +# If false, OpenMW-VR will disable display lists when using multiview. Necessary on some buggy drivers, but may incur a slight performance penalty. +allow display lists for multiview = false + +# If false, the default OSG horizontal split will be used for stereo +# If true, the config defined in the [Stereo View] settings category will be used +# Note: This option is ignored in VR, and exists primarily for debugging purposes +use custom view = false + +# If true, overrides rendering resolution for each eye. +# Note: This option is ignored in VR, and exists primarily for debugging purposes +use custom eye resolution = false + +[Stereo View] +# The default values are based on an HP Reverb G2 HMD +eye resolution x = 3128 +eye resolution y = 3060 + +# Left eye offset from center, expressed in MW units (1 meter = ~70) +left eye offset x = -2.35 +left eye offset y = 0.0 +left eye offset z = 0.0 +# Left eye orientation, expressed as a quaternion +left eye orientation x = 0.0 +left eye orientation y = 0.0 +left eye orientation z = 0.0 +left eye orientation w = 1.0 +# Left eye field of view, expressed in radians +left eye fov left = -0.86 +left eye fov right = 0.78 +left eye fov up = 0.8 +left eye fov down = -0.8 + +# Left eye offset from center, expressed in MW units (1 meter = ~70) +right eye offset x = 2.35 +right eye offset y = 0.0 +right eye offset z = 0.0 +# Left eye orientation, expressed as a quaternion +right eye orientation x = 0.0 +right eye orientation y = 0.0 +right eye orientation z = 0.0 +right eye orientation w = 1.0 +# Left eye field of view +right eye fov left = -0.78 +right eye fov right = 0.86 +right eye fov up = 0.8 +right eye fov down = -0.8 + +[Post Processing] + +# Enables post-processing +enabled = false + +# List of active shaders. This is more easily with the in-game shader HUD, by default accessible with the F2 key. +chain = + +# Reload active shaders when modified on local filesystem. This is a DEBUG mode, and should be disabled in normal gameplay. +live reload = false + +# Used for eye adaptation to control speed at which scene luminance can change from one frame to the next. No effect when HDR is not being utilized. +hdr exposure time = 0.05 + +# Transparent depth postpass. Re-renders transparent objects with alpha-clipping forced with a fixed threshold. +transparent postpass = true diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 8012c2bc10..05fe62bf0a 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -1,4 +1,4 @@ -if (NOT DEFINED OPENMW_SHADERS_ROOT) +if (NOT DEFINED OPENMW_RESOURCES_ROOT) return() endif() @@ -7,14 +7,25 @@ set(SDIR ${CMAKE_CURRENT_SOURCE_DIR}) set(DDIRRELATIVE resources/shaders) set(SHADER_FILES + groundcover_vertex.glsl + groundcover_fragment.glsl water_vertex.glsl water_fragment.glsl water_nm.png + alpha.glsl + depth.glsl objects_vertex.glsl objects_fragment.glsl + openmw_fragment.glsl + openmw_fragment.h.glsl + openmw_fragment_multiview.glsl + openmw_vertex.glsl + openmw_vertex.h.glsl + openmw_vertex_multiview.glsl terrain_vertex.glsl terrain_fragment.glsl lighting.glsl + lighting_util.glsl parallax.glsl s360_fragment.glsl s360_vertex.glsl @@ -22,6 +33,28 @@ set(SHADER_FILES shadows_fragment.glsl shadowcasting_vertex.glsl shadowcasting_fragment.glsl + vertexcolors.glsl + multiview_resolve_vertex.glsl + multiview_resolve_fragment.glsl + nv_default_vertex.glsl + nv_default_fragment.glsl + nv_nolighting_vertex.glsl + nv_nolighting_fragment.glsl + blended_depth_postpass_vertex.glsl + blended_depth_postpass_fragment.glsl + gui_vertex.glsl + gui_fragment.glsl + debug_vertex.glsl + debug_fragment.glsl + sky_vertex.glsl + sky_fragment.glsl + skypasses.glsl + softparticles.glsl + hdr_fragment.glsl + hdr_luminance_fragment.glsl + fullscreen_tri_vertex.glsl + fullscreen_tri_fragment.glsl + fog.glsl ) -copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/alpha.glsl b/files/shaders/alpha.glsl new file mode 100644 index 0000000000..46b748236c --- /dev/null +++ b/files/shaders/alpha.glsl @@ -0,0 +1,85 @@ + +#define FUNC_NEVER 512 // 0x0200 +#define FUNC_LESS 513 // 0x0201 +#define FUNC_EQUAL 514 // 0x0202 +#define FUNC_LEQUAL 515 // 0x0203 +#define FUNC_GREATER 516 // 0x0204 +#define FUNC_NOTEQUAL 517 // 0x0205 +#define FUNC_GEQUAL 518 // 0x0206 +#define FUNC_ALWAYS 519 // 0x0207 + +#if @alphaFunc != FUNC_ALWAYS && @alphaFunc != FUNC_NEVER +uniform float alphaRef; +#endif + +float mipmapLevel(vec2 scaleduv) +{ + vec2 dUVdx = dFdx(scaleduv); + vec2 dUVdy = dFdy(scaleduv); + float maxDUVSquared = max(dot(dUVdx, dUVdx), dot(dUVdy, dUVdy)); + return max(0.0, 0.5 * log2(maxDUVSquared)); +} + +float coveragePreservingAlphaScale(sampler2D diffuseMap, vec2 uv) +{ + #if @adjustCoverage + vec2 textureSize; + #if @useGPUShader4 + textureSize = textureSize2D(diffuseMap, 0); + #else + textureSize = vec2(256.0); + #endif + return 1.0 + mipmapLevel(uv * textureSize) * 0.25; + #else + return 1.0; + #endif +} + +void alphaTest() +{ + #if @alphaToCoverage + float coverageAlpha = (gl_FragData[0].a - clamp(alphaRef, 0.0001, 0.9999)) / max(fwidth(gl_FragData[0].a), 0.0001) + 0.5; + + // Some functions don't make sense with A2C or are a pain to think about and no meshes use them anyway + // Use regular alpha testing in such cases until someone complains. + #if @alphaFunc == FUNC_NEVER + discard; + #elif @alphaFunc == FUNC_LESS + gl_FragData[0].a = 1.0 - coverageAlpha; + #elif @alphaFunc == FUNC_EQUAL + if (gl_FragData[0].a != alphaRef) + discard; + #elif @alphaFunc == FUNC_LEQUAL + gl_FragData[0].a = 1.0 - coverageAlpha; + #elif @alphaFunc == FUNC_GREATER + gl_FragData[0].a = coverageAlpha; + #elif @alphaFunc == FUNC_NOTEQUAL + if (gl_FragData[0].a == alphaRef) + discard; + #elif @alphaFunc == FUNC_GEQUAL + gl_FragData[0].a = coverageAlpha; + #endif + #else + #if @alphaFunc == FUNC_NEVER + discard; + #elif @alphaFunc == FUNC_LESS + if (gl_FragData[0].a >= alphaRef) + discard; + #elif @alphaFunc == FUNC_EQUAL + if (gl_FragData[0].a != alphaRef) + discard; + #elif @alphaFunc == FUNC_LEQUAL + if (gl_FragData[0].a > alphaRef) + discard; + #elif @alphaFunc == FUNC_GREATER + if (gl_FragData[0].a <= alphaRef) + discard; + #elif @alphaFunc == FUNC_NOTEQUAL + if (gl_FragData[0].a == alphaRef) + discard; + #elif @alphaFunc == FUNC_GEQUAL + if (gl_FragData[0].a < alphaRef) + discard; + #endif + #endif +} \ No newline at end of file diff --git a/files/shaders/blended_depth_postpass_fragment.glsl b/files/shaders/blended_depth_postpass_fragment.glsl new file mode 100644 index 0000000000..61e8b4ea7e --- /dev/null +++ b/files/shaders/blended_depth_postpass_fragment.glsl @@ -0,0 +1,19 @@ +#version 120 + +uniform sampler2D diffuseMap; + +varying vec2 diffuseMapUV; +varying float alphaPassthrough; + +void main() +{ + float alpha = texture2D(diffuseMap, diffuseMapUV).a * alphaPassthrough; + + const float alphaRef = 0.5; + + if (alpha < alphaRef) + discard; + + // DO NOT write to color! + // This is a post-pass of transparent objects in charge of only updating depth buffer. +} diff --git a/files/shaders/blended_depth_postpass_vertex.glsl b/files/shaders/blended_depth_postpass_vertex.glsl new file mode 100644 index 0000000000..ee27507983 --- /dev/null +++ b/files/shaders/blended_depth_postpass_vertex.glsl @@ -0,0 +1,21 @@ +#version 120 + +uniform mat4 projectionMatrix; + +varying vec2 diffuseMapUV; +varying float alphaPassthrough; + +#include "openmw_vertex.h.glsl" +#include "vertexcolors.glsl" + +void main() +{ + gl_Position = mw_modelToClip(gl_Vertex); + + if (colorMode == 2) + alphaPassthrough = gl_Color.a; + else + alphaPassthrough = gl_FrontMaterial.diffuse.a; + + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; +} diff --git a/files/shaders/debug_fragment.glsl b/files/shaders/debug_fragment.glsl new file mode 100644 index 0000000000..1b25510d66 --- /dev/null +++ b/files/shaders/debug_fragment.glsl @@ -0,0 +1,8 @@ +#version 120 + +#include "vertexcolors.glsl" + +void main() +{ + gl_FragData[0] = getDiffuseColor(); +} diff --git a/files/shaders/debug_vertex.glsl b/files/shaders/debug_vertex.glsl new file mode 100644 index 0000000000..fd41a6ff48 --- /dev/null +++ b/files/shaders/debug_vertex.glsl @@ -0,0 +1,12 @@ +#version 120 + +#include "openmw_vertex.h.glsl" + +centroid varying vec4 passColor; + +void main() +{ + gl_Position = mw_modelToClip(gl_Vertex); + + passColor = gl_Color; +} diff --git a/files/shaders/depth.glsl b/files/shaders/depth.glsl new file mode 100644 index 0000000000..aa6f54b99d --- /dev/null +++ b/files/shaders/depth.glsl @@ -0,0 +1,12 @@ +#if @reverseZ +uniform float linearFac; +#endif + +float getLinearDepth(in float z, in float viewZ) +{ +#if @reverseZ + return linearFac*viewZ; +#else + return z; +#endif +} \ No newline at end of file diff --git a/files/shaders/fog.glsl b/files/shaders/fog.glsl new file mode 100644 index 0000000000..d9d0548aa9 --- /dev/null +++ b/files/shaders/fog.glsl @@ -0,0 +1,43 @@ +uniform float far; + +#if @skyBlending +#include "openmw_fragment.h.glsl" + +uniform float skyBlendingStart; +#endif + +vec4 applyFogAtDist(vec4 color, float euclideanDist, float linearDist) +{ +#if @radialFog + float dist = euclideanDist; +#else + float dist = abs(linearDist); +#endif +#if @exponentialFog + float fogValue = 1.0 - exp(-2.0 * max(0.0, dist - gl_Fog.start/2.0) / (gl_Fog.end - gl_Fog.start/2.0)); +#else + float fogValue = clamp((dist - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); +#endif +#ifdef ADDITIVE_BLENDING + color.xyz *= 1.0 - fogValue; +#else + color.xyz = mix(color.xyz, gl_Fog.color.xyz, fogValue); +#endif + +#if @skyBlending + float fadeValue = clamp((far - dist) / (far - skyBlendingStart), 0.0, 1.0); + fadeValue *= fadeValue; +#ifdef ADDITIVE_BLENDING + color.xyz *= fadeValue; +#else + color.xyz = mix(mw_sampleSkyColor(gl_FragCoord.xy / screenRes), color.xyz, fadeValue); +#endif +#endif + + return color; +} + +vec4 applyFogAtPos(vec4 color, vec3 pos) +{ + return applyFogAtDist(color, length(pos), pos.z); +} diff --git a/files/shaders/fullscreen_tri_fragment.glsl b/files/shaders/fullscreen_tri_fragment.glsl new file mode 100644 index 0000000000..b71f98365d --- /dev/null +++ b/files/shaders/fullscreen_tri_fragment.glsl @@ -0,0 +1,10 @@ +#version 120 + +varying vec2 uv; + +#include "openmw_fragment.h.glsl" + +void main() +{ + gl_FragColor = mw_samplerLastShader(uv); +} \ No newline at end of file diff --git a/files/shaders/fullscreen_tri_vertex.glsl b/files/shaders/fullscreen_tri_vertex.glsl new file mode 100644 index 0000000000..1166a44a54 --- /dev/null +++ b/files/shaders/fullscreen_tri_vertex.glsl @@ -0,0 +1,11 @@ +#version 120 + +varying vec2 uv; + +#include "openmw_vertex.h.glsl" + +void main() +{ + gl_Position = vec4(gl_Vertex.xy, 0.0, 1.0); + uv = gl_Position.xy * 0.5 + 0.5; +} diff --git a/files/shaders/groundcover_fragment.glsl b/files/shaders/groundcover_fragment.glsl new file mode 100644 index 0000000000..0bdc3d915d --- /dev/null +++ b/files/shaders/groundcover_fragment.glsl @@ -0,0 +1,94 @@ +#version 120 + +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#define GROUNDCOVER + +#if @diffuseMap +uniform sampler2D diffuseMap; +varying vec2 diffuseMapUV; +#endif + +#if @normalMap +uniform sampler2D normalMap; +varying vec2 normalMapUV; +varying vec4 passTangent; +#endif + +// Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting. +// They may do not look as intended with per-pixel lighting, so ignore this setting for now. +#define PER_PIXEL_LIGHTING @normalMap + +varying float euclideanDepth; +varying float linearDepth; +uniform vec2 screenRes; + +#if PER_PIXEL_LIGHTING +varying vec3 passViewPos; +#else +centroid varying vec3 passLighting; +centroid varying vec3 shadowDiffuseLighting; +#endif + +varying vec3 passNormal; + +#include "shadows_fragment.glsl" +#include "lighting.glsl" +#include "alpha.glsl" +#include "fog.glsl" + +void main() +{ + vec3 worldNormal = normalize(passNormal); + +#if @normalMap + vec4 normalTex = texture2D(normalMap, normalMapUV); + + vec3 normalizedNormal = worldNormal; + vec3 normalizedTangent = normalize(passTangent.xyz); + vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; + mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); + + worldNormal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + vec3 viewNormal = gl_NormalMatrix * worldNormal; +#endif + +#if @diffuseMap + gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); +#else + gl_FragData[0] = vec4(1.0); +#endif + + if (euclideanDepth > @groundcoverFadeStart) + gl_FragData[0].a *= 1.0-smoothstep(@groundcoverFadeStart, @groundcoverFadeEnd, euclideanDepth); + + alphaTest(); + + float shadowing = unshadowedLightRatio(linearDepth); + + vec3 lighting; +#if !PER_PIXEL_LIGHTING + lighting = passLighting + shadowDiffuseLighting * shadowing; +#else + vec3 diffuseLight, ambientLight; + doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); + lighting = diffuseLight + ambientLight; +#endif + + clampLightingResult(lighting); + + gl_FragData[0].xyz *= lighting; + gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth); + +#if !@disableNormals + gl_FragData[1].xyz = worldNormal * 0.5 + 0.5; +#endif + + applyShadowDebugOverlay(); +} diff --git a/files/shaders/groundcover_vertex.glsl b/files/shaders/groundcover_vertex.glsl new file mode 100644 index 0000000000..aa9dea3355 --- /dev/null +++ b/files/shaders/groundcover_vertex.glsl @@ -0,0 +1,178 @@ +#version 120 + +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#include "openmw_vertex.h.glsl" + +#define GROUNDCOVER + +attribute vec4 aOffset; +attribute vec3 aRotation; + +#if @diffuseMap +varying vec2 diffuseMapUV; +#endif + +#if @normalMap +varying vec2 normalMapUV; +varying vec4 passTangent; +#endif + +// Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting. +// They may do not look as intended with per-pixel lighting, so ignore this setting for now. +#define PER_PIXEL_LIGHTING @normalMap + +varying float euclideanDepth; +varying float linearDepth; + +#if PER_PIXEL_LIGHTING +varying vec3 passViewPos; +#else +centroid varying vec3 passLighting; +centroid varying vec3 shadowDiffuseLighting; +#endif + +varying vec3 passNormal; + +#include "shadows_vertex.glsl" +#include "lighting.glsl" +#include "depth.glsl" + +uniform float osg_SimulationTime; +uniform mat4 osg_ViewMatrixInverse; +uniform mat4 osg_ViewMatrix; +uniform float windSpeed; +uniform vec3 playerPos; + +#if @groundcoverStompMode == 0 +#else + #define STOMP 1 + #if @groundcoverStompMode == 2 + #define STOMP_HEIGHT_SENSITIVE 1 + #endif + #define STOMP_INTENSITY_LEVEL @groundcoverStompIntensity +#endif + +vec2 groundcoverDisplacement(in vec3 worldpos, float h) +{ + vec2 windDirection = vec2(1.0); + vec3 footPos = playerPos; + vec3 windVec = vec3(windSpeed * windDirection, 1.0); + + float v = length(windVec); + vec2 displace = vec2(2.0 * windVec + 0.1); + vec2 harmonics = vec2(0.0); + + harmonics += vec2((1.0 - 0.10*v) * sin(1.0*osg_SimulationTime + worldpos.xy / 1100.0)); + harmonics += vec2((1.0 - 0.04*v) * cos(2.0*osg_SimulationTime + worldpos.xy / 750.0)); + harmonics += vec2((1.0 + 0.14*v) * sin(3.0*osg_SimulationTime + worldpos.xy / 500.0)); + harmonics += vec2((1.0 + 0.28*v) * sin(5.0*osg_SimulationTime + worldpos.xy / 200.0)); + + vec2 stomp = vec2(0.0); +#if STOMP + float d = length(worldpos.xy - footPos.xy); +#if STOMP_INTENSITY_LEVEL == 0 + // Gentle intensity + const float STOMP_RANGE = 50.0; // maximum distance from player that grass is affected by stomping + const float STOMP_DISTANCE = 20.0; // maximum distance stomping can move grass +#elif STOMP_INTENSITY_LEVEL == 1 + // Reduced intensity + const float STOMP_RANGE = 80.0; + const float STOMP_DISTANCE = 40.0; +#elif STOMP_INTENSITY_LEVEL == 2 + // MGE XE intensity + const float STOMP_RANGE = 150.0; + const float STOMP_DISTANCE = 60.0; +#endif + if (d < STOMP_RANGE && d > 0.0) + stomp = (STOMP_DISTANCE / d - STOMP_DISTANCE / STOMP_RANGE) * (worldpos.xy - footPos.xy); + +#ifdef STOMP_HEIGHT_SENSITIVE + stomp *= clamp((worldpos.z - footPos.z) / h, 0.0, 1.0); +#endif +#endif + + return clamp(0.02 * h, 0.0, 1.0) * (harmonics * displace + stomp); +} + +mat4 rotation(in vec3 angle) +{ + float sin_x = sin(angle.x); + float cos_x = cos(angle.x); + float sin_y = sin(angle.y); + float cos_y = cos(angle.y); + float sin_z = sin(angle.z); + float cos_z = cos(angle.z); + + return mat4( + cos_z*cos_y+sin_x*sin_y*sin_z, -sin_z*cos_x, cos_z*sin_y+sin_z*sin_x*cos_y, 0.0, + sin_z*cos_y+cos_z*sin_x*sin_y, cos_z*cos_x, sin_z*sin_y-cos_z*sin_x*cos_y, 0.0, + -sin_y*cos_x, sin_x, cos_x*cos_y, 0.0, + 0.0, 0.0, 0.0, 1.0); +} + +mat3 rotation3(in mat4 rot4) +{ + return mat3( + rot4[0].xyz, + rot4[1].xyz, + rot4[2].xyz); +} + +void main(void) +{ + vec3 position = aOffset.xyz; + float scale = aOffset.w; + + mat4 rotation = rotation(aRotation); + vec4 displacedVertex = rotation * scale * gl_Vertex; + + displacedVertex = vec4(displacedVertex.xyz + position, 1.0); + + vec4 worldPos = osg_ViewMatrixInverse * gl_ModelViewMatrix * displacedVertex; + worldPos.xy += groundcoverDisplacement(worldPos.xyz, gl_Vertex.z); + vec4 viewPos = osg_ViewMatrix * worldPos; + + gl_ClipVertex = viewPos; + euclideanDepth = length(viewPos.xyz); + + if (length(gl_ModelViewMatrix * vec4(position, 1.0)) > @groundcoverFadeEnd) + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); + else + gl_Position = mw_viewToClip(viewPos); + + linearDepth = getLinearDepth(gl_Position.z, viewPos.z); + +#if (!PER_PIXEL_LIGHTING || @shadows_enabled) + vec3 viewNormal = normalize((gl_NormalMatrix * rotation3(rotation) * gl_Normal).xyz); +#endif + +#if @diffuseMap + diffuseMapUV = (gl_TextureMatrix[@diffuseMapUV] * gl_MultiTexCoord@diffuseMapUV).xy; +#endif + +#if @normalMap + normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy; + passTangent = gl_MultiTexCoord7.xyzw * rotation; +#endif + + passNormal = rotation3(rotation) * gl_Normal.xyz; +#if PER_PIXEL_LIGHTING + passViewPos = viewPos.xyz; +#else + vec3 diffuseLight, ambientLight; + doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); + passLighting = diffuseLight + ambientLight; + clampLightingResult(passLighting); +#endif + +#if (@shadows_enabled) + setupShadowCoords(viewPos, viewNormal); +#endif +} diff --git a/files/shaders/gui_fragment.glsl b/files/shaders/gui_fragment.glsl new file mode 100644 index 0000000000..a8c9434711 --- /dev/null +++ b/files/shaders/gui_fragment.glsl @@ -0,0 +1,11 @@ +#version 120 + +uniform sampler2D diffuseMap; + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void main() +{ + gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV) * passColor; +} diff --git a/files/shaders/gui_vertex.glsl b/files/shaders/gui_vertex.glsl new file mode 100644 index 0000000000..b378b097bd --- /dev/null +++ b/files/shaders/gui_vertex.glsl @@ -0,0 +1,11 @@ +#version 120 + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void main() +{ + gl_Position = vec4(gl_Vertex.xyz, 1.0); + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + passColor = gl_Color; +} diff --git a/files/shaders/hdr_fragment.glsl b/files/shaders/hdr_fragment.glsl new file mode 100644 index 0000000000..5490a6873d --- /dev/null +++ b/files/shaders/hdr_fragment.glsl @@ -0,0 +1,14 @@ +#version 120 + +varying vec2 uv; +uniform sampler2D luminanceSceneTex; +uniform sampler2D prevLuminanceSceneTex; + +void main() +{ + float prevLum = texture2D(prevLuminanceSceneTex, vec2(0.5, 0.5)).r; + + float l = texture2D(luminanceSceneTex, vec2(0.5, 0.5)).r; + float weightedAvgLum = exp2((l * @logLumRange) + @minLog); + gl_FragColor.r = prevLum + (weightedAvgLum - prevLum) * @hdrExposureTime; +} diff --git a/files/shaders/hdr_luminance_fragment.glsl b/files/shaders/hdr_luminance_fragment.glsl new file mode 100644 index 0000000000..f78dc41dfa --- /dev/null +++ b/files/shaders/hdr_luminance_fragment.glsl @@ -0,0 +1,17 @@ +#version 120 + +varying vec2 uv; +uniform sampler2D sceneTex; + +void main() +{ + float lum = dot(texture2D(sceneTex, uv).rgb, vec3(0.2126, 0.7152, 0.0722)); + + if (lum < @epsilon) + { + gl_FragColor.r = 0.0; + return; + } + + gl_FragColor.r = clamp((log2(lum) - @minLog) * @invLogLumRange, 0.0, 1.0); +} diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index 1ed162eeac..177017f822 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -1,95 +1,99 @@ -#define MAX_LIGHTS 8 +#include "lighting_util.glsl" -uniform int colorMode; - -const int ColorMode_None = 0; -const int ColorMode_Emission = 1; -const int ColorMode_AmbientAndDiffuse = 2; -const int ColorMode_Ambient = 3; -const int ColorMode_Diffuse = 4; -const int ColorMode_Specular = 5; - -void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal, vec4 diffuse, vec3 ambient) +void perLightSun(out vec3 diffuseOut, vec3 viewPos, vec3 viewNormal) { - vec3 lightDir; - float lightDistance; + vec3 lightDir = normalize(lcalcPosition(0)); + float lambert = dot(viewNormal.xyz, lightDir); - lightDir = gl_LightSource[lightIndex].position.xyz - (viewPos.xyz * gl_LightSource[lightIndex].position.w); - lightDistance = length(lightDir); - lightDir = normalize(lightDir); - float illumination = clamp(1.0 / (gl_LightSource[lightIndex].constantAttenuation + gl_LightSource[lightIndex].linearAttenuation * lightDistance + gl_LightSource[lightIndex].quadraticAttenuation * lightDistance * lightDistance), 0.0, 1.0); +#ifndef GROUNDCOVER + lambert = max(lambert, 0.0); +#else + float eyeCosine = dot(normalize(viewPos), viewNormal.xyz); + if (lambert < 0.0) + { + lambert = -lambert; + eyeCosine = -eyeCosine; + } + lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); +#endif - ambientOut = ambient * gl_LightSource[lightIndex].ambient.xyz * illumination; - diffuseOut = diffuse.xyz * gl_LightSource[lightIndex].diffuse.xyz * max(dot(viewNormal.xyz, lightDir), 0.0) * illumination; + diffuseOut = lcalcDiffuse(0).xyz * lambert; } -#if PER_PIXEL_LIGHTING -vec4 doLighting(vec3 viewPos, vec3 viewNormal, vec4 vertexColor, float shadowing) -#else -vec4 doLighting(vec3 viewPos, vec3 viewNormal, vec4 vertexColor, out vec3 shadowDiffuse) -#endif +void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) { - vec4 diffuse; - vec3 ambient; - if (colorMode == ColorMode_AmbientAndDiffuse) - { - diffuse = vertexColor; - ambient = vertexColor.xyz; - } - else if (colorMode == ColorMode_Diffuse) - { - diffuse = vertexColor; - ambient = gl_FrontMaterial.ambient.xyz; - } - else if (colorMode == ColorMode_Ambient) + vec3 lightPos = lcalcPosition(lightIndex) - viewPos; + float lightDistance = length(lightPos); + +// cull non-FFP point lighting by radius, light is guaranteed to not fall outside this bound with our cutoff +#if !@lightingMethodFFP + float radius = lcalcRadius(lightIndex); + + if (lightDistance > radius * 2.0) { - diffuse = gl_FrontMaterial.diffuse; - ambient = vertexColor.xyz; + ambientOut = vec3(0.0); + diffuseOut = vec3(0.0); + return; } - else +#endif + + lightPos = normalize(lightPos); + + float illumination = lcalcIllumination(lightIndex, lightDistance); + ambientOut = lcalcAmbient(lightIndex) * illumination; + float lambert = dot(viewNormal.xyz, lightPos) * illumination; + +#ifndef GROUNDCOVER + lambert = max(lambert, 0.0); +#else + float eyeCosine = dot(normalize(viewPos), viewNormal.xyz); + if (lambert < 0.0) { - diffuse = gl_FrontMaterial.diffuse; - ambient = gl_FrontMaterial.ambient.xyz; + lambert = -lambert; + eyeCosine = -eyeCosine; } - vec4 lightResult = vec4(0.0, 0.0, 0.0, diffuse.a); + lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); +#endif + + diffuseOut = lcalcDiffuse(lightIndex) * lambert; +} - vec3 diffuseLight, ambientLight; - perLight(ambientLight, diffuseLight, 0, viewPos, viewNormal, diffuse, ambient); #if PER_PIXEL_LIGHTING - lightResult.xyz += diffuseLight * shadowing - diffuseLight; // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. +void doLighting(vec3 viewPos, vec3 viewNormal, float shadowing, out vec3 diffuseLight, out vec3 ambientLight) #else - shadowDiffuse = diffuseLight; - lightResult.xyz -= shadowDiffuse; // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. +void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 ambientLight, out vec3 shadowDiffuse) #endif - for (int i=0; i> shift.x) & mask)) / 255.0) + ,(float(((data >> shift.y) & mask)) / 255.0) + ,(float(((data >> shift.z) & mask)) / 255.0)); +} + +vec4 unpackRGBA(int data) +{ + return vec4( (float(((data >> shift.x) & mask)) / 255.0) + ,(float(((data >> shift.y) & mask)) / 255.0) + ,(float(((data >> shift.z) & mask)) / 255.0) + ,(float(((data >> shift.w) & mask)) / 255.0)); +} + +/* Layout: +packedColors: 8-bit unsigned RGB packed as (diffuse, ambient, specular). + sign bit is stored in unused alpha component +attenuation: constant, linear, quadratic, light radius (as defined in content) +*/ +struct LightData +{ + ivec4 packedColors; + vec4 position; + vec4 attenuation; +}; + +uniform int PointLightIndex[@maxLights]; +uniform int PointLightCount; + +// Defaults to shared layout. If we ever move to GLSL 140, std140 layout should be considered +uniform LightBufferBinding +{ + LightData LightBuffer[@maxLightsInScene]; +}; + +#elif @lightingMethodPerObjectUniform + +/* Layout: +--------------------------------------- ----------- +| pos_x | ambi_r | diff_r | spec_r | +| pos_y | ambi_g | diff_g | spec_g | +| pos_z | ambi_b | diff_b | spec_b | +| att_c | att_l | att_q | radius/spec_a | + -------------------------------------------------- +*/ +uniform mat4 LightBuffer[@maxLights]; +uniform int PointLightCount; + +#endif + +#if !@lightingMethodFFP +float lcalcRadius(int lightIndex) +{ +#if @lightingMethodPerObjectUniform + return @getLight[lightIndex][3].w; +#else + return @getLight[lightIndex].attenuation.w; +#endif +} +#endif + +float lcalcIllumination(int lightIndex, float lightDistance) +{ +#if @lightingMethodPerObjectUniform + float illumination = clamp(1.0 / (@getLight[lightIndex][0].w + @getLight[lightIndex][1].w * lightDistance + @getLight[lightIndex][2].w * lightDistance * lightDistance), 0.0, 1.0); + return (illumination * (1.0 - quickstep((lightDistance / lcalcRadius(lightIndex)) - 1.0))); +#elif @lightingMethodUBO + float illumination = clamp(1.0 / (@getLight[lightIndex].attenuation.x + @getLight[lightIndex].attenuation.y * lightDistance + @getLight[lightIndex].attenuation.z * lightDistance * lightDistance), 0.0, 1.0); + return (illumination * (1.0 - quickstep((lightDistance / lcalcRadius(lightIndex)) - 1.0))); +#else + return clamp(1.0 / (@getLight[lightIndex].constantAttenuation + @getLight[lightIndex].linearAttenuation * lightDistance + @getLight[lightIndex].quadraticAttenuation * lightDistance * lightDistance), 0.0, 1.0); +#endif +} + +vec3 lcalcPosition(int lightIndex) +{ +#if @lightingMethodPerObjectUniform + return @getLight[lightIndex][0].xyz; +#else + return @getLight[lightIndex].position.xyz; +#endif +} + +vec3 lcalcDiffuse(int lightIndex) +{ +#if @lightingMethodPerObjectUniform + return @getLight[lightIndex][2].xyz; +#elif @lightingMethodUBO + return unpackRGB(@getLight[lightIndex].packedColors.x) * float(@getLight[lightIndex].packedColors.w); +#else + return @getLight[lightIndex].diffuse.xyz; +#endif +} + +vec3 lcalcAmbient(int lightIndex) +{ +#if @lightingMethodPerObjectUniform + return @getLight[lightIndex][1].xyz; +#elif @lightingMethodUBO + return unpackRGB(@getLight[lightIndex].packedColors.y); +#else + return @getLight[lightIndex].ambient.xyz; +#endif +} + +vec4 lcalcSpecular(int lightIndex) +{ +#if @lightingMethodPerObjectUniform + return @getLight[lightIndex][3]; +#elif @lightingMethodUBO + return unpackRGBA(@getLight[lightIndex].packedColors.z); +#else + return @getLight[lightIndex].specular; +#endif +} + +void clampLightingResult(inout vec3 lighting) +{ +#if @clamp + lighting = clamp(lighting, vec3(0.0), vec3(1.0)); +#else + lighting = max(lighting, 0.0); +#endif +} diff --git a/files/shaders/multiview_resolve_fragment.glsl b/files/shaders/multiview_resolve_fragment.glsl new file mode 100644 index 0000000000..c0a137464f --- /dev/null +++ b/files/shaders/multiview_resolve_fragment.glsl @@ -0,0 +1,18 @@ +#version 120 +#extension GL_EXT_texture_array : require + +varying vec2 uv; +uniform sampler2DArray omw_SamplerLastShader; + +void main() +{ + int view = 0; + vec3 uvz = vec3(uv.x * 2., uv.y, 0); + if(uvz.x > 1.) + { + uvz.x -= 1.; + uvz.z = 1; + } + + gl_FragColor = texture2DArray(omw_SamplerLastShader, uvz); +} \ No newline at end of file diff --git a/files/shaders/multiview_resolve_vertex.glsl b/files/shaders/multiview_resolve_vertex.glsl new file mode 100644 index 0000000000..794e0827c4 --- /dev/null +++ b/files/shaders/multiview_resolve_vertex.glsl @@ -0,0 +1,9 @@ +#version 120 + +varying vec2 uv; + +void main() +{ + gl_Position = vec4(gl_Vertex.xy, 0.0, 1.0); + uv = gl_Position.xy * 0.5 + 0.5; +} diff --git a/files/shaders/nv_default_fragment.glsl b/files/shaders/nv_default_fragment.glsl new file mode 100644 index 0000000000..57ef7b14df --- /dev/null +++ b/files/shaders/nv_default_fragment.glsl @@ -0,0 +1,110 @@ +#version 120 +#pragma import_defines(FORCE_OPAQUE) + +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#if @diffuseMap +uniform sampler2D diffuseMap; +varying vec2 diffuseMapUV; +#endif + +#if @emissiveMap +uniform sampler2D emissiveMap; +varying vec2 emissiveMapUV; +#endif + +#if @normalMap +uniform sampler2D normalMap; +varying vec2 normalMapUV; +varying vec4 passTangent; +#endif + +varying float euclideanDepth; +varying float linearDepth; + +#define PER_PIXEL_LIGHTING 1 + +varying vec3 passViewPos; +varying vec3 passNormal; + +uniform vec2 screenRes; + +#include "vertexcolors.glsl" +#include "shadows_fragment.glsl" +#include "lighting.glsl" +#include "alpha.glsl" +#include "fog.glsl" + +uniform float emissiveMult; +uniform float specStrength; + +void main() +{ + vec3 worldNormal = normalize(passNormal); + +#if @diffuseMap + gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV); +#else + gl_FragData[0] = vec4(1.0); +#endif + + vec4 diffuseColor = getDiffuseColor(); + gl_FragData[0].a *= diffuseColor.a; + alphaTest(); + +#if @normalMap + vec4 normalTex = texture2D(normalMap, normalMapUV); + + vec3 normalizedNormal = worldNormal; + vec3 normalizedTangent = normalize(passTangent.xyz); + vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; + mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); + + worldNormal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + vec3 viewNormal = gl_NormalMatrix * worldNormal; +#else + vec3 viewNormal = gl_NormalMatrix * worldNormal; +#endif + + float shadowing = unshadowedLightRatio(linearDepth); + vec3 diffuseLight, ambientLight; + doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); + vec3 emission = getEmissionColor().xyz * emissiveMult; +#if @emissiveMap + emission *= texture2D(emissiveMap, emissiveMapUV).xyz; +#endif + vec3 lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; + + clampLightingResult(lighting); + + gl_FragData[0].xyz *= lighting; + + float shininess = gl_FrontMaterial.shininess; + vec3 matSpec = getSpecularColor().xyz * specStrength; +#if @normalMap + matSpec *= normalTex.a; +#endif + + if (matSpec != vec3(0.0)) + gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing; + + gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth); + +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE + // having testing & blending isn't enough - we need to write an opaque pixel to be opaque + gl_FragData[0].a = 1.0; +#endif + +#if !defined(FORCE_OPAQUE) && !@disableNormals + gl_FragData[1].xyz = worldNormal * 0.5 + 0.5; +#endif + + applyShadowDebugOverlay(); +} diff --git a/files/shaders/nv_default_vertex.glsl b/files/shaders/nv_default_vertex.glsl new file mode 100644 index 0000000000..0d014b02fa --- /dev/null +++ b/files/shaders/nv_default_vertex.glsl @@ -0,0 +1,68 @@ +#version 120 + +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#include "openmw_vertex.h.glsl" +#if @diffuseMap +varying vec2 diffuseMapUV; +#endif + +#if @emissiveMap +varying vec2 emissiveMapUV; +#endif + +#if @normalMap +varying vec2 normalMapUV; +varying vec4 passTangent; +#endif + +varying float euclideanDepth; +varying float linearDepth; + +varying vec3 passViewPos; +varying vec3 passNormal; + +#define PER_PIXEL_LIGHTING 1 + +#include "vertexcolors.glsl" +#include "shadows_vertex.glsl" +#include "lighting.glsl" +#include "depth.glsl" + +void main(void) +{ + gl_Position = mw_modelToClip(gl_Vertex); + + vec4 viewPos = mw_modelToView(gl_Vertex); + gl_ClipVertex = viewPos; + euclideanDepth = length(viewPos.xyz); + linearDepth = getLinearDepth(gl_Position.z, viewPos.z); + +#if @diffuseMap + diffuseMapUV = (gl_TextureMatrix[@diffuseMapUV] * gl_MultiTexCoord@diffuseMapUV).xy; +#endif + +#if @emissiveMap + emissiveMapUV = (gl_TextureMatrix[@emissiveMapUV] * gl_MultiTexCoord@emissiveMapUV).xy; +#endif + +#if @normalMap + normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy; + passTangent = gl_MultiTexCoord7.xyzw; +#endif + + passColor = gl_Color; + passViewPos = viewPos.xyz; + passNormal = gl_Normal.xyz; + +#if @shadows_enabled + vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); + setupShadowCoords(viewPos, viewNormal); +#endif +} diff --git a/files/shaders/nv_nolighting_fragment.glsl b/files/shaders/nv_nolighting_fragment.glsl new file mode 100644 index 0000000000..59dfda9ee8 --- /dev/null +++ b/files/shaders/nv_nolighting_fragment.glsl @@ -0,0 +1,46 @@ +#version 120 +#pragma import_defines(FORCE_OPAQUE) + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#if @diffuseMap +uniform sampler2D diffuseMap; +varying vec2 diffuseMapUV; +#endif + +varying float euclideanDepth; +varying float linearDepth; + +uniform bool useFalloff; +uniform vec2 screenRes; + +varying float passFalloff; + +#include "vertexcolors.glsl" +#include "alpha.glsl" +#include "fog.glsl" + +void main() +{ +#if @diffuseMap + gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV); +#else + gl_FragData[0] = vec4(1.0); +#endif + + gl_FragData[0] *= getDiffuseColor(); + + if (useFalloff) + gl_FragData[0].a *= passFalloff; + + alphaTest(); + +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE + gl_FragData[0].a = 1.0; +#endif + + gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth); +} diff --git a/files/shaders/nv_nolighting_vertex.glsl b/files/shaders/nv_nolighting_vertex.glsl new file mode 100644 index 0000000000..7b1f6961b4 --- /dev/null +++ b/files/shaders/nv_nolighting_vertex.glsl @@ -0,0 +1,52 @@ +#version 120 + +#include "openmw_vertex.h.glsl" + +#if @diffuseMap +varying vec2 diffuseMapUV; +#endif + +#if @radialFog +varying float euclideanDepth; +#else +varying float linearDepth; +#endif + +uniform bool useFalloff; +uniform vec4 falloffParams; + +varying vec3 passViewPos; +varying float passFalloff; + +#include "vertexcolors.glsl" +#include "depth.glsl" + +void main(void) +{ + gl_Position = mw_modelToClip(gl_Vertex); + + vec4 viewPos = mw_modelToView(gl_Vertex); + gl_ClipVertex = viewPos; +#if @radialFog + euclideanDepth = length(viewPos.xyz); +#else + linearDepth = getLinearDepth(gl_Position.z, viewPos.z); +#endif + +#if @diffuseMap + diffuseMapUV = (gl_TextureMatrix[@diffuseMapUV] * gl_MultiTexCoord@diffuseMapUV).xy; +#endif + + passColor = gl_Color; + if (useFalloff) + { + vec3 viewNormal = gl_NormalMatrix * normalize(gl_Normal.xyz); + vec3 viewDir = normalize(viewPos.xyz); + float viewAngle = abs(dot(viewNormal, viewDir)); + passFalloff = smoothstep(falloffParams.y, falloffParams.x, viewAngle); + } + else + { + passFalloff = 1.0; + } +} diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index d5716c378b..caf8b672ca 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -1,4 +1,13 @@ #version 120 +#pragma import_defines(FORCE_OPAQUE) + +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif #if @diffuseMap uniform sampler2D diffuseMap; @@ -49,24 +58,39 @@ uniform vec2 envMapLumaBias; uniform mat2 bumpMapMatrix; #endif -uniform bool simpleWater; +#if @glossMap +uniform sampler2D glossMap; +varying vec2 glossMapUV; +#endif -varying float euclideanDepth; -varying float linearDepth; +uniform vec2 screenRes; #define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING -centroid varying vec4 lighting; +centroid varying vec3 passLighting; centroid varying vec3 shadowDiffuseLighting; +#else +uniform float emissiveMult; #endif -centroid varying vec4 passColor; +uniform float specStrength; varying vec3 passViewPos; varying vec3 passNormal; +#if @additiveBlending +#define ADDITIVE_BLENDING +#endif + +#include "vertexcolors.glsl" #include "shadows_fragment.glsl" #include "lighting.glsl" #include "parallax.glsl" +#include "alpha.glsl" +#include "fog.glsl" + +#if @softParticles +#include "softparticles.glsl" +#endif void main() { @@ -74,19 +98,23 @@ void main() vec2 adjustedDiffuseUV = diffuseMapUV; #endif + vec3 worldNormal = normalize(passNormal); + vec3 viewVec = normalize(passViewPos.xyz); + #if @normalMap vec4 normalTex = texture2D(normalMap, normalMapUV); - vec3 normalizedNormal = normalize(passNormal); + vec3 normalizedNormal = worldNormal; vec3 normalizedTangent = normalize(passTangent.xyz); vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); - vec3 viewNormal = gl_NormalMatrix * normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + worldNormal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + vec3 viewNormal = gl_NormalMatrix * worldNormal; #endif -#if (!@normalMap && (@parallax || @forcePPL)) - vec3 viewNormal = gl_NormalMatrix * normalize(passNormal); +#if (!@normalMap && (@parallax || @forcePPL || @softParticles)) + vec3 viewNormal = gl_NormalMatrix * worldNormal; #endif #if @parallax @@ -101,28 +129,37 @@ void main() #if 1 // fetch a new normal using updated coordinates normalTex = texture2D(normalMap, adjustedDiffuseUV); - viewNormal = gl_NormalMatrix * normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + + worldNormal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + viewNormal = gl_NormalMatrix * worldNormal; #endif #endif #if @diffuseMap gl_FragData[0] = texture2D(diffuseMap, adjustedDiffuseUV); + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, adjustedDiffuseUV); #else gl_FragData[0] = vec4(1.0); #endif -#if @detailMap - gl_FragData[0].xyz *= texture2D(detailMap, detailMapUV).xyz * 2.0; -#endif + vec4 diffuseColor = getDiffuseColor(); + gl_FragData[0].a *= diffuseColor.a; #if @darkMap - gl_FragData[0].xyz *= texture2D(darkMap, darkMapUV).xyz; + gl_FragData[0] *= texture2D(darkMap, darkMapUV); + gl_FragData[0].a *= coveragePreservingAlphaScale(darkMap, darkMapUV); +#endif + + alphaTest(); + +#if @detailMap + gl_FragData[0].xyz *= texture2D(detailMap, detailMapUV).xyz * 2.0; #endif #if @decalMap vec4 decalTex = texture2D(decalMap, decalMapUV); - gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a); + gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a * diffuseColor.a); #endif #if @envMap @@ -132,7 +169,6 @@ void main() #if @normalMap // if using normal map + env map, take advantage of per-pixel normals for envTexCoordGen - vec3 viewVec = normalize(passViewPos.xyz); vec3 r = reflect( viewVec, viewNormal ); float m = 2.0 * sqrt( r.x*r.x + r.y*r.y + (r.z+1.0)*(r.z+1.0) ); envTexCoordGen = vec2(r.x/m + 0.5, r.y/m + 0.5); @@ -144,28 +180,35 @@ void main() envLuma = clamp(bumpTex.b * envMapLumaBias.x + envMapLumaBias.y, 0.0, 1.0); #endif -#if @preLightEnv - gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma; + vec3 envEffect = texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma; + +#if @glossMap + envEffect *= texture2D(glossMap, glossMapUV).xyz; #endif +#if @preLightEnv + gl_FragData[0].xyz += envEffect; #endif - float shadowing = unshadowedLightRatio(linearDepth); +#endif + float shadowing = unshadowedLightRatio(-passViewPos.z); + vec3 lighting; #if !PER_PIXEL_LIGHTING - -#if @clamp - gl_FragData[0] *= clamp(lighting + vec4(shadowDiffuseLighting * shadowing, 0), vec4(0.0), vec4(1.0)); + lighting = passLighting + shadowDiffuseLighting * shadowing; #else - gl_FragData[0] *= lighting + vec4(shadowDiffuseLighting * shadowing, 0); + vec3 diffuseLight, ambientLight; + doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); + vec3 emission = getEmissionColor().xyz * emissiveMult; + lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; #endif -#else - gl_FragData[0] *= doLighting(passViewPos, normalize(viewNormal), passColor, shadowing); -#endif + clampLightingResult(lighting); + + gl_FragData[0].xyz *= lighting; #if @envMap && !@preLightEnv - gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma; + gl_FragData[0].xyz += envEffect; #endif #if @emissiveMap @@ -178,32 +221,32 @@ void main() vec3 matSpec = specTex.xyz; #else float shininess = gl_FrontMaterial.shininess; - vec3 matSpec; - if (colorMode == ColorMode_Specular) - matSpec = passColor.xyz; - else - matSpec = gl_FrontMaterial.specular.xyz; + vec3 matSpec = getSpecularColor().xyz; #endif + matSpec *= specStrength; if (matSpec != vec3(0.0)) { #if (!@normalMap && !@parallax && !@forcePPL) - vec3 viewNormal = gl_NormalMatrix * normalize(passNormal); + vec3 viewNormal = gl_NormalMatrix * worldNormal; #endif - gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos.xyz), shininess, matSpec) * shadowing; + gl_FragData[0].xyz += getSpecular(normalize(viewNormal), viewVec, shininess, matSpec) * shadowing; } -#if @radialFog - float depth; - // For the less detailed mesh of simple water we need to recalculate depth on per-pixel basis - if (simpleWater) - depth = length(passViewPos); - else - depth = euclideanDepth; - float fogValue = clamp((depth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); -#else - float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); + + gl_FragData[0] = applyFogAtPos(gl_FragData[0], passViewPos); + +#if !defined(FORCE_OPAQUE) && @softParticles + gl_FragData[0].a *= calcSoftParticleFade(viewVec, viewNormal, passViewPos); +#endif + +#if defined(FORCE_OPAQUE) && FORCE_OPAQUE + // having testing & blending isn't enough - we need to write an opaque pixel to be opaque + gl_FragData[0].a = 1.0; +#endif + +#if !defined(FORCE_OPAQUE) && !@disableNormals + gl_FragData[1].xyz = worldNormal * 0.5 + 0.5; #endif - gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); applyShadowDebugOverlay(); } diff --git a/files/shaders/objects_vertex.glsl b/files/shaders/objects_vertex.glsl index 40c448de99..971b95d8e0 100644 --- a/files/shaders/objects_vertex.glsl +++ b/files/shaders/objects_vertex.glsl @@ -1,5 +1,14 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#include "openmw_vertex.h.glsl" #if @diffuseMap varying vec2 diffuseMapUV; #endif @@ -37,31 +46,32 @@ varying vec2 bumpMapUV; varying vec2 specularMapUV; #endif -varying float euclideanDepth; -varying float linearDepth; +#if @glossMap +varying vec2 glossMapUV; +#endif #define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING -centroid varying vec4 lighting; +centroid varying vec3 passLighting; centroid varying vec3 shadowDiffuseLighting; +uniform float emissiveMult; #endif -centroid varying vec4 passColor; varying vec3 passViewPos; varying vec3 passNormal; +#include "vertexcolors.glsl" #include "shadows_vertex.glsl" #include "lighting.glsl" +#include "depth.glsl" void main(void) { - gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + gl_Position = mw_modelToClip(gl_Vertex); - vec4 viewPos = (gl_ModelViewMatrix * gl_Vertex); + vec4 viewPos = mw_modelToView(gl_Vertex); gl_ClipVertex = viewPos; - euclideanDepth = length(viewPos.xyz); - linearDepth = gl_Position.z; #if (@envMap || !PER_PIXEL_LIGHTING || @shadows_enabled) vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); @@ -107,13 +117,23 @@ void main(void) specularMapUV = (gl_TextureMatrix[@specularMapUV] * gl_MultiTexCoord@specularMapUV).xy; #endif -#if !PER_PIXEL_LIGHTING - lighting = doLighting(viewPos.xyz, viewNormal, gl_Color, shadowDiffuseLighting); +#if @glossMap + glossMapUV = (gl_TextureMatrix[@glossMapUV] * gl_MultiTexCoord@glossMapUV).xy; #endif + passColor = gl_Color; passViewPos = viewPos.xyz; passNormal = gl_Normal.xyz; +#if !PER_PIXEL_LIGHTING + vec3 diffuseLight, ambientLight; + doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); + vec3 emission = getEmissionColor().xyz * emissiveMult; + passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; + clampLightingResult(passLighting); + shadowDiffuseLighting *= getDiffuseColor().xyz; +#endif + #if (@shadows_enabled) setupShadowCoords(viewPos, viewNormal); #endif diff --git a/files/shaders/openmw_fragment.glsl b/files/shaders/openmw_fragment.glsl new file mode 100644 index 0000000000..8c8302f3ae --- /dev/null +++ b/files/shaders/openmw_fragment.glsl @@ -0,0 +1,42 @@ +#version 120 + +#include "openmw_fragment.h.glsl" + +uniform sampler2D reflectionMap; + +vec4 mw_sampleReflectionMap(vec2 uv) +{ + return texture2D(reflectionMap, uv); +} + +#if @refraction_enabled +uniform sampler2D refractionMap; +uniform sampler2D refractionDepthMap; + +vec4 mw_sampleRefractionMap(vec2 uv) +{ + return texture2D(refractionMap, uv); +} + +float mw_sampleRefractionDepthMap(vec2 uv) +{ + return texture2D(refractionDepthMap, uv).x; +} + +#endif + +uniform sampler2D omw_SamplerLastShader; + +vec4 mw_samplerLastShader(vec2 uv) +{ + return texture2D(omw_SamplerLastShader, uv); +} + +#if @skyBlending +uniform sampler2D sky; + +vec3 mw_sampleSkyColor(vec2 uv) +{ + return texture2D(sky, uv).xyz; +} +#endif diff --git a/files/shaders/openmw_fragment.h.glsl b/files/shaders/openmw_fragment.h.glsl new file mode 100644 index 0000000000..f7c1e2f21e --- /dev/null +++ b/files/shaders/openmw_fragment.h.glsl @@ -0,0 +1,20 @@ +#ifndef OPENMW_FRAGMENT_H_GLSL +#define OPENMW_FRAGMENT_H_GLSL + +@link "openmw_fragment.glsl" if !@useOVR_multiview +@link "openmw_fragment_multiview.glsl" if @useOVR_multiview + +vec4 mw_sampleReflectionMap(vec2 uv); + +#if @refraction_enabled +vec4 mw_sampleRefractionMap(vec2 uv); +float mw_sampleRefractionDepthMap(vec2 uv); +#endif + +vec4 mw_samplerLastShader(vec2 uv); + +#if @skyBlending +vec3 mw_sampleSkyColor(vec2 uv); +#endif + +#endif // OPENMW_FRAGMENT_H_GLSL diff --git a/files/shaders/openmw_fragment_multiview.glsl b/files/shaders/openmw_fragment_multiview.glsl new file mode 100644 index 0000000000..61f69cc2d9 --- /dev/null +++ b/files/shaders/openmw_fragment_multiview.glsl @@ -0,0 +1,47 @@ +#version 330 compatibility + +#extension GL_OVR_multiview : require +#extension GL_OVR_multiview2 : require +#extension GL_EXT_texture_array : require + +#include "openmw_fragment.h.glsl" + +uniform sampler2DArray reflectionMap; + +vec4 mw_sampleReflectionMap(vec2 uv) +{ + return texture2DArray(reflectionMap, vec3((uv), gl_ViewID_OVR)); +} + +#if @refraction_enabled + +uniform sampler2DArray refractionMap; +uniform sampler2DArray refractionDepthMap; + +vec4 mw_sampleRefractionMap(vec2 uv) +{ + return texture2DArray(refractionMap, vec3((uv), gl_ViewID_OVR)); +} + +float mw_sampleRefractionDepthMap(vec2 uv) +{ + return texture2DArray(refractionDepthMap, vec3((uv), gl_ViewID_OVR)).x; +} + +#endif + +uniform sampler2DArray omw_SamplerLastShader; + +vec4 mw_samplerLastShader(vec2 uv) +{ + return texture2DArray(omw_SamplerLastShader, vec3((uv), gl_ViewID_OVR)); +} + +#if @skyBlending +uniform sampler2DArray sky; + +vec3 mw_sampleSkyColor(vec2 uv) +{ + return texture2DArray(sky, vec3((uv), gl_ViewID_OVR)).xyz; +} +#endif diff --git a/files/shaders/openmw_vertex.glsl b/files/shaders/openmw_vertex.glsl new file mode 100644 index 0000000000..b671570348 --- /dev/null +++ b/files/shaders/openmw_vertex.glsl @@ -0,0 +1,20 @@ +#version 120 + +#include "openmw_vertex.h.glsl" + +uniform mat4 projectionMatrix; + +vec4 mw_modelToClip(vec4 pos) +{ + return projectionMatrix * mw_modelToView(pos); +} + +vec4 mw_modelToView(vec4 pos) +{ + return gl_ModelViewMatrix * pos; +} + +vec4 mw_viewToClip(vec4 pos) +{ + return projectionMatrix * pos; +} diff --git a/files/shaders/openmw_vertex.h.glsl b/files/shaders/openmw_vertex.h.glsl new file mode 100644 index 0000000000..9d7f496a6a --- /dev/null +++ b/files/shaders/openmw_vertex.h.glsl @@ -0,0 +1,6 @@ +@link "openmw_vertex.glsl" if !@useOVR_multiview +@link "openmw_vertex_multiview.glsl" if @useOVR_multiview + +vec4 mw_modelToClip(vec4 pos); +vec4 mw_modelToView(vec4 pos); +vec4 mw_viewToClip(vec4 pos); \ No newline at end of file diff --git a/files/shaders/openmw_vertex_multiview.glsl b/files/shaders/openmw_vertex_multiview.glsl new file mode 100644 index 0000000000..64230a6e4a --- /dev/null +++ b/files/shaders/openmw_vertex_multiview.glsl @@ -0,0 +1,25 @@ +#version 330 compatibility + +#extension GL_OVR_multiview : require +#extension GL_OVR_multiview2 : require + +layout(num_views = @numViews) in; + +#include "openmw_vertex.h.glsl" + +uniform mat4 projectionMatrixMultiView[@numViews]; + +vec4 mw_modelToClip(vec4 pos) +{ + return mw_viewToClip(mw_modelToView(pos)); +} + +vec4 mw_modelToView(vec4 pos) +{ + return gl_ModelViewMatrix * pos; +} + +vec4 mw_viewToClip(vec4 pos) +{ + return projectionMatrixMultiView[gl_ViewID_OVR] * pos; +} \ No newline at end of file diff --git a/files/shaders/shadowcasting_fragment.glsl b/files/shaders/shadowcasting_fragment.glsl index a5410d0089..07fad047e1 100644 --- a/files/shaders/shadowcasting_fragment.glsl +++ b/files/shaders/shadowcasting_fragment.glsl @@ -1,5 +1,9 @@ #version 120 +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + uniform sampler2D diffuseMap; varying vec2 diffuseMapUV; @@ -8,6 +12,8 @@ varying float alphaPassthrough; uniform bool useDiffuseMapForShadowAlpha; uniform bool alphaTestShadows; +#include "alpha.glsl" + void main() { gl_FragData[0].rgb = vec3(1.0); @@ -16,7 +22,10 @@ void main() else gl_FragData[0].a = alphaPassthrough; - // Prevent translucent things casting shadow (including the player using an invisibility effect). For now, rely on the deprecated FF test for non-blended stuff. + alphaTest(); + + // Prevent translucent things casting shadow (including the player using an invisibility effect). + // This replaces alpha blending, which obviously doesn't work with depth buffers if (alphaTestShadows && gl_FragData[0].a <= 0.5) discard; } diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl new file mode 100644 index 0000000000..e12614c082 --- /dev/null +++ b/files/shaders/sky_fragment.glsl @@ -0,0 +1,88 @@ +#version 120 + +#include "skypasses.glsl" + +uniform int pass; +uniform sampler2D diffuseMap; +uniform sampler2D maskMap; // PASS_MOON +uniform float opacity; // PASS_CLOUDS, PASS_ATMOSPHERE_NIGHT +uniform vec4 moonBlend; // PASS_MOON +uniform vec4 atmosphereFade; // PASS_MOON + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void paintAtmosphere(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a *= passColor.a; +} + +void paintAtmosphereNight(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; +} + +void paintClouds(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; + color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); + + // ease transition between clear color and atmosphere/clouds + color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a); +} + +void paintMoon(inout vec4 color) +{ + vec4 phase = texture2D(diffuseMap, diffuseMapUV); + vec4 mask = texture2D(maskMap, diffuseMapUV); + + vec4 blendedLayer = phase * moonBlend; + color = vec4(blendedLayer.xyz + atmosphereFade.xyz, atmosphereFade.a * mask.a); +} + +void paintSun(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= gl_FrontMaterial.diffuse.a; +} + +void paintSunglare(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a = gl_FrontMaterial.diffuse.a; +} + +void processSunflashQuery() +{ + const float threshold = 0.8; + + if (texture2D(diffuseMap, diffuseMapUV).a <= threshold) + discard; +} + +void main() +{ + vec4 color = vec4(0.0); + + if (pass == PASS_ATMOSPHERE) + paintAtmosphere(color); + else if (pass == PASS_ATMOSPHERE_NIGHT) + paintAtmosphereNight(color); + else if (pass == PASS_CLOUDS) + paintClouds(color); + else if (pass == PASS_MOON) + paintMoon(color); + else if (pass == PASS_SUN) + paintSun(color); + else if (pass == PASS_SUNGLARE) + paintSunglare(color); + else if (pass == PASS_SUNFLASH_QUERY) { + processSunflashQuery(); + return; + } + + gl_FragData[0] = color; +} diff --git a/files/shaders/sky_vertex.glsl b/files/shaders/sky_vertex.glsl new file mode 100644 index 0000000000..8ff9c0f156 --- /dev/null +++ b/files/shaders/sky_vertex.glsl @@ -0,0 +1,21 @@ +#version 120 + +#include "openmw_vertex.h.glsl" + +#include "skypasses.glsl" + +uniform int pass; + +varying vec4 passColor; +varying vec2 diffuseMapUV; + +void main() +{ + gl_Position = mw_modelToClip(gl_Vertex); + passColor = gl_Color; + + if (pass == PASS_CLOUDS) + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + else + diffuseMapUV = gl_MultiTexCoord0.xy; +} diff --git a/files/shaders/skypasses.glsl b/files/shaders/skypasses.glsl new file mode 100644 index 0000000000..e80d4eb259 --- /dev/null +++ b/files/shaders/skypasses.glsl @@ -0,0 +1,7 @@ +#define PASS_ATMOSPHERE 0 +#define PASS_ATMOSPHERE_NIGHT 1 +#define PASS_CLOUDS 2 +#define PASS_MOON 3 +#define PASS_SUN 4 +#define PASS_SUNFLASH_QUERY 5 +#define PASS_SUNGLARE 6 diff --git a/files/shaders/softparticles.glsl b/files/shaders/softparticles.glsl new file mode 100644 index 0000000000..c4bb90ebe2 --- /dev/null +++ b/files/shaders/softparticles.glsl @@ -0,0 +1,40 @@ +uniform float near; +uniform sampler2D opaqueDepthTex; +uniform float particleSize; +uniform bool particleFade; + +float viewDepth(float depth) +{ +#if @reverseZ + depth = 1.0 - depth; +#endif + return (near * far) / ((far - near) * depth - far); +} + +float calcSoftParticleFade(in vec3 viewDir, in vec3 viewNormal, in vec3 viewPos) +{ + float euclidianDepth = length(viewPos); + + const float falloffMultiplier = 0.33; + const float contrast = 1.30; + + vec2 screenCoords = gl_FragCoord.xy / screenRes; + + float depth = texture2D(opaqueDepthTex, screenCoords).x; + + float sceneDepth = viewDepth(depth); + float particleDepth = passViewPos.z; + float falloff = particleSize * falloffMultiplier; + float delta = particleDepth - sceneDepth; + + const float nearMult = 300.0; + float viewBias = 1.0; + + if (particleFade) { + float VdotN = dot(viewDir, viewNormal); + viewBias = abs(VdotN) * quickstep(euclidianDepth / nearMult) * (1.0 - pow(1.0 + VdotN, 1.3)); + } + + const float shift = 0.845; + return shift * pow(clamp(delta/falloff, 0.0, 1.0), contrast) * viewBias; +} diff --git a/files/shaders/terrain_fragment.glsl b/files/shaders/terrain_fragment.glsl index 7409ce0455..1e72cf5828 100644 --- a/files/shaders/terrain_fragment.glsl +++ b/files/shaders/terrain_fragment.glsl @@ -1,5 +1,13 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + varying vec2 uv; uniform sampler2D diffuseMap; @@ -18,35 +26,42 @@ varying float linearDepth; #define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING -centroid varying vec4 lighting; +centroid varying vec3 passLighting; centroid varying vec3 shadowDiffuseLighting; #endif -centroid varying vec4 passColor; varying vec3 passViewPos; varying vec3 passNormal; +uniform vec2 screenRes; + +#include "vertexcolors.glsl" #include "shadows_fragment.glsl" #include "lighting.glsl" #include "parallax.glsl" +#include "fog.glsl" void main() { vec2 adjustedUV = (gl_TextureMatrix[0] * vec4(uv, 0.0, 1.0)).xy; + vec3 worldNormal = normalize(passNormal); + #if @normalMap vec4 normalTex = texture2D(normalMap, adjustedUV); - vec3 normalizedNormal = normalize(passNormal); + vec3 normalizedNormal = worldNormal; vec3 tangent = vec3(1.0, 0.0, 0.0); vec3 binormal = normalize(cross(tangent, normalizedNormal)); tangent = normalize(cross(normalizedNormal, binormal)); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal mat3 tbnTranspose = mat3(tangent, binormal, normalizedNormal); - vec3 viewNormal = normalize(gl_NormalMatrix * (tbnTranspose * (normalTex.xyz * 2.0 - 1.0))); + worldNormal = tbnTranspose * (normalTex.xyz * 2.0 - 1.0); + vec3 viewNormal = normalize(gl_NormalMatrix * worldNormal); + normalize(worldNormal); #endif #if (!@normalMap && (@parallax || @forcePPL)) - vec3 viewNormal = gl_NormalMatrix * normalize(passNormal); + vec3 viewNormal = gl_NormalMatrix * worldNormal; #endif #if @parallax @@ -57,7 +72,10 @@ void main() // update normal using new coordinates normalTex = texture2D(normalMap, adjustedUV); - viewNormal = normalize(gl_NormalMatrix * (tbnTranspose * (normalTex.xyz * 2.0 - 1.0))); + + worldNormal = tbnTranspose * (normalTex.xyz * 2.0 - 1.0); + viewNormal = normalize(gl_NormalMatrix * worldNormal); + normalize(worldNormal); #endif vec4 diffuseTex = texture2D(diffuseMap, adjustedUV); @@ -68,46 +86,44 @@ void main() gl_FragData[0].a *= texture2D(blendMap, blendMapUV).a; #endif - float shadowing = unshadowedLightRatio(linearDepth); + vec4 diffuseColor = getDiffuseColor(); + gl_FragData[0].a *= diffuseColor.a; + float shadowing = unshadowedLightRatio(linearDepth); + vec3 lighting; #if !PER_PIXEL_LIGHTING - -#if @clamp - gl_FragData[0] *= clamp(lighting + vec4(shadowDiffuseLighting * shadowing, 0), vec4(0.0), vec4(1.0)); + lighting = passLighting + shadowDiffuseLighting * shadowing; #else - gl_FragData[0] *= lighting + vec4(shadowDiffuseLighting * shadowing, 0); + vec3 diffuseLight, ambientLight; + doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); + lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz; #endif -#else - gl_FragData[0] *= doLighting(passViewPos, normalize(viewNormal), passColor, shadowing); -#endif + clampLightingResult(lighting); + + gl_FragData[0].xyz *= lighting; #if @specularMap float shininess = 128.0; // TODO: make configurable vec3 matSpec = vec3(diffuseTex.a); #else float shininess = gl_FrontMaterial.shininess; - vec3 matSpec; - if (colorMode == ColorMode_Specular) - matSpec = passColor.xyz; - else - matSpec = gl_FrontMaterial.specular.xyz; + vec3 matSpec = getSpecularColor().xyz; #endif if (matSpec != vec3(0.0)) { #if (!@normalMap && !@parallax && !@forcePPL) - vec3 viewNormal = gl_NormalMatrix * normalize(passNormal); + vec3 viewNormal = gl_NormalMatrix * worldNormal; #endif gl_FragData[0].xyz += getSpecular(normalize(viewNormal), normalize(passViewPos), shininess, matSpec) * shadowing; } -#if @radialFog - float fogValue = clamp((euclideanDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); -#else - float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); + gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth); + +#if !@disableNormals && @writeNormals + gl_FragData[1].xyz = worldNormal.xyz * 0.5 + 0.5; #endif - gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); applyShadowDebugOverlay(); } diff --git a/files/shaders/terrain_vertex.glsl b/files/shaders/terrain_vertex.glsl index bf337cf548..a426061941 100644 --- a/files/shaders/terrain_vertex.glsl +++ b/files/shaders/terrain_vertex.glsl @@ -1,5 +1,14 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#include "openmw_vertex.h.glsl" varying vec2 uv; varying float euclideanDepth; varying float linearDepth; @@ -7,37 +16,43 @@ varying float linearDepth; #define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #if !PER_PIXEL_LIGHTING -centroid varying vec4 lighting; +centroid varying vec3 passLighting; centroid varying vec3 shadowDiffuseLighting; #endif -centroid varying vec4 passColor; varying vec3 passViewPos; varying vec3 passNormal; +#include "vertexcolors.glsl" #include "shadows_vertex.glsl" #include "lighting.glsl" +#include "depth.glsl" void main(void) { - gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + gl_Position = mw_modelToClip(gl_Vertex); - vec4 viewPos = (gl_ModelViewMatrix * gl_Vertex); + vec4 viewPos = mw_modelToView(gl_Vertex); gl_ClipVertex = viewPos; euclideanDepth = length(viewPos.xyz); - linearDepth = gl_Position.z; + linearDepth = getLinearDepth(gl_Position.z, viewPos.z); #if (!PER_PIXEL_LIGHTING || @shadows_enabled) vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); #endif -#if !PER_PIXEL_LIGHTING - lighting = doLighting(viewPos.xyz, viewNormal, gl_Color, shadowDiffuseLighting); -#endif passColor = gl_Color; passNormal = gl_Normal.xyz; passViewPos = viewPos.xyz; +#if !PER_PIXEL_LIGHTING + vec3 diffuseLight, ambientLight; + doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); + passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz; + clampLightingResult(passLighting); + shadowDiffuseLighting *= getDiffuseColor().xyz; +#endif + uv = gl_MultiTexCoord0.xy; #if (@shadows_enabled) diff --git a/files/shaders/vertexcolors.glsl b/files/shaders/vertexcolors.glsl new file mode 100644 index 0000000000..38026d6acf --- /dev/null +++ b/files/shaders/vertexcolors.glsl @@ -0,0 +1,38 @@ +centroid varying vec4 passColor; + +uniform int colorMode; + +const int ColorMode_None = 0; +const int ColorMode_Emission = 1; +const int ColorMode_AmbientAndDiffuse = 2; +const int ColorMode_Ambient = 3; +const int ColorMode_Diffuse = 4; +const int ColorMode_Specular = 5; + +vec4 getEmissionColor() +{ + if (colorMode == ColorMode_Emission) + return passColor; + return gl_FrontMaterial.emission; +} + +vec4 getAmbientColor() +{ + if (colorMode == ColorMode_AmbientAndDiffuse || colorMode == ColorMode_Ambient) + return passColor; + return gl_FrontMaterial.ambient; +} + +vec4 getDiffuseColor() +{ + if (colorMode == ColorMode_AmbientAndDiffuse || colorMode == ColorMode_Diffuse) + return passColor; + return gl_FrontMaterial.diffuse; +} + +vec4 getSpecularColor() +{ + if (colorMode == ColorMode_Specular) + return passColor; + return gl_FrontMaterial.specular; +} diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl index 93ba156beb..f60ec765b1 100644 --- a/files/shaders/water_fragment.glsl +++ b/files/shaders/water_fragment.glsl @@ -1,6 +1,17 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + +#include "openmw_fragment.h.glsl" + #define REFRACTION @refraction_enabled +#define RAIN_RIPPLE_DETAIL @rain_ripple_detail // Inspired by Blender GLSL Water by martinsh ( https://devlog-martinsh.blogspot.de/2012/07/waterundewater-shader-wip.html ) @@ -43,16 +54,26 @@ const float WIND_SPEED = 0.2f; const vec3 WATER_COLOR = vec3(0.090195, 0.115685, 0.12745); +const float WOBBLY_SHORE_FADE_DISTANCE = 6200.0; // fade out wobbly shores to mask precision errors, the effect is almost impossible to see at a distance + // ---------------- rain ripples related stuff --------------------- -const float RAIN_RIPPLE_GAPS = 5.0; -const float RAIN_RIPPLE_RADIUS = 0.1; +const float RAIN_RIPPLE_GAPS = 10.0; +const float RAIN_RIPPLE_RADIUS = 0.2; -vec2 randOffset(vec2 c) +float scramble(float x, float z) { - return fract(vec2( - c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2, - c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7)); + return fract(pow(fract(x)*3.0+1.0, z)); +} + +vec2 randOffset(vec2 c, float time) +{ + time = fract(time/1000.0); + c = vec2(c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2, + c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7); + c.x *= scramble(scramble(time + c.x/1000.0, 4.0), 3.0) + 1.0; + c.y *= scramble(scramble(time + c.y/1000.0, 3.5), 3.0) + 1.0; + return fract(c); } float randPhase(vec2 c) @@ -60,43 +81,104 @@ float randPhase(vec2 c) return fract((c.x * c.y) / (c.x + c.y + 0.1)); } -vec4 circle(vec2 coords, vec2 i_part, float phase) +float blip(float x) { - vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(i_part) - 1.0); - vec2 toCenter = coords - center; - float d = length(toCenter); + x = max(0.0, 1.0-x*x); + return x*x*x; +} - float r = RAIN_RIPPLE_RADIUS * phase; +float blipDerivative(float x) +{ + x = clamp(x, -1.0, 1.0); + float n = x*x-1.0; + return -6.0*x*n*n; +} - if (d > r) - return vec4(0.0,0.0,1.0,0.0); +const float RAIN_RING_TIME_OFFSET = 1.0/6.0; - float sinValue = (sin(d / r * 1.2) + 0.7) / 2.0; +vec4 circle(vec2 coords, vec2 corner, float adjusted_time) +{ + vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(corner, floor(adjusted_time)) - 1.0); + float phase = fract(adjusted_time); + vec2 toCenter = coords - center; - float height = (1.0 - abs(phase)) * pow(sinValue,3.0); + float r = RAIN_RIPPLE_RADIUS; + float d = length(toCenter); + float ringfollower = (phase-d/r)/RAIN_RING_TIME_OFFSET-1.0; // -1.0 ~ +1.0 cover the breadth of the ripple's ring + +#if RAIN_RIPPLE_DETAIL > 0 +// normal mapped ripples + if(ringfollower < -1.0 || ringfollower > 1.0) + return vec4(0.0); + + if(d > 1.0) // normalize center direction vector, but not for near-center ripples + toCenter /= d; + + float height = blip(ringfollower*2.0+0.5); // brighten up outer edge of ring; for fake specularity + float range_limit = blip(min(0.0, ringfollower)); + float energy = 1.0-phase; + + vec2 normal2d = -toCenter*blipDerivative(ringfollower)*5.0; + vec3 normal = vec3(normal2d, 0.5); + vec4 ret = vec4(normal, height); + ret.xyw *= energy*energy; + // do energy adjustment here rather than later, so that we can use the w component for fake specularity + ret.xyz = normalize(ret.xyz) * energy*range_limit; + ret.z *= range_limit; + return ret; +#else +// ring-only ripples + if(ringfollower < -1.0 || ringfollower > 0.5) + return vec4(0.0); - vec3 normal = normalize(mix(vec3(0.0,0.0,1.0),vec3(normalize(toCenter),0.0),height)); + float energy = 1.0-phase; + float height = blip(ringfollower*2.0+0.5)*energy*energy; // fake specularity - return vec4(normal,height); + return vec4(0.0, 0.0, 0.0, height); +#endif } - vec4 rain(vec2 uv, float time) { - vec2 i_part = floor(uv * RAIN_RIPPLE_GAPS); - vec2 f_part = fract(uv * RAIN_RIPPLE_GAPS); - return circle(f_part,i_part,fract(time * 1.2 + randPhase(i_part))); + uv *= RAIN_RIPPLE_GAPS; + vec2 f_part = fract(uv); + vec2 i_part = floor(uv); + float adjusted_time = time * 1.2 + randPhase(i_part); +#if RAIN_RIPPLE_DETAIL > 0 + vec4 a = circle(f_part, i_part, adjusted_time); + vec4 b = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET); + vec4 c = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*2.0); + vec4 d = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*3.0); + vec4 ret; + ret.xy = a.xy - b.xy/2.0 + c.xy/4.0 - d.xy/8.0; + // z should always point up + ret.z = a.z + b.z /2.0 + c.z /4.0 + d.z /8.0; + //ret.xyz *= 1.5; + // fake specularity looks weird if we use every single ring, also if the inner rings are too bright + ret.w = (a.w + c.w /8.0)*1.5; + return ret; +#else + return circle(f_part, i_part, adjusted_time) * 1.5; +#endif } -vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and ripple height in w +vec2 complex_mult(vec2 a, vec2 b) +{ + return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x); +} +vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and fake specularity in w { return - rain(uv,time) + - rain(uv + vec2(10.5,5.7),time) + - rain(uv * 0.75 + vec2(3.7,18.9),time) + - rain(uv * 0.9 + vec2(5.7,30.1),time) + - rain(uv * 0.8 + vec2(1.2,3.0),time); + rain(uv, time) + + rain(complex_mult(uv, vec2(0.4, 0.7)) + vec2(1.2, 3.0),time) + #if RAIN_RIPPLE_DETAIL == 2 + + rain(uv * 0.75 + vec2( 3.7,18.9),time) + + rain(uv * 0.9 + vec2( 5.7,30.1),time) + + rain(uv * 1.0 + vec2(10.5 ,5.7),time) + #endif + ; } + // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - float fresnel_dielectric(vec3 Incoming, vec3 Normal, float eta) @@ -122,36 +204,37 @@ vec2 normalCoords(vec2 uv, float scale, float speed, float time, float timer1, f return uv * (WAVE_SCALE * scale) + WIND_DIR * time * (WIND_SPEED * speed) -(previousNormal.xy/previousNormal.zz) * WAVE_CHOPPYNESS + vec2(time * timer1,time * timer2); } -varying vec3 screenCoordsPassthrough; varying vec4 position; varying float linearDepth; uniform sampler2D normalMap; -uniform sampler2D reflectionMap; -#if REFRACTION -uniform sampler2D refractionMap; -uniform sampler2D refractionDepthMap; -#endif - uniform float osg_SimulationTime; uniform float near; -uniform float far; uniform vec3 nodePosition; uniform float rainIntensity; +uniform vec2 screenRes; + +#define PER_PIXEL_LIGHTING 0 + #include "shadows_fragment.glsl" +#include "lighting.glsl" +#include "fog.glsl" float frustumDepth; float linearizeDepth(float depth) - { - float z_n = 2.0 * depth - 1.0; - depth = 2.0 * near * far / (far + near - z_n * frustumDepth); - return depth; - } +{ +#if @reverseZ + depth = 1.0 - depth; +#endif + float z_n = 2.0 * depth - 1.0; + depth = 2.0 * near * far / (far + near - z_n * frustumDepth); + return depth; +} void main(void) { @@ -162,8 +245,7 @@ void main(void) float shadow = unshadowedLightRatio(linearDepth); - vec2 screenCoords = screenCoordsPassthrough.xy / screenCoordsPassthrough.z; - screenCoords.y = (1.0-screenCoords.y); + vec2 screenCoords = gl_FragCoord.xy / screenRes; #define waterTimer osg_SimulationTime @@ -177,11 +259,11 @@ void main(void) vec4 rainRipple; if (rainIntensity > 0.01) - rainRipple = rainCombined(position.xy / 1000.0,waterTimer) * clamp(rainIntensity,0.0,1.0); + rainRipple = rainCombined(position.xy/1000.0, waterTimer) * clamp(rainIntensity, 0.0, 1.0); else rainRipple = vec4(0.0); - vec3 rippleAdd = rainRipple.xyz * rainRipple.w * 10.0; + vec3 rippleAdd = rainRipple.xyz * 10.0; vec2 bigWaves = vec2(BIG_WAVES_X,BIG_WAVES_Y); vec2 midWaves = mix(vec2(MID_WAVES_X,MID_WAVES_Y),vec2(MID_WAVES_RAIN_X,MID_WAVES_RAIN_Y),rainIntensity); @@ -192,8 +274,7 @@ void main(void) normal3 * midWaves.y + normal4 * smallWaves.x + normal5 * smallWaves.y + rippleAdd); normal = normalize(vec3(-normal.x * bump, -normal.y * bump, normal.z)); - vec3 lVec = normalize((gl_ModelViewMatrixInverse * vec4(gl_LightSource[0].position.xyz, 0.0)).xyz); - + vec3 lVec = normalize((gl_ModelViewMatrixInverse * vec4(lcalcPosition(0).xyz, 0.0)).xyz); vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz; vec3 vVec = normalize(position.xyz - cameraPos.xyz); @@ -210,27 +291,39 @@ void main(void) // TODO: Figure out how to properly radialise refraction depth and thus underwater fog // while avoiding oddities when the water plane is close to the clipping plane // radialise = radialDepth / linearDepth; +#else + float radialDepth = 0.0; #endif vec2 screenCoordsOffset = normal.xy * REFL_BUMP; #if REFRACTION - float depthSample = linearizeDepth(texture2D(refractionDepthMap,screenCoords).x) * radialise; - float depthSampleDistorted = linearizeDepth(texture2D(refractionDepthMap,screenCoords-screenCoordsOffset).x) * radialise; + float depthSample = linearizeDepth(mw_sampleRefractionDepthMap(screenCoords)) * radialise; + float depthSampleDistorted = linearizeDepth(mw_sampleRefractionDepthMap(screenCoords-screenCoordsOffset)) * radialise; float surfaceDepth = linearizeDepth(gl_FragCoord.z) * radialise; float realWaterDepth = depthSample - surfaceDepth; // undistorted water depth in view direction, independent of frustum screenCoordsOffset *= clamp(realWaterDepth / BUMP_SUPPRESS_DEPTH,0,1); #endif // reflection - vec3 reflection = texture2D(reflectionMap, screenCoords + screenCoordsOffset).rgb; + vec3 reflection = mw_sampleReflectionMap(screenCoords + screenCoordsOffset).rgb; // specular float specular = pow(max(dot(reflect(vVec, normal), lVec), 0.0),SPEC_HARDNESS) * shadow; vec3 waterColor = WATER_COLOR * sunFade; + vec4 sunSpec = lcalcSpecular(0); + + // artificial specularity to make rain ripples more noticeable + vec3 skyColorEstimate = vec3(max(0.0, mix(-0.3, 1.0, sunFade))); + vec3 rainSpecular = abs(rainRipple.w)*mix(skyColorEstimate, vec3(1.0), 0.05)*0.5; + #if REFRACTION + // no alpha here, so make sure raindrop ripple specularity gets properly subdued + rainSpecular *= clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); + // refraction - vec3 refraction = texture2D(refractionMap, screenCoords - screenCoordsOffset).rgb; + vec3 refraction = mw_sampleRefractionMap(screenCoords - screenCoordsOffset).rgb; + vec3 rawRefraction = refraction; // brighten up the refraction underwater if (cameraPos.z < 0.0) @@ -246,21 +339,29 @@ void main(void) float sunHeight = lVec.z; vec3 scatterColour = mix(SCATTER_COLOUR*vec3(1.0,0.4,0.0), SCATTER_COLOUR, clamp(1.0-exp(-sunHeight*SUN_EXT), 0.0, 1.0)); vec3 lR = reflect(lVec, lNormal); - float lightScatter = shadow * clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0); - gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * gl_LightSource[0].specular.xyz + vec3(rainRipple.w) * 0.2; + float lightScatter = clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0); + gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + rainSpecular; gl_FragData[0].w = 1.0; + + // wobbly water: hard-fade into refraction texture at extremely low depth, with a wobble based on normal mapping + vec3 normalShoreRippleRain = texture2D(normalMap,normalCoords(UV, 2.0, 2.7, -1.0*waterTimer, 0.05, 0.1, normal3)).rgb - 0.5 + + texture2D(normalMap,normalCoords(UV, 2.0, 2.7, waterTimer, 0.04, -0.13, normal4)).rgb - 0.5; + float verticalWaterDepth = realWaterDepth * mix(abs(vVec.z), 1.0, 0.2); // an estimate + float shoreOffset = verticalWaterDepth - (normal2.r + mix(0.0, normalShoreRippleRain.r, rainIntensity) + 0.15)*8.0; + float fuzzFactor = min(1.0, 1000.0/surfaceDepth) * mix(abs(vVec.z), 1.0, 0.2); + shoreOffset *= fuzzFactor; + shoreOffset = clamp(mix(shoreOffset, 1.0, clamp(linearDepth / WOBBLY_SHORE_FADE_DISTANCE, 0.0, 1.0)), 0.0, 1.0); + gl_FragData[0].xyz = mix(rawRefraction, gl_FragData[0].xyz, shoreOffset); #else - gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * gl_LightSource[0].specular.xyz + vec3(rainRipple.w) * 0.7; - gl_FragData[0].w = clamp(fresnel*6.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0); + gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + rainSpecular; + gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0); #endif - // fog -#if @radialFog - float fogValue = clamp((radialDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); -#else - float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); + gl_FragData[0] = applyFogAtDist(gl_FragData[0], radialDepth, linearDepth); + +#if !@disableNormals + gl_FragData[1].rgb = normal * 0.5 + 0.5; #endif - gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); applyShadowDebugOverlay(); } diff --git a/files/shaders/water_vertex.glsl b/files/shaders/water_vertex.glsl index 02a395f95d..b09d3b54ae 100644 --- a/files/shaders/water_vertex.glsl +++ b/files/shaders/water_vertex.glsl @@ -1,26 +1,21 @@ #version 120 - -varying vec3 screenCoordsPassthrough; + +#include "openmw_vertex.h.glsl" + varying vec4 position; varying float linearDepth; #include "shadows_vertex.glsl" +#include "depth.glsl" void main(void) { - gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; - - mat4 scalemat = mat4(0.5, 0.0, 0.0, 0.0, - 0.0, -0.5, 0.0, 0.0, - 0.0, 0.0, 0.5, 0.0, - 0.5, 0.5, 0.5, 1.0); - - vec4 texcoordProj = ((scalemat) * ( gl_Position)); - screenCoordsPassthrough = texcoordProj.xyw; + gl_Position = mw_modelToClip(gl_Vertex); position = gl_Vertex; - linearDepth = gl_Position.z; + vec4 viewPos = mw_modelToView(gl_Vertex); + linearDepth = getLinearDepth(gl_Position.z, viewPos.z); - setupShadowCoords(gl_ModelViewMatrix * gl_Vertex, normalize((gl_NormalMatrix * gl_Normal).xyz)); + setupShadowCoords(viewPos, normalize((gl_NormalMatrix * gl_Normal).xyz)); } diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 3f53180daf..0a662fc091 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -6,7 +6,7 @@ 0 0 - 617 + 732 487 @@ -153,6 +153,16 @@ + + + + Give NPC 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. + + + Always allow NPC to follow over water surface + + + @@ -262,337 +272,607 @@ - - - - - <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 - - - - - - - <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>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>See 'auto use object normal maps'. Affects terrain.</p></body></html> - - - Auto use terrain normal maps - - - - - - - <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>Use object paging for active cells grid.</p></body></html> - - - Active grid object paging - - - - - - - <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>Load per-group KF-files and skeleton files from Animations folder</p></body></html> - - - Use additional animation sources - - - - - - - <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>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 - - - - - + + + true + + + + + 0 + 0 + 665 + 525 + + + - - - Viewing distance + + + Animations + + + + + <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>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 + + + + + + + <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 + + + + + + + 20 + + + + + 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 + + + + + + - - - Cells + + + Shaders - - 0.000000000000000 + + + + + <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>See 'auto use object normal maps'. Affects terrain.</p></body></html> + + + Auto use terrain normal maps + + + + + + + <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>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>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>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + + + Use anti-alias alpha testing + + + + + + + <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 + + + + + + + + + + Fog - - 0.500000000000000 + + + + + <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 + + + + + + + <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> + + + Exponential fog + + + + + + + <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> + + + Sky blending + + + + + + + false + + + Sky blending start + + + <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> + + + + + + + false + + + 3 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.005000000000000 + + + + + + + + + + Terrain + + + + + + + Viewing distance + + + + + + + Cells + + + 0.000000000000000 + + + 0.500000000000000 + + + + + + + + + + + <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 + + + + + + + + + <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 + + + + + + + 20 + + + + + <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + + + Active grid object paging + + + + + + + + + + + + Models + + + + + + <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 + + + + + + + + Post Processing + + + + + + <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> + + + Enable post processing + + + + + + + 20 + + + + + false + + + <html><head/><body><p>Debug Mode. Automatically reload active shaders when they are modified on filesystem.</p></body></html> + + + Live reload + + + + + + + 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> + + + HDR exposure time + + + + + + + false + + + 3 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.001000000000000 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + - - - + + + + + + + + Audio + + + + + + - <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> + Select your preferred audio device. - Auto use object specular maps + Audio Device - - - - <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> + + + + + 0 + 0 + + + + + 283 + 0 + - - Radial fog + + 0 + + + Default + + - - - 20 - + - - - false - + - <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> + This setting controls HRTF, which simulates 3D sound on stereo systems. - Weapon sheathing + HRTF - - - false - - - <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> + + + + 0 + 0 + + + + + 283 + 0 + - - Shield sheathing + + 0 + + + Automatic + + + + + Off + + + + + On + + - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Camera - - - - - - <html><head/><body><p>This setting controls third person view mode.</p><p>False: View is centered on the character's head. Crosshair is hidden. -True: In non-combat mode camera is positioned behind the character's shoulder. Crosshair is visible in third person mode as well. -</p></body></html> - - - View over the shoulder - - - - - - - <html><head/><body><p>When player is close to an obstacle, automatically switches camera to the shoulder that is farther away from the obstacle.</p></body></html> - - - Auto switch shoulder - - - - - - - 20 - - - 0 - - - 0 - - - 0 - + - - - 0 + + + Select your preferred HRTF profile. - - 0 + + HRTF Profile - - 0 + + + + + + + 0 + 0 + + + + + 283 + 0 + - + 0 - - - Default shoulder: - - - - - - - 0 - - - - Right - - - - - Left - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + + Default + - + - - - - <html><head/><body><p>If enabled then the character rotation is not synchonized with the camera rotation while the character doesn't move and not in combat mode.</p></body></html> - - - Preview if stand still - - - - - - - <html><head/><body><p>If enabled then the character smoothly rotates to the view direction after exiting preview or vanity mode. If disabled then the camera rotates rather than the character.</p></body></html> - - - Deferred preview rotation - - - - - - - <html><head/><body><p>Enables head bobbing when move in first person mode.</p></body></html> - - - Head bobbing in 1st person mode - - - @@ -662,6 +942,55 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + + + + + + + + + <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 + + + + + + + 2 + + + 0.500000000000000 + + + 8.000000000000000 + + + 0.250000000000000 + + + 1.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -724,6 +1053,26 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + + <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, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> + + + Enable graphic herbalism + + + @@ -829,7 +1178,7 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C - + Screenshot Format @@ -857,6 +1206,13 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + + Notify on saved screenshot + + + diff --git a/files/ui/contentselector.ui b/files/ui/contentselector.ui index c099649b31..f19e2ba487 100644 --- a/files/ui/contentselector.ui +++ b/files/ui/contentselector.ui @@ -34,9 +34,6 @@ - - Content - 3 diff --git a/files/ui/datafilespage.ui b/files/ui/datafilespage.ui index ccac5050ed..c2cbfe6f2e 100644 --- a/files/ui/datafilespage.ui +++ b/files/ui/datafilespage.ui @@ -2,12 +2,253 @@ DataFilesPage + + + 0 + 0 + 571 + 384 + + Qt::DefaultContextMenu - + + + 0 + + + + Content Files + + + + + + + + + <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> + + + + + + + + Data Directories + + + + + + QAbstractItemView::InternalMove + + + + + + + Scan directories for likely data directories and append them at the end of the list. + + + Append + + + + + + + Scan directories for likely data directories and insert them above the selected position + + + Insert Above + + + + + + + Move selected directory one position up + + + Move Up + + + + + + + Move selected directory one position down + + + Move Down + + + + + + + Remove selected directory + + + Remove + + + + + + + + 0 + 0 + + + + <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> + + + + + + + + Archive Files + + + + + + QAbstractItemView::InternalMove + + + + + + + Move selected archive one position up + + + Move Up + + + + + + + Move selected archive one position down + + + Move Down + + + + + + + <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> + + + + + + + + Navigation Mesh Cache + + + + + + + + Qt::TabFocus + + + Generate navigation mesh cache for all content. Will be used by the engine to make cell loading faster. + + + Update + + + + + + + false + + + 0 + + + + + + + false + + + Cancel navigation mesh generation. Already processed data will be saved. + + + Cancel + + + + + + + + + Remove unused tiles + + + true + + + + + + + + + Max size + + + + + + + MiB + + + 2147483647 + + + 2048 + + + + + + + + + true + + + QPlainTextEdit::NoWrap + + + true + + + + + + @@ -20,7 +261,7 @@ false - + 6 diff --git a/files/ui/directorypicker.ui b/files/ui/directorypicker.ui new file mode 100644 index 0000000000..6350bcd2d3 --- /dev/null +++ b/files/ui/directorypicker.ui @@ -0,0 +1,47 @@ + + + SelectSubdirs + + + + 0 + 0 + 800 + 500 + + + + Select directories you wish to add + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + confirmButton + accepted() + SelectSubdirs + accept() + + + confirmButton + rejected() + SelectSubdirs + reject() + + + diff --git a/files/ui/graphicspage.ui b/files/ui/graphicspage.ui index 5ec72611f8..8cf26aa1e2 100644 --- a/files/ui/graphicspage.ui +++ b/files/ui/graphicspage.ui @@ -112,17 +112,39 @@ - + Window Border - - + + + + 0 + + + + Fullscreen + + + + + Windowed Fullscreen + + + + + Windowed + + + + + + - Full Screen + Window Mode: @@ -187,6 +209,56 @@ + + + Lighting + + + + + + + + Lighting Method: + + + + + + + + legacy + + + + + shaders compatibility + + + + + shaders + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + Shadows diff --git a/files/ui/mainwindow.ui b/files/ui/mainwindow.ui index 13e8593e07..54a2d16a1f 100644 --- a/files/ui/mainwindow.ui +++ b/files/ui/mainwindow.ui @@ -52,27 +52,13 @@ - - - + + + + + + Qt::Horizontal - - - 0 - - - 0 - - - 0 - - - 0 - - - - - diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index 4d4d66badc..c2dd260527 100644 --- a/files/ui/wizard/methodselectionpage.ui +++ b/files/ui/wizard/methodselectionpage.ui @@ -147,6 +147,83 @@ + + + + Qt::Horizontal + + + + + + + + + Don't have a copy? + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> + + + + + + + Buy the game + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + diff --git a/files/vfs/CMakeLists.txt b/files/vfs/CMakeLists.txt deleted file mode 100644 index a97210d1df..0000000000 --- a/files/vfs/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -if (NOT DEFINED OPENMW_MYGUI_FILES_ROOT) - return() -endif() - -# Copy resource files into the build directory -set(SDIR ${CMAKE_CURRENT_SOURCE_DIR}) -set(DDIRRELATIVE resources/vfs/textures) - -set(TEXTURE_FILES - textures/omw_menu_scroll_down.dds - textures/omw_menu_scroll_up.dds - textures/omw_menu_scroll_left.dds - textures/omw_menu_scroll_right.dds - textures/omw_menu_scroll_center_h.dds - textures/omw_menu_scroll_center_v.dds -) - -copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_MYGUI_FILES_ROOT} ${DDIRRELATIVE} "${TEXTURE_FILES}") diff --git a/files/wizard/icons/tango/48x48/dollar.png b/files/wizard/icons/tango/48x48/dollar.png new file mode 100644 index 0000000000..a14ba2505d Binary files /dev/null and b/files/wizard/icons/tango/48x48/dollar.png differ diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index 99623e9856..7dbb8fe080 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -4,6 +4,7 @@ icons/tango/index.theme icons/tango/48x48/folder.png icons/tango/48x48/system-installer.png + icons/tango/48x48/dollar.png images/intropage-background.png diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index e53e54b785..0000000000 --- a/readthedocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -# Don't build any extra formats -formats: [] \ No newline at end of file diff --git a/scripts/HOWTO-benchmark.md b/scripts/HOWTO-benchmark.md new file mode 100644 index 0000000000..9641bfcd66 --- /dev/null +++ b/scripts/HOWTO-benchmark.md @@ -0,0 +1,112 @@ +OpenMW ships with its own benchmarking tool. This document describes how to collect performance data and vizualise it. + +Collecting data +=============== + +The data collected is the same as displayed by repeatedly pressing F3 and/or F4 while in the game (correspondance is shown below). +We can select what should be collected by setting the `OPENMW_OSG_STATS_LIST` environment variables to a semi-colon separated list of metrics to collect. There can be any combination and order doesn't matter. Note that data collection can **significantly reduce** performance + +| Metric to collect | Equivalent in-game profiler | +|-------------------|-------------------------------------------------------------------| +| `frame_rate` | Frame rate | +| `engine` | OpenMW specifics metric (white bars) | +| `event` | Event traversal bar | +| `update` | Update traversal bar | +| `rendering` | Draw bar | +| `gpu` | GPU bar | +| `times` | Alias for `event;update;rendering;engine;gpu`, that is all graphs | +| `cameraobjects` | Table shown when pressing F3 3 times | +| `viewerobjects` | Table shown when pressing F3 4 times | +| `resource` | Table shown when pressing F4 | + +It is necessary to write the collected metrics to a file which needs to be defined by the `OPENMW_OSG_STATS_FILE` environment variable. + +Example +------- + +Posix shell +```sh +OPENMW_OSG_STATS_FILE=/tmp/stats OPENMW_OSG_STATS_LIST="resource;engine" /usr/local/bin/openmw +``` + +Windows PowerShell +```powershell +$env:OPENMW_OSG_STATS_FILE="c:\stats" +$env:OPENMW_OSG_STATS_LIST="resource;engine" +openmw +``` + + +Analyzing results +================= + +`scripts/osg_stats.py` is a python script that can perform statistical analysis and generate graphs from a trace. It doesn't need to be run on the same machine as the trace was taken. + +`osg_stats.py --help` list the available options. + +Examples +-------- + +### Print the available metrics in a given trace file + +`osg_stats.py --print_keys /tmp/stats` + +Sample output +>>> +gui_time_begin +gui_time_end +gui_time_taken +input_time_begin +input_time_end +input_time_taken +lua_time_begin +lua_time_end +lua_time_taken +mechanics_time_begin +mechanics_time_end +mechanics_time_taken +physics_time_begin +physics_time_end +physics_time_taken +physicsworker_time_begin +physicsworker_time_end +physicsworker_time_taken +script_time_begin +script_time_end +script_time_taken +sound_time_begin +sound_time_end +sound_time_taken +state_time_begin +state_time_end +state_time_taken +world_time_begin +world_time_end +world_time_taken +>>> + +### Print a table with statistics data for some metrics + +`osg_stats.py --stats physicsworker_time_taken --stats mechanics_time_taken --stats world_time_taken --stats physics_time_taken /tmp/stats` + +Sample output + +> | source | key | number | min | max | mean | median | stdev | q95 | +> |------------|--------------------------|--------|-----|----------|------------------------|----------|------------------------|----------------------| +> | /tmp/stats | physicsworker_time_taken | 56245 | 0.0 | 0.01526 | 0.0018826463330073784 | 7.7e-05 | 0.003210274653689913 | 0.009509799999999995 | +> | /tmp/stats | mechanics_time_taken | 56245 | 0.0 | 0.015808 | 0.0030202102942483776 | 0.002177 | 0.002565895193458489 | 0.008489799999999995 | +> | /tmp/stats | world_time_taken | 56245 | 0.0 | 0.003643 | 5.230777846919726e-05 | 5.1e-05 | 1.7322475294417906e-05 | 6.6e-05 | +> | /tmp/stats | physics_time_taken | 56245 | 0.0 | 0.004407 | 0.00019985403146946396 | 0.000129 | 0.0002106166003676915 | 0.000697 | + + +### Plot a timeserie of aforementioned metrics, ignoring first 1000 frames + +`osg_stats.py --begin_frame 1000 --end_frame 1200 --timeseries physicsworker_time_taken --timeseries mechanics_time_taken --timeseries world_time_taken --timeseries physics_time_taken /tmp/stats` + +### Plot time spent in physics and mechanics depending on number of actors in the scene + +`osg_stats.py --plot 'Physics Actors' physicsworker_time_taken mean --plot 'Physics Actors' mechanics_time_taken mean --plot 'Physics Actors' physics_time_taken mean /tmp/stats` + +### Plot timeserie from 2 traces + +`osg_stats.py --timeseries 'Frame Duration' /tmp/shadowson /tmp/shadowsoff` diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua new file mode 100644 index 0000000000..63e88b0556 --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -0,0 +1,72 @@ +local testing = require('testing_util') +local self = require('openmw.self') +local util = require('openmw.util') +local core = require('openmw.core') +local input = require('openmw.input') +local types = require('openmw.types') + +input.setControlSwitch(input.CONTROL_SWITCH.Fighting, false) +input.setControlSwitch(input.CONTROL_SWITCH.Jumping, false) +input.setControlSwitch(input.CONTROL_SWITCH.Looking, false) +input.setControlSwitch(input.CONTROL_SWITCH.Magic, false) +input.setControlSwitch(input.CONTROL_SWITCH.VanityMode, false) +input.setControlSwitch(input.CONTROL_SWITCH.ViewMode, false) + +testing.registerLocalTest('playerRotation', + function() + local endTime = core.getSimulationTime() + 1 + while core.getSimulationTime() < endTime do + self.controls.jump = false + self.controls.run = true + self.controls.movement = 0 + self.controls.sideMovement = 0 + self.controls.yawChange = util.normalizeAngle(math.rad(90) - self.rotation.z) * 0.5 + coroutine.yield() + end + testing.expectEqualWithDelta(self.rotation.z, math.rad(90), 0.05, 'Incorrect rotation') + end) + +testing.registerLocalTest('playerForwardRunning', + function() + local startPos = self.position + local endTime = core.getSimulationTime() + 1 + while core.getSimulationTime() < endTime do + self.controls.jump = false + self.controls.run = true + self.controls.movement = 1 + self.controls.sideMovement = 0 + self.controls.yawChange = 0 + coroutine.yield() + end + local direction, distance = (self.position - startPos):normalize() + local normalizedDistance = distance / types.Actor.runSpeed(self) + testing.expectEqualWithDelta(normalizedDistance, 1, 0.2, 'Normalized forward runned distance') + testing.expectEqualWithDelta(direction.x, 0, 0.1, 'Run forward, X coord') + testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord') + end) + +testing.registerLocalTest('playerDiagonalWalking', + function() + local startPos = self.position + local endTime = core.getSimulationTime() + 1 + while core.getSimulationTime() < endTime do + self.controls.jump = false + self.controls.run = false + self.controls.movement = -1 + self.controls.sideMovement = -1 + self.controls.yawChange = 0 + coroutine.yield() + end + local direction, distance = (self.position - startPos):normalize() + local normalizedDistance = distance / types.Actor.walkSpeed(self) + testing.expectEqualWithDelta(normalizedDistance, 1, 0.2, 'Normalized diagonally walked distance') + testing.expectEqualWithDelta(direction.x, -0.707, 0.1, 'Walk diagonally, X coord') + testing.expectEqualWithDelta(direction.y, -0.707, 0.1, 'Walk diagonally, Y coord') + end) + +return { + engineHandlers = { + onUpdate = testing.updateLocal, + }, + eventHandlers = testing.eventHandlers +} diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua new file mode 100644 index 0000000000..ba0d45ab23 --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -0,0 +1,83 @@ +local testing = require('testing_util') +local core = require('openmw.core') +local async = require('openmw.async') +local util = require('openmw.util') + +local function testTimers() + testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') + testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result') + + local startGameTime = core.getGameTime() + local startSimulationTime = core.getSimulationTime() + + local ts1, ts2, th1, th2 + local cb = async:registerTimerCallback("tfunc", function(arg) + if arg == 'g' then + th1 = core.getGameTime() - startGameTime + else + ts1 = core.getSimulationTime() - startSimulationTime + end + end) + async:newGameTimer(36, cb, 'g') + async:newSimulationTimer(0.5, cb, 's') + async:newUnsavableGameTimer(72, function() + th2 = core.getGameTime() - startGameTime + end) + async:newUnsavableSimulationTimer(1, function() + ts2 = core.getSimulationTime() - startSimulationTime + end) + + while not (ts1 and ts2 and th1 and th2) do coroutine.yield() end + + testing.expectGreaterOrEqual(th1, 36, 'async:newGameTimer failed') + testing.expectGreaterOrEqual(ts1, 0.5, 'async:newSimulationTimer failed') + testing.expectGreaterOrEqual(th2, 72, 'async:newUnsavableGameTimer failed') + testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed') +end + +local function testTeleport() + player:teleport('', util.vector3(100, 50, 0), util.vector3(0, 0, math.rad(-90))) + coroutine.yield() + testing.expect(player.cell.isExterior, 'teleport to exterior failed') + testing.expectEqualWithDelta(player.position.x, 100, 1, 'incorrect position after teleporting') + testing.expectEqualWithDelta(player.position.y, 50, 1, 'incorrect position after teleporting') + testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'incorrect rotation after teleporting') + + player:teleport('', util.vector3(50, -100, 0)) + coroutine.yield() + testing.expect(player.cell.isExterior, 'teleport to exterior failed') + testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting') + testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting') + testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'teleporting changes rotation') +end + +local function initPlayer() + player:teleport('', util.vector3(4096, 4096, 867.237), util.vector3(0, 0, 0)) + coroutine.yield() +end + +tests = { + {'timers', testTimers}, + {'playerRotation', function() + initPlayer() + testing.runLocalTest(player, 'playerRotation') + end}, + {'playerForwardRunning', function() + initPlayer() + testing.runLocalTest(player, 'playerForwardRunning') + end}, + {'playerDiagonalWalking', function() + initPlayer() + testing.runLocalTest(player, 'playerDiagonalWalking') + end}, + {'teleport', testTeleport}, +} + +return { + engineHandlers = { + onUpdate = testing.testRunner(tests), + onPlayerAdded = function(p) player = p end, + }, + eventHandlers = testing.eventHandlers, +} + diff --git a/scripts/data/integration_tests/test_lua_api/test.omwscripts b/scripts/data/integration_tests/test_lua_api/test.omwscripts new file mode 100644 index 0000000000..97f523afbd --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/test.omwscripts @@ -0,0 +1,3 @@ +GLOBAL: test.lua +PLAYER: player.lua + diff --git a/scripts/data/integration_tests/test_lua_api/testing_util.lua b/scripts/data/integration_tests/test_lua_api/testing_util.lua new file mode 100644 index 0000000000..5ce5b2ff26 --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/testing_util.lua @@ -0,0 +1,99 @@ +local core = require('openmw.core') + +local M = {} +local currentLocalTest = nil +local currentLocalTestError = nil + +function M.testRunner(tests) + local fn = function() + for i, test in ipairs(tests) do + local name, fn = unpack(test) + print('TEST_START', i, name) + local status, err = pcall(fn) + if status then + print('TEST_OK', i, name) + else + print('TEST_FAILED', i, name, err) + end + end + core.quit() + end + local co = coroutine.create(fn) + return function() + if coroutine.status(co) ~= 'dead' then + coroutine.resume(co) + end + end +end + +function M.runLocalTest(obj, name) + currentLocalTest = name + currentLocalTestError = nil + obj:sendEvent('runLocalTest', name) + while currentLocalTest do coroutine.yield() end + if currentLocalTestError then error(currentLocalTestError, 2) end +end + +function M.expect(cond, delta, msg) + if not cond then + error(msg or '"true" expected', 2) + end +end + +function M.expectEqualWithDelta(v1, v2, delta, msg) + if math.abs(v1 - v2) > delta then + error(string.format('%s: %f ~= %f', msg or '', v1, v2), 2) + end +end + +function M.expectAlmostEqual(v1, v2, msg) + if math.abs(v1 - v2) / (math.abs(v1) + math.abs(v2)) > 0.05 then + error(string.format('%s: %f ~= %f', msg or '', v1, v2), 2) + end +end + +function M.expectGreaterOrEqual(v1, v2, msg) + if not (v1 >= v2) then + error(string.format('%s: %f >= %f', msg or '', v1, v2), 2) + end +end + +local localTests = {} +local localTestRunner = nil + +function M.registerLocalTest(name, fn) + localTests[name] = fn +end + +function M.updateLocal() + if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then + coroutine.resume(localTestRunner) + else + localTestRunner = nil + end +end + +M.eventHandlers = { + runLocalTest = function(name) -- used only in local scripts + fn = localTests[name] + if not fn then + core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Test not found'}) + return + end + localTestRunner = coroutine.create(function() + local status, err = pcall(fn) + if status then err = nil end + core.sendGlobalEvent('localTestFinished', {name=name, errMsg=err}) + end) + end, + localTestFinished = function(data) -- used only in global scripts + if data.name ~= currentLocalTest then + error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest)) + end + currentLocalTest = nil + currentLocalTestError = data.errMsg + end, +} + +return M + diff --git a/scripts/find_missing_merge_requests.py b/scripts/find_missing_merge_requests.py new file mode 100755 index 0000000000..1cca15c03b --- /dev/null +++ b/scripts/find_missing_merge_requests.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +import click +import discord_webhook +import multiprocessing +import os +import pathlib +import requests +import urllib.parse + + +@click.command() +@click.option('--token_path', type=str, default=pathlib.Path.home() / '.gitlab_token', + help='Path to text file with Gitlab token.') +@click.option('--project_id', type=int, default=7107382, + help='Gitlab project id.') +@click.option('--job_id', type=int, default=os.getenv('CI_JOB_ID'), + help='Gitlab job id.') +@click.option('--host', type=str, default='gitlab.com', + help='Gitlab host.') +@click.option('--workers', type=int, default=10, + help='Number of parallel workers.') +@click.option('--target_branch', type=str, default='master', + help='Merge request target branch.') +@click.option('--begin_page', type=int, default=1, + help='Begin with given /merge_requests page.') +@click.option('--end_page', type=int, default=4, + help='End before given /merge_requests page.') +@click.option('--per_page', type=int, default=100, + help='Number of merge requests per page.') +@click.option('--ignored_mrs_path', type=str, + help='Path to a list of ignored MRs.') +def main(token_path, project_id, job_id, host, workers, target_branch, begin_page, end_page, per_page, ignored_mrs_path): + headers = make_headers(token_path) + base_url = f'https://{host}/api/v4/projects/{project_id}/' + discord_webhook_url = os.getenv('DISCORD_WEBHOOK_URL') + ignored_mrs = frozenset(read_ignored_mrs(ignored_mrs_path)) + checked = 0 + filtered = 0 + missing = 0 + missing_mrs = list() + for page in range(begin_page, end_page): + merge_requests = parse_gitlab_response(requests.get( + url=urllib.parse.urljoin(base_url, 'merge_requests'), + headers=headers, + params=dict(state='merged', per_page=per_page, page=page), + )) + if not merge_requests: + break + checked += len(merge_requests) + merge_requests = [v for v in merge_requests if v['target_branch'] == target_branch] + if not merge_requests: + continue + filtered += len(merge_requests) + with multiprocessing.Pool(workers) as pool: + missing_merge_requests = pool.map(FilterMissingMergeRequest(headers, base_url), merge_requests) + for mr in missing_merge_requests: + if mr is None: + continue + if mr['reference'] in ignored_mrs or mr['reference'].strip('!') in ignored_mrs: + print(f"Ignored MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']}," + f" previously was merged as {mr['merge_commit_sha']}") + continue + missing += 1 + missing_mrs.append(mr) + print(f"MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']}," + f" previously was merged as {mr['merge_commit_sha']}") + print(f'Checked {checked} MRs ({filtered} with {target_branch} target branch), {missing} are missing') + if discord_webhook_url is not None and missing_mrs: + project_web_url = parse_gitlab_response(requests.get(url=base_url, headers=headers))['web_url'] + '/' + discord_message = format_discord_message(missing=missing, filtered=filtered, target_branch=target_branch, + project_web_url=project_web_url, missing_mrs=missing_mrs, + job_id=job_id) + print('Sending Discord notification...') + print(discord_message) + discord_webhook.DiscordWebhook(url=discord_webhook_url, content=discord_message, rate_limit_retry=True).execute() + if missing_mrs: + exit(-1) + + +def format_discord_message(missing, filtered, target_branch, project_web_url, missing_mrs, job_id): + target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}')) + job = f' by job ' + format_link(job_id, urllib.parse.urljoin(project_web_url, f'-/jobs/{job_id}')) if job_id else '' + return ( + f'Found {missing} missing MRs out of {filtered} from {target_branch} target branch{job}:\n' + + '\n'.join(format_missing_mr_message(v, project_web_url) for v in missing_mrs) + ) + + +def format_missing_mr_message(mr, project_web_url): + web_url = mr.get('web_url') + reference = mr['reference'] + target_branch = mr['target_branch'] + commit = mr['merge_commit_sha'] + if web_url is not None: + reference = format_link(reference, web_url) + target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}')) + commit = format_link(commit, urllib.parse.urljoin(project_web_url, f'-/commit/{commit}')) + return f"MR {reference} is missing from branch {target_branch}, previously was merged as {commit}" + + +def format_link(name, url): + return f'[{name}]({url})' + + +class FilterMissingMergeRequest: + def __init__(self, headers, base_url): + self.headers = headers + self.base_url = base_url + + def __call__(self, merge_request): + commit_refs = requests.get( + url=urllib.parse.urljoin(self.base_url, f"repository/commits/{merge_request['merge_commit_sha']}/refs"), + headers=self.headers, + ).json() + if 'message' in commit_refs and commit_refs['message'] == '404 Commit Not Found': + return merge_request + if not present_in_branch(commit_refs, branch=merge_request['target_branch']): + return merge_request + + +def present_in_branch(commit_refs, branch): + return bool(next((v for v in commit_refs if v['type'] == 'branch' and v['name'] == branch), None)) + + +def make_headers(token_path): + job_token = os.environ.get('CI_JOB_TOKEN') + if job_token is not None: + print('Using auth token from CI_JOB_TOKEN env') + return {'JOB_TOKEN': job_token} + if not os.path.exists(token_path): + print(f'Ignore absent token path: {token_path}') + return dict() + print(f'Using auth token from: {token_path}') + return {'PRIVATE-TOKEN': read_token(token_path)} + + +def read_ignored_mrs(path): + if path is None: + return + with open(path) as stream: + for line in stream: + yield line.strip() + + +def read_token(path): + with open(path) as stream: + return stream.readline().strip() + + +def parse_gitlab_response(response): + response = response.json() + if isinstance(response, dict): + message = response.get('message') + if message is not None: + raise RuntimeError(f'Gitlab request has failed: {message}') + return response + + +if __name__ == '__main__': + main() diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py new file mode 100755 index 0000000000..d106c71283 --- /dev/null +++ b/scripts/integration_tests.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import argparse, datetime, os, subprocess, sys, shutil +from pathlib import Path + +parser = argparse.ArgumentParser(description="OpenMW integration tests.") +parser.add_argument( + "example_suite", + type=str, + help="path to openmw example suite (use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it)", +) +parser.add_argument("--omw", type=str, default="openmw", help="path to openmw binary") +parser.add_argument( + "--workdir", type=str, default="integration_tests_output", help="directory for temporary files and logs" +) +args = parser.parse_args() + +example_suite_dir = Path(args.example_suite).resolve() +example_suite_content = example_suite_dir / "game_template" / "data" / "template.omwgame" +if not example_suite_content.is_file(): + sys.exit( + f"{example_suite_content} not found, use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it" + ) + +openmw_binary = Path(args.omw).resolve() +if not openmw_binary.is_file(): + sys.exit(f"{openmw_binary} not found") + +work_dir = Path(args.workdir).resolve() +work_dir.mkdir(parents=True, exist_ok=True) +config_dir = work_dir / "config" +userdata_dir = work_dir / "userdata" +tests_dir = Path(__file__).resolve().parent / "data" / "integration_tests" +time_str = datetime.datetime.now().strftime("%Y-%m-%d-%H.%M.%S") + + +def runTest(name): + print(f"Start {name}") + shutil.rmtree(config_dir, ignore_errors=True) + config_dir.mkdir() + shutil.copyfile(example_suite_dir / "settings.cfg", config_dir / "settings.cfg") + test_dir = tests_dir / name + with open(config_dir / "openmw.cfg", "w", encoding="utf-8") as omw_cfg: + omw_cfg.writelines( + ( + f'data="{example_suite_dir}{os.sep}game_template{os.sep}data"\n', + f'data-local="{test_dir}"\n', + f'user-data="{userdata_dir}"\n', + "content=template.omwgame\n", + ) + ) + if (test_dir / "test.omwscripts").exists(): + omw_cfg.write("content=test.omwscripts\n") + with open(config_dir / "settings.cfg", "a", encoding="utf-8") as settings_cfg: + settings_cfg.write( + "[Video]\n" + "resolution x = 640\n" + "resolution y = 480\n" + "framerate limit = 60\n" + ) + stdout_lines = list() + exit_ok = True + test_success = True + with subprocess.Popen( + [openmw_binary, "--replace=config", "--config", config_dir, "--skip-menu", "--no-grab", "--no-sound"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + env={ + "OPENMW_OSG_STATS_FILE": work_dir / f"{name}.{time_str}.osg_stats.log", + "OPENMW_OSG_STATS_LIST": "times", + **os.environ, + }, + ) as process: + quit_requested = False + for line in process.stdout: + stdout_lines.append(line) + words = line.split(" ") + if len(words) > 1 and words[1] == "E]": + print(line, end="") + elif "Quit requested by a Lua script" in line: + quit_requested = True + elif "TEST_START" in line: + w = line.split("TEST_START")[1].split("\t") + print(f"TEST {w[2].strip()}\t\t", end="") + elif "TEST_OK" in line: + print(f"OK") + elif "TEST_FAILED" in line: + w = line.split("TEST_FAILED")[1].split("\t") + print(f"FAILED {w[3]}\t\t") + test_success = False + process.wait(5) + if not quit_requested: + print("ERROR: Unexpected termination") + exit_ok = False + if process.returncode != 0: + print(f"ERROR: openmw exited with code {process.returncode}") + exit_ok = False + if os.path.exists(config_dir / "openmw.log"): + shutil.copyfile(config_dir / "openmw.log", work_dir / f"{name}.{time_str}.log") + if not exit_ok: + sys.stdout.writelines(stdout_lines) + if test_success and exit_ok: + print(f"{name} succeeded") + else: + print(f"{name} failed") + return test_success and exit_ok + + +status = 0 +for entry in tests_dir.glob("test_*"): + if entry.is_dir(): + if not runTest(entry.name): + status = -1 +shutil.rmtree(config_dir, ignore_errors=True) +shutil.rmtree(userdata_dir, ignore_errors=True) +exit(status) diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py new file mode 100755 index 0000000000..3a5067851d --- /dev/null +++ b/scripts/osg_stats.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +osg_stats.py is a script to analyze OpenSceneGraph log. It parses given file +and builds timeseries, histograms, plots, calculate statistics for a given +set of keys over given range of frames. +""" + +import click +import collections +import matplotlib.pyplot +import numpy +import statistics +import sys +import termtables +import re + + +@click.command() +@click.option('--print_keys', is_flag=True, + help='Print a list of all present keys in the input file.') +@click.option('--regexp_match', is_flag=True, + help='Use all metric that match given key. ' + 'Can be used with stats, timeseries, commulative_timeseries, hist, hist_threshold') +@click.option('--timeseries', type=str, multiple=True, + help='Show a graph for given metric over time.') +@click.option('--commulative_timeseries', type=str, multiple=True, + help='Show a graph for commulative sum of a given metric over time.') +@click.option('--hist', type=str, multiple=True, + help='Show a histogram for all values of given metric.') +@click.option('--hist_ratio', nargs=2, type=str, multiple=True, + help='Show a histogram for a ratio of two given metric (first / second). ' + 'Format: --hist_ratio .') +@click.option('--stdev_hist', nargs=2, type=str, multiple=True, + help='Show a histogram for a standard deviation of a given metric at given scale (number). ' + 'Format: --stdev_hist .') +@click.option('--plot', nargs=3, type=str, multiple=True, + help='Show a 2D plot for relation between two metrix (first is axis x, second is y)' + 'using one of aggregation functions (mean, median). For example show a relation ' + 'between Physics Actors and physics_time_taken. Format: --plot .') +@click.option('--stats', type=str, multiple=True, + help='Print table with stats for a given metric containing min, max, mean, median etc.') +@click.option('--precision', type=int, + help='Format floating point numbers with given precision') +@click.option('--timeseries_sum', is_flag=True, + help='Add a graph to timeseries for a sum per frame of all given timeseries metrics.') +@click.option('--commulative_timeseries_sum', is_flag=True, + help='Add a graph to timeseries for a sum per frame of all given commulative timeseries.') +@click.option('--stats_sum', is_flag=True, + help='Add a row to stats table for a sum per frame of all given stats metrics.') +@click.option('--begin_frame', type=int, default=0, + help='Start processing from this frame.') +@click.option('--end_frame', type=int, default=sys.maxsize, + help='End processing at this frame.') +@click.option('--frame_number_name', type=str, default='FrameNumber', + help='Frame number metric name.') +@click.option('--hist_threshold', type=str, multiple=True, + help='Show a histogram for given metric only for frames with threshold_name metric over threshold_value.') +@click.option('--threshold_name', type=str, default='Frame duration', + help='Frame duration metric name.') +@click.option('--threshold_value', type=float, default=1.05/60, + help='Threshold for hist_over.') +@click.argument('path', type=click.Path(), nargs=-1) +def main(print_keys, regexp_match, timeseries, hist, hist_ratio, stdev_hist, plot, stats, precision, + timeseries_sum, stats_sum, begin_frame, end_frame, path, + commulative_timeseries, commulative_timeseries_sum, frame_number_name, + hist_threshold, threshold_name, threshold_value): + sources = {v: list(read_data(v)) for v in path} if path else {'stdin': list(read_data(None))} + keys = collect_unique_keys(sources) + frames, begin_frame, end_frame = collect_per_frame( + sources=sources, keys=keys, begin_frame=begin_frame, + end_frame=end_frame, frame_number_name=frame_number_name, + ) + if print_keys: + for v in keys: + print(v) + def matching_keys(patterns): + if regexp_match: + return [key for pattern in patterns for key in keys if re.search(pattern, key)] + return patterns + if timeseries: + draw_timeseries(sources=frames, keys=matching_keys(timeseries), add_sum=timeseries_sum, + begin_frame=begin_frame, end_frame=end_frame) + if commulative_timeseries: + draw_commulative_timeseries(sources=frames, keys=matching_keys(commulative_timeseries), add_sum=commulative_timeseries_sum, + begin_frame=begin_frame, end_frame=end_frame) + if hist: + draw_hists(sources=frames, keys=matching_keys(hist)) + if hist_ratio: + draw_hist_ratio(sources=frames, pairs=hist_ratio) + if stdev_hist: + draw_stdev_hists(sources=frames, stdev_hists=stdev_hist) + if plot: + draw_plots(sources=frames, plots=plot) + if stats: + print_stats(sources=frames, keys=matching_keys(stats), stats_sum=stats_sum, precision=precision) + if hist_threshold: + draw_hist_threshold(sources=frames, keys=matching_keys(hist_threshold), begin_frame=begin_frame, + threshold_name=threshold_name, threshold_value=threshold_value) + matplotlib.pyplot.show() + + +def read_data(path): + with open(path) if path else sys.stdin as stream: + frame = dict() + camera = 0 + for line in stream: + if line.startswith('Stats Viewer'): + if frame: + camera = 0 + yield frame + _, _, key, value = line.split(' ') + frame = {key: int(value)} + elif line.startswith('Stats Camera'): + camera += 1 + elif line.startswith(' '): + key, value = line.strip().rsplit(maxsplit=1) + if camera: + key = f'{key} Camera {camera}' + frame[key] = to_number(value) + + +def collect_per_frame(sources, keys, begin_frame, end_frame, frame_number_name): + assert begin_frame < end_frame + result = collections.defaultdict(lambda: collections.defaultdict(list)) + begin_frame = max(begin_frame, min(v[0][frame_number_name] for v in sources.values())) + end_frame = min(end_frame, begin_frame + max(len(v) for v in sources.values())) + for name in sources.keys(): + for key in keys: + result[name][key] = [0] * (end_frame - begin_frame) + for name, frames in sources.items(): + for frame in frames: + number = frame[frame_number_name] + if begin_frame <= number < end_frame: + index = number - begin_frame + for key in keys: + if key in frame: + result[name][key][index] = frame[key] + for name in result.keys(): + for key in keys: + result[name][key] = numpy.array(result[name][key]) + return result, begin_frame, end_frame + + +def collect_unique_keys(sources): + result = set() + for frames in sources.values(): + for frame in frames: + for key in frame.keys(): + result.add(key) + return sorted(result) + + +def draw_timeseries(sources, keys, add_sum, begin_frame, end_frame): + fig, ax = matplotlib.pyplot.subplots() + x = numpy.array(range(begin_frame, end_frame)) + for name, frames in sources.items(): + for key in keys: + ax.plot(x, frames[key], label=f'{key}:{name}') + if add_sum: + ax.plot(x, numpy.sum(list(frames[k] for k in keys), axis=0), label=f'sum:{name}') + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('timeseries') + + +def draw_commulative_timeseries(sources, keys, add_sum, begin_frame, end_frame): + fig, ax = matplotlib.pyplot.subplots() + x = numpy.array(range(begin_frame, end_frame)) + for name, frames in sources.items(): + for key in keys: + ax.plot(x, numpy.cumsum(frames[key]), label=f'{key}:{name}') + if add_sum: + ax.plot(x, numpy.cumsum(numpy.sum(list(frames[k] for k in keys), axis=0)), label=f'sum:{name}') + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('commulative_timeseries') + + +def draw_hists(sources, keys): + fig, ax = matplotlib.pyplot.subplots() + bins = numpy.linspace( + start=min(min(min(v) for k, v in f.items() if k in keys) for f in sources.values()), + stop=max(max(max(v) for k, v in f.items() if k in keys) for f in sources.values()), + num=20, + ) + for name, frames in sources.items(): + for key in keys: + ax.hist(frames[key], bins=bins, label=f'{key}:{name}', alpha=1 / (len(keys) * len(sources))) + ax.set_xticks(bins) + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('hists') + + +def draw_hist_ratio(sources, pairs): + fig, ax = matplotlib.pyplot.subplots() + bins = numpy.linspace( + start=min(min(min(a / b for a, b in zip(f[a], f[b])) for a, b in pairs) for f in sources.values()), + stop=max(max(max(a / b for a, b in zip(f[a], f[b])) for a, b in pairs) for f in sources.values()), + num=20, + ) + for name, frames in sources.items(): + for a, b in pairs: + ax.hist(frames[a] / frames[b], bins=bins, label=f'{a} / {b}:{name}', alpha=1 / (len(pairs) * len(sources))) + ax.set_xticks(bins) + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('hists_ratio') + + +def draw_stdev_hists(sources, stdev_hists): + for key, scale in stdev_hists: + scale = float(scale) + fig, ax = matplotlib.pyplot.subplots() + first_frames = next(v for v in sources.values()) + median = statistics.median(first_frames[key]) + stdev = statistics.stdev(first_frames[key]) + start = median - stdev / 2 * scale + stop = median + stdev / 2 * scale + bins = numpy.linspace(start=start, stop=stop, num=9) + for name, frames in sources.items(): + values = [v for v in frames[key] if start <= v <= stop] + ax.hist(values, bins=bins, label=f'{key}:{name}', alpha=1 / (len(stdev_hists) * len(sources))) + ax.set_xticks(bins) + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('stdev_hists') + + +def draw_plots(sources, plots): + fig, ax = matplotlib.pyplot.subplots() + for name, frames in sources.items(): + for x_key, y_key, agg in plots: + if agg is None: + ax.plot(frames[x_key], frames[y_key], label=f'x={x_key}, y={y_key}:{name}') + elif agg: + agg_f = dict( + mean=statistics.mean, + median=statistics.median, + )[agg] + grouped = collections.defaultdict(list) + for x, y in zip(frames[x_key], frames[y_key]): + grouped[x].append(y) + aggregated = sorted((k, agg_f(v)) for k, v in grouped.items()) + ax.plot( + numpy.array([v[0] for v in aggregated]), + numpy.array([v[1] for v in aggregated]), + label=f'x={x_key}, y={y_key}, agg={agg}:{name}', + ) + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('plots') + + +def print_stats(sources, keys, stats_sum, precision): + stats = list() + for name, frames in sources.items(): + for key in keys: + stats.append(make_stats(source=name, key=key, values=filter_not_none(frames[key]), precision=precision)) + if stats_sum: + stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys), precision=precision)) + metrics = list(stats[0].keys()) + termtables.print( + [list(v.values()) for v in stats], + header=metrics, + style=termtables.styles.markdown, + ) + + +def draw_hist_threshold(sources, keys, begin_frame, threshold_name, threshold_value): + for name, frames in sources.items(): + indices = [n for n, v in enumerate(frames[threshold_name]) if v > threshold_value] + numbers = [v + begin_frame for v in indices] + x = [v for v in range(0, len(indices))] + fig, ax = matplotlib.pyplot.subplots() + ax.set_title(f'Frames with "{threshold_name}" > {threshold_value} ({len(indices)})') + ax.bar(x, [frames[threshold_name][v] for v in indices], label=threshold_name, color='black', alpha=0.2) + prev = 0 + for key in keys: + values = [frames[key][v] for v in indices] + ax.bar(x, values, bottom=prev, label=key) + prev = values + ax.hlines(threshold_value, x[0] - 1, x[-1] + 1, color='black', label='threshold', linestyles='dashed') + ax.xaxis.set_major_locator(matplotlib.pyplot.FixedLocator(x)) + ax.xaxis.set_major_formatter(matplotlib.pyplot.FixedFormatter(numbers)) + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title(f'hist_threshold:{name}') + + +def filter_not_none(values): + return [v for v in values if v is not None] + + +def fixed_float(value, precision): + return '{v:.{p}f}'.format(v=value, p=precision) if precision else value + + +def sum_multiple(frames, keys): + result = collections.Counter() + for key in keys: + values = frames[key] + for i, value in enumerate(values): + if value is not None: + result[i] += float(value) + return numpy.array([result[k] for k in sorted(result.keys())]) + + +def make_stats(source, key, values, precision): + return collections.OrderedDict( + source=source, + key=key, + number=len(values), + min=fixed_float(min(values), precision), + max=fixed_float(max(values), precision), + mean=fixed_float(statistics.mean(values), precision), + median=fixed_float(statistics.median(values), precision), + stdev=fixed_float(statistics.stdev(values), precision), + q95=fixed_float(numpy.quantile(values, 0.95), precision), + ) + + +def to_number(value): + try: + return int(value) + except ValueError: + return float(value) + + +if __name__ == '__main__': + main()