diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 29f67d75e..f5c1fe269 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,8 @@ stages: - apt-cache/ - ccache/ stage: build + rules: + - if: '$CI_PIPELINE_SOURCE != "schedule"' script: - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" @@ -30,6 +32,30 @@ stages: paths: - build/install/ +Coverity: + extends: .Debian_Image + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity + - 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 + # Add more than just `openmw` once we can build everything under 3h + - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw + 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="`git describe --tags`" + --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" + variables: + CC: gcc + CXX: g++ + timeout: 8h + Debian_GCC: extends: .Debian cache: @@ -93,125 +119,152 @@ Debian_Clang_tests: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 -MacOS: +.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 + - 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" +macOS11_Xcode12: + extends: .MacOS + image: macos-11-xcode-12 + cache: + key: macOS11_Xcode12.v1 + variables: + CCACHE_SIZE: 3G + +macOS10.15_Xcode11: + extends: .MacOS + image: macos-10.15-xcode-11 + allow_failure: true + cache: + key: macOS10.15_Xcode11.v1 + variables: + CCACHE_SIZE: 3G + variables: &engine-targets - targets: "openmw_vr,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" + targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" package: "Engine" variables: &cs-targets targets: "openmw-cs,bsatool,esmtool,niftest" package: "CS" -#.Windows_Ninja_Base: -# tags: -# - windows -# 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 install git --force --params "/GitAndUnixToolsOnPath" -y -# - choco install 7zip -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 -# 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 -# - cd MSVC2019_64_Ninja -# - .\ActivateMSVC.ps1 -# - cmake --build . --config $config --target ($targets.Split(',')) -# - 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 -# - | -# if (Get-ChildItem -Recurse *.pdb) { -# 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${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_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' -# after_script: -# - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log -# cache: -# key: ninja-v2 -# paths: -# - deps -# - MSVC2019_64_Ninja/deps/Qt -# artifacts: -# when: always -# paths: -# - "*.zip" -# - "*.log" -# - MSVC2019_64_Ninja/*.log -# - MSVC2019_64_Ninja/*/*.log -# - MSVC2019_64_Ninja/*/*/*.log -# - MSVC2019_64_Ninja/*/*/*/*.log -# - MSVC2019_64_Ninja/*/*/*/*/*.log -# - MSVC2019_64_Ninja/*/*/*/*/*/*.log -# - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log -# - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log +.Windows_Ninja_Base: + tags: + - windows + 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 install git --force --params "/GitAndUnixToolsOnPath" -y + - choco install 7zip -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 + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE != "schedule"' + 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 + - cd MSVC2019_64_Ninja + - .\ActivateMSVC.ps1 + - cmake --build . --config $config --target ($targets.Split(',')) + - 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 + - | + if (Get-ChildItem -Recurse *.pdb) { + 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${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_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' + after_script: + - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log + cache: + key: ninja-v2 + paths: + - deps + - MSVC2019_64_Ninja/deps/Qt + artifacts: + when: always + paths: + - "*.zip" + - "*.log" + - MSVC2019_64_Ninja/*.log + - MSVC2019_64_Ninja/*/*.log + - MSVC2019_64_Ninja/*/*/*.log + - MSVC2019_64_Ninja/*/*/*/*.log + - MSVC2019_64_Ninja/*/*/*/*/*.log + - MSVC2019_64_Ninja/*/*/*/*/*/*.log + - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log + - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log + +Windows_Ninja_Engine_Release: + extends: + - .Windows_Ninja_Base + variables: + <<: *engine-targets + config: "Release" + +Windows_Ninja_Engine_Debug: + 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 + config: "Release" + +Windows_Ninja_CS_Debug: + extends: + - .Windows_Ninja_Base + variables: + <<: *cs-targets + config: "Debug" -# -#Windows_Ninja_Engine_Release: -# extends: -# - .Windows_Ninja_Base -# variables: -# <<: *engine-targets -# config: "Release" -# -#Windows_Ninja_Engine_Debug: -# 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 -# config: "Release" -# -#Windows_Ninja_CS_Debug: -# extends: -# - .Windows_Ninja_Base -# variables: -# <<: *cs-targets -# config: "Debug" -# -#Windows_Ninja_CS_RelWithDebInfo: -# extends: -# - .Windows_Ninja_Base -# variables: -# <<: *cs-targets -# config: "RelWithDebInfo" +Windows_Ninja_CS_RelWithDebInfo: + extends: + - .Windows_Ninja_Base + variables: + <<: *cs-targets + config: "RelWithDebInfo" .Windows_MSBuild_Base: tags: @@ -226,6 +279,8 @@ variables: &cs-targets - choco install python -y - refreshenv stage: build + rules: + - if: '$CI_PIPELINE_SOURCE != "schedule"' script: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} @@ -304,37 +359,37 @@ Windows_MSBuild_CS_RelWithDebInfo: <<: *cs-targets config: "RelWithDebInfo" -#Debian_AndroidNDK_arm64-v8a: -# tags: -# - linux -# image: debian:bullseye -# variables: -# CCACHE_SIZE: 3G -# cache: -# key: Debian_AndroidNDK_arm64-v8a.v3 -# 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 -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer -# stage: build -# script: -# - export CCACHE_BASEDIR="`pwd`" -# - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" -# - ccache -z -M "${CCACHE_SIZE}" -# - CI/before_install.android.sh -# - CI/before_script.android.sh -# - cd build -# - cmake --build . -- -j $(nproc) -# - cmake --install . -# - ccache -s -# 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 +Debian_AndroidNDK_arm64-v8a: + tags: + - linux + image: debian:bullseye + variables: + CCACHE_SIZE: 3G + cache: + key: Debian_AndroidNDK_arm64-v8a.v3 + 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 -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer + stage: build + script: + - export CCACHE_BASEDIR="`pwd`" + - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" + - ccache -z -M "${CCACHE_SIZE}" + - CI/before_install.android.sh + - CI/before_script.android.sh + - cd build + - cmake --build . -- -j $(nproc) + - cmake --install . + - ccache -s + 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 diff --git a/.travis.yml b/.travis.yml index e84a9ad71..8d2ab43f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - sourceline: 'ppa:openmw/openmw' packages: [ # Dev - build-essential, cmake, clang-tools, ccache, + build-essential, cmake, clang-tools-9, ccache, # Boost libboost-filesystem-dev, libboost-iostreams-dev, libboost-program-options-dev, libboost-system-dev, # FFmpeg @@ -22,9 +22,9 @@ addons: ] matrix: include: - - name: OpenMW (all) on MacOS 10.15 with Xcode 11.6 + - name: OpenMW (all) on MacOS 10.15 with Xcode 10.2 os: osx - osx_image: xcode11.6 + osx_image: xcode10.2 - name: OpenMW (all) on Ubuntu Focal with GCC os: linux dist: focal @@ -37,8 +37,8 @@ matrix: os: linux dist: focal env: - - MATRIX_EVAL="CC=clang && CXX=clang++" - - ANALYZE="scan-build --force-analyze-debug-code --use-cc clang --use-c++ clang++" + - MATRIX_EVAL="CC=clang-9 && CXX=clang++-9" + - ANALYZE="scan-build-9 --force-analyze-debug-code --use-cc clang-9 --use-c++ clang++-9" compiler: clang before_install: @@ -66,11 +66,13 @@ deploy: repo: OpenMW/openmw notifications: email: + if: repository_slug = OpenMW/openmw AND branch = master recipients: - corrmage+travis-ci@gmail.com on_success: change on_failure: always irc: + if: repository_slug = OpenMW/openmw AND branch = master channels: - "chat.freenode.net#openmw" on_success: change diff --git a/AUTHORS.md b/AUTHORS.md index b30168fd8..75302908e 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,7 +24,7 @@ Programmers Alex McKibben alexanderkjall Alexander Nadeau (wareya) - Alexander Olofsson (Ace) + Alexander Olofsson (Ananace) Alex Rice Alex S (docwest) Allofich @@ -49,6 +49,7 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) @@ -89,6 +90,7 @@ Programmers Internecine Jackerty Jacob Essex (Yacoby) + Jacob Turnbull (Tankinfrank) Jake Westrip (16bitint) James Carty (MrTopCat) James Moore (moore.work) @@ -150,6 +152,7 @@ Programmers Nathan Jeffords (blunted2night) NeveHanter Nialsy + Nick Crawford (nighthawk469) Nikolay Kasyanov (corristo) nobrakal Nolan Poe (nopoe) @@ -184,6 +187,7 @@ Programmers sergoz ShadowRadiance Siimacore + Simon Meulenbeek (simonmb) sir_herrbatka smbas Sophie Kirschner (pineapplemachine) @@ -197,6 +201,7 @@ Programmers Sylvain Thesnieres (Garvek) t6 terrorfisch + Tess (tescoShoppah) thegriglat Thomas Luppi (Digmaster) tlmullis @@ -235,7 +240,8 @@ Documentation 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 b1fea773e..dec5e1e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,9 @@ 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 Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references Feature #1536: Show more information about level on menu @@ -122,8 +125,10 @@ Feature #2404: Levelled List can not be placed into a container Feature #2686: Timestamps in openmw.log Feature #3171: OpenMW-CS: Instance drag selection + Feature #3983: Wizard: Add link to buy Morrowind 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 @@ -133,6 +138,7 @@ 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 #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 #5545: Option to allow stealing from an unconscious NPC during combat @@ -147,6 +153,8 @@ 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 Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 6dad0abc1..c03341b86 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,9 +1,9 @@ #!/bin/sh -ex # workaround python issue on travis -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true +[-z "${TRAVIS}"] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true +[-z "${TRAVIS}"] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true +[-z "${TRAVIS}"] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true # Some of these tools can come from places other than brew, so check before installing command -v ccache >/dev/null 2>&1 || brew install ccache diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 36fa79299..f9191eb89 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -16,7 +16,7 @@ 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.14" \ -D CMAKE_BUILD_TYPE=RELEASE \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ -D BUILD_OPENMW=TRUE \ diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 762ae5329..1f35ccebc 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -28,6 +28,8 @@ declare -rA GROUPED_DEPS=( # These dependencies can alternatively be built and linked statically. [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev" + + [coverity]="curl" # Pre-requisites for building MyGUI and OSG for static linking. # diff --git a/CMakeLists.txt b/CMakeLists.txt index 144485632..e322497a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -612,8 +612,8 @@ if (WIN32) # 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 + 4061 4263 4264 4266 4350 4371 4435 4514 4548 4571 4582 4583 4610 4619 4623 4625 + 4626 4628 4640 4668 4710 4711 4768 4820 4826 4917 4946 5032 5039 5045 5219 5220 # Warnings that are thrown on standard libraries and not OpenMW 4347 # Non-template function with same name and parameter count as template function @@ -635,7 +635,6 @@ if (WIN32) 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 @@ -671,6 +670,12 @@ if (WIN32) ) endif() + 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() + foreach(d ${WARNINGS_DISABLE}) set(WARNINGS "${WARNINGS} /wd${d}") endforeach(d) diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 3afbd777f..8e8cf8918 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -20,6 +20,7 @@ struct Arguments std::string mode; std::string filename; std::string extractfile; + std::string addfile; std::string outdir; bool longformat; @@ -36,6 +37,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 +100,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 +131,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]; @@ -138,6 +154,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) 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 add(std::unique_ptr& bsa, Arguments& info); int main(int argc, char** argv) { @@ -157,6 +174,12 @@ int main(int argc, char** argv) else bsa = std::make_unique(Bsa::BSAFile()); + if (info.mode == "create") + { + bsa->open(info.filename); + return 0; + } + bsa->open(info.filename); if (info.mode == "list") @@ -165,6 +188,8 @@ int main(int argc, char** argv) 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; @@ -188,13 +213,13 @@ int list(std::unique_ptr& bsa, Arguments& info) { // 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; @@ -253,7 +278,7 @@ 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) @@ -272,7 +297,7 @@ int extractAll(std::unique_ptr& bsa, Arguments& info) // Get a stream for the file to extract // (inefficient because getFile iter on the list again) - Files::IStreamPtr data = bsa->getFile(file.name); + Files::IStreamPtr data = bsa->getFile(file.name()); bfs::ofstream out(target, std::ios::binary); // Write the file to disk @@ -283,3 +308,11 @@ int extractAll(std::unique_ptr& bsa, Arguments& info) return 0; } + +int add(std::unique_ptr& bsa, Arguments& info) +{ + boost::filesystem::fstream stream(info.addfile, std::ios_base::binary | std::ios_base::out | std::ios_base::in); + bsa->addFile(info.addfile, stream); + + return 0; +} diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 7cea574c8..579f5f67d 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -322,7 +322,7 @@ int load(Arguments& info) std::string filename = info.filename; std::cout << "Loading file: " << filename << std::endl; - std::list skipped; + std::list skipped; try { diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp index ca81ebbbf..cb7947e40 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/importinventory.cpp b/apps/essimporter/importinventory.cpp index dbf9ce0bd..e91c39452 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -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/main.cpp b/apps/essimporter/main.cpp index d593669c3..9b969e35a 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -57,7 +57,7 @@ int main(int argc, char** argv) else { const std::string& ext = ".omwsave"; - if (boost::filesystem::exists(boost::filesystem::path(outputFile)) + if (bfs::exists(bfs::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 329d06a57..301823770 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,6 +32,7 @@ set(LAUNCHER_HEADER utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp + utils/openalutil.hpp ) # Headers that must be pre-processed @@ -47,6 +49,7 @@ set(LAUNCHER_HEADER_MOC utils/textinputdialog.hpp utils/profilescombobox.hpp utils/lineedit.hpp + utils/openalutil.hpp ) @@ -95,6 +98,7 @@ endif (WIN32) target_link_libraries(openmw-launcher ${SDL2_LIBRARY_ONLY} + ${OPENAL_LIBRARY} components ) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 81f451a27..b4b5f5dda 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,15 +1,20 @@ #include "advancedpage.hpp" +#include + #include #include #include #include #include +#include #include #include #include +#include "utils/openalutil.hpp" + Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, Settings::Manager &engineSettings, QWidget *parent) : QWidget(parent) @@ -19,7 +24,17 @@ Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, setObjectName ("AdvancedPage"); setupUi(this); + for(const char * name : Launcher::enumerateOpenALDevices()) + { + audioDeviceSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); + } + for(const char * name : Launcher::enumerateOpenALDevicesHrtf()) + { + hrtfProfileSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); + } + loadSettings(); + mCellNameCompleter.setModel(&mCellNameCompleterModel); startDefaultCharacterAtField->setCompleter(&mCellNameCompleter); } @@ -95,6 +110,7 @@ bool Launcher::AdvancedPage::loadSettings() int numPhysicsThreads = mEngineSettings.getInt("async num threads", "Physics"); if (numPhysicsThreads >= 0) physicsThreadsSpinBox->setValue(numPhysicsThreads); + loadSettingBool(allowNPCToFollowOverWaterSurfaceCheckBox, "allow actors to follow over water surface", "Game"); } // Visuals @@ -124,8 +140,43 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); viewingDistanceComboBox->setValue(convertToCells(mEngineSettings.getInt("viewing distance", "Camera"))); + + int lightingMethod = 1; + if (mEngineSettings.getString("lighting method", "Shaders") == "legacy") + lightingMethod = 0; + else if (mEngineSettings.getString("lighting method", "Shaders") == "shaders") + lightingMethod = 2; + lightingMethodComboBox->setCurrentIndex(lightingMethod); } + // Audio + { + std::string selectedAudioDevice = mEngineSettings.getString("device", "Sound"); + if (selectedAudioDevice.empty() == false) + { + int audioDeviceIndex = audioDeviceSelectorComboBox->findData(QString::fromStdString(selectedAudioDevice)); + if (audioDeviceIndex != -1) + { + audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); + } + } + int hrtfEnabledIndex = mEngineSettings.getInt("hrtf enable", "Sound"); + if (hrtfEnabledIndex >= -1 && hrtfEnabledIndex <= 1) + { + enableHRTFComboBox->setCurrentIndex(hrtfEnabledIndex + 1); + } + std::string selectedHRTFProfile = mEngineSettings.getString("hrtf", "Sound"); + if (selectedHRTFProfile.empty() == false) + { + int hrtfProfileIndex = hrtfProfileSelectorComboBox->findData(QString::fromStdString(selectedHRTFProfile)); + if (hrtfProfileIndex != -1) + { + hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); + } + } + } + + // Camera { loadSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); @@ -152,6 +203,7 @@ bool Launcher::AdvancedPage::loadSettings() showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + scalingSpinBox->setValue(mEngineSettings.getFloat("scaling factor", "GUI")); } // Bug fixes @@ -266,6 +318,36 @@ void Launcher::AdvancedPage::saveSettings() { mEngineSettings.setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); } + + static std::array lightingMethodMap = {"legacy", "shaders compatibility", "shaders"}; + mEngineSettings.setString("lighting method", "Shaders", lightingMethodMap[lightingMethodComboBox->currentIndex()]); + } + + // Audio + { + int audioDeviceIndex = audioDeviceSelectorComboBox->currentIndex(); + if (audioDeviceIndex != 0) + { + mEngineSettings.setString("device", "Sound", audioDeviceSelectorComboBox->currentText().toUtf8().constData()); + } + else + { + mEngineSettings.setString("device", "Sound", ""); + } + int hrtfEnabledIndex = enableHRTFComboBox->currentIndex() - 1; + if (hrtfEnabledIndex != mEngineSettings.getInt("hrtf enable", "Sound")) + { + mEngineSettings.setInt("hrtf enable", "Sound", hrtfEnabledIndex); + } + int selectedHRTFProfileIndex = hrtfProfileSelectorComboBox->currentIndex(); + if (selectedHRTFProfileIndex != 0) + { + mEngineSettings.setString("hrtf", "Sound", hrtfProfileSelectorComboBox->currentText().toUtf8().constData()); + } + else + { + mEngineSettings.setString("hrtf", "Sound", ""); + } } // Camera @@ -299,6 +381,9 @@ void Launcher::AdvancedPage::saveSettings() mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + float uiScalingFactor = scalingSpinBox->value(); + if (uiScalingFactor != mEngineSettings.getFloat("scaling factor", "GUI")) + mEngineSettings.setFloat("scaling factor", "GUI", uiScalingFactor); } // Bug fixes diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 01205043e..c6e74573c 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -20,6 +20,9 @@ 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 @@ -298,9 +301,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 + ")"); diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp new file mode 100644 index 000000000..52ad20894 --- /dev/null +++ b/apps/launcher/utils/openalutil.cpp @@ -0,0 +1,61 @@ +#include +#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 000000000..4a84fbae7 --- /dev/null +++ b/apps/launcher/utils/openalutil.hpp @@ -0,0 +1,7 @@ +#include + +namespace Launcher +{ + std::vector enumerateOpenALDevices(); + std::vector enumerateOpenALDevicesHrtf(); +} \ No newline at end of file diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 65575580a..b73cd37b8 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -116,7 +116,7 @@ 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 diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index 1c065f7da..e0cd5bb07 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -13,8 +13,6 @@ namespace CSMPrefs { - const int ShortcutSetting::MaxKeys; - ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 32b1ee33f..0958fa8d4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -421,6 +421,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) @@ -557,6 +567,24 @@ CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut (const std::string& 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_ = mSettings.getString (key, mCurrentCategory->second.getKey()); + + CSMPrefs::StringSetting *setting = + new CSMPrefs::StringSetting (&mCurrentCategory->second, &mSettings, &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_) { diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index a32583dc5..aa63de595 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -16,6 +16,7 @@ #include "category.hpp" #include "setting.hpp" #include "enumsetting.hpp" +#include "stringsetting.hpp" #include "shortcutmanager.hpp" class QColor; @@ -78,6 +79,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 000000000..27290b6c7 --- /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, Settings::Manager *values, + QMutex *mutex, const std::string& key, const std::string& label, std::string default_) +: Setting (parent, values, 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(getValues().getString(getKey(), getParent()->getKey()))); + } +} + +void CSMPrefs::StringSetting::textChanged (const QString& text) +{ + { + QMutexLocker lock (getMutex()); + getValues().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 000000000..36d020f29 --- /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, Settings::Manager *values, + 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/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 34485a46d..5ec7401dc 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -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)); diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 70c496e3f..4ccd2a06d 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -83,6 +83,7 @@ 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"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); @@ -985,20 +986,6 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, nullptr, &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; - } - } - return mReader->getRecordCount(); } diff --git a/apps/opencs/model/world/idtree.cpp b/apps/opencs/model/world/idtree.cpp index a8dfacb01..1e3398bbb 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/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index d944adc23..644092f16 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -56,7 +56,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 +587,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) @@ -1454,7 +1462,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 0a29afcad..95d1a09a2 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -178,7 +178,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 +251,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 @@ -405,7 +414,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 +487,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 +566,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) @@ -2054,7 +2081,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 diff --git a/apps/opencs/view/render/cameracontroller.cpp b/apps/opencs/view/render/cameracontroller.cpp index 5dbb7a28c..f21224d73 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/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index ac260fe83..4d6155123 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -151,7 +151,7 @@ void CSVRender::CellArrow::buildShape() osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.11, 0.6f, 0.95f, 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.08f, 0.44f, 0.7f, 1.0f)); diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index deeab4996..818be8b22 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/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 4babb5881..97f4c6ce2 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -121,7 +121,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); } diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index d7d9dba0c..922776e9f 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(); diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index f4a3f461c..fa46cf673 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -606,7 +606,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; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 82a1459f2..6eb437daf 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -163,7 +163,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) { diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 3de3e44b9..416a5eec0 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -43,26 +43,6 @@ namespace Misc class CallbackManager; } -namespace MWScript -{ - class ScriptManager; -} - -namespace MWSound -{ - class SoundManager; -} - -namespace MWWorld -{ - class World; -} - -namespace MWGui -{ - class WindowManager; -} - namespace Files { struct ConfigurationManager; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index de6451cd8..c4bea24f3 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -32,7 +32,6 @@ namespace MyGUI namespace ESM { - struct Class; class ESMReader; class ESMWriter; struct CellId; @@ -173,6 +172,8 @@ namespace MWBase virtual void setDragDrop(bool dragDrop) = 0; virtual bool getWorldMouseOver() = 0; + virtual float getScalingFactor() = 0; + virtual bool toggleFogOfWar() = 0; virtual bool toggleFullHelp() = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 6c3ee4580..16efa7def 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -288,13 +288,13 @@ 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, float x, float y, float z, 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; ///< @return an updated Ptr - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec) = 0; + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec, bool moveToActive) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; @@ -456,6 +456,8 @@ namespace MWBase virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0; virtual void disableDeferredPreviewRotation() = 0; + virtual void saveLoaded() = 0; + virtual void setupPlayer() = 0; virtual void renderPlayer() = 0; @@ -618,7 +620,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; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 28305c394..de560608c 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -46,11 +46,6 @@ namespace MWClass mStore.readState(inventory); } - MWWorld::CustomData *ContainerCustomData::clone() const - { - return new ContainerCustomData (*this); - } - ContainerCustomData& ContainerCustomData::asContainerCustomData() { return *this; @@ -72,7 +67,7 @@ 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()); } @@ -317,7 +312,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 2dc0c06ca..1c8937006 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -13,15 +13,13 @@ 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; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index ab3d1ef29..692ca72bb 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -52,14 +52,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 { @@ -69,16 +71,13 @@ namespace MWClass { return *this; } - - CreatureCustomData() : mContainerStore(nullptr) {} - virtual ~CreatureCustomData() { delete mContainerStore; } }; - MWWorld::CustomData *CreatureCustomData::clone() const + CreatureCustomData::CreatureCustomData(const CreatureCustomData& other) + : mCreatureStats(other.mCreatureStats), + mContainerStore(other.mContainerStore->clone()), + mMovement(other.mMovement) { - CreatureCustomData* cloned = new CreatureCustomData (*this); - cloned->mContainerStore = mContainerStore->clone(); - return cloned; } const Creature::GMST& Creature::getGmst() @@ -149,16 +148,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); // store - ptr.getRefData().setCustomData(data.release()); + ptr.getRefData().setCustomData(std::move(data)); getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); @@ -772,11 +771,11 @@ namespace MWClass std::unique_ptr data (new CreatureCustomData); if (hasInventoryStore(ptr)) - data->mContainerStore = new MWWorld::InventoryStore(); + data->mContainerStore = std::make_unique(); else - data->mContainerStore = new MWWorld::ContainerStore(); + data->mContainerStore = std::make_unique(); - ptr.getRefData().setCustomData (data.release()); + ptr.getRefData().setCustomData (std::move(data)); } } else diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index e3e52901e..f86004c61 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -10,15 +10,13 @@ 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,11 +27,6 @@ namespace MWClass } }; - MWWorld::CustomData *CreatureLevListCustomData::clone() const - { - return new CreatureLevListCustomData (*this); - } - std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; @@ -138,11 +131,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/door.cpp b/apps/openmw/mwclass/door.cpp index 25f7fc456..3a5ff0d9a 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -31,13 +31,11 @@ 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,11 +46,6 @@ namespace MWClass } }; - MWWorld::CustomData *DoorCustomData::clone() const - { - return new DoorCustomData (*this); - } - void Door::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) @@ -327,8 +320,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/npc.cpp b/apps/openmw/mwclass/npc.cpp index 0f2e756b7..9d2c45b10 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -247,15 +247,13 @@ namespace namespace MWClass { - 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; @@ -266,11 +264,6 @@ namespace MWClass } }; - MWWorld::CustomData *NpcCustomData::clone() const - { - return new NpcCustomData (*this); - } - const Npc::GMST& Npc::getGmst() { static GMST gmst; @@ -398,7 +391,7 @@ namespace MWClass data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData (data.release()); + ptr.getRefData().setCustomData(std::move(data)); getInventoryStore(ptr).autoEquip(ptr); } @@ -1331,8 +1324,7 @@ namespace MWClass 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()); + ptr.getRefData().setCustomData(std::make_unique()); } } else diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp index 28e450e2b..fa7de97d2 100644 --- a/apps/openmw/mwdialogue/hypertextparser.cpp +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -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); diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index c70783f39..fba136f88 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1,5 +1,7 @@ #include "bookpage.hpp" +#include + #include "MyGUI_RenderItem.h" #include "MyGUI_RenderManager.h" #include "MyGUI_TextureUtility.h" @@ -894,6 +896,27 @@ 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); +#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) + // work around inconsistency in MyGUI where the mouse press coordinates aren't + // transformed by the current Layer (even though mouse *move* events are). + if(!move) + pos = mNode->getLayer()->getPosition(left, top); +#endif + pos.left -= mCroppedParent->getAbsoluteLeft (); + pos.top -= mCroppedParent->getAbsoluteTop (); + pos.top += mViewTop; + return pos; + } + public: typedef TypesetBookImpl::StyleImpl Style; @@ -952,16 +975,10 @@ public: 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) { @@ -991,24 +1008,11 @@ public: void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) { - if (!mBook) - return; + auto pos = getAdjustedPos(left, top); - if (mPage >= mBook->mPages.size()) - return; - - // 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 +1023,11 @@ public: void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) { - if (!mBook) - return; - - if (mPage >= mBook->mPages.size()) - return; - - // 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 (); + auto pos = getAdjustedPos(left, top); - 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; diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index feda123fb..85c0dddc6 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; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index da48197c4..eb2b36c8a 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('@'); diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 02401f2e1..ac6303e20 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; diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 9854e5446..6eb3c01a5 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -71,13 +71,8 @@ namespace MWGui , mLastYSize(0) , mPreview(new MWRender::InventoryPreview(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())); mPreview->rebuild(); @@ -473,10 +468,11 @@ namespace MWGui 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)); + float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mPreview->setViewport(int(width*scalingFactor), int(height*scalingFactor)); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, - width*mScaleFactor/float(mPreview->getTextureWidth()), height*mScaleFactor/float(mPreview->getTextureHeight()))); + width*scalingFactor/float(mPreview->getTextureWidth()), height*scalingFactor/float(mPreview->getTextureHeight()))); } void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender) @@ -641,8 +637,9 @@ namespace MWGui y = (mAvatarImage->getHeight()-1) - y; // Scale coordinates - x = int(x*mScaleFactor); - y = int(y*mScaleFactor); + float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + x = static_cast(x*scalingFactor); + y = static_cast(y*scalingFactor); int slot = mPreview->getSlotSelected (x, y); diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index bd986744f..e3cebc849 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(); diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 426c3b437..6b38cd0d9 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -129,7 +129,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; } diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index 0299c2a1a..170c1dbce 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -7,11 +7,6 @@ #include -namespace MWGui -{ - class WindowManager; -} - namespace MWRender { class RaceSelectionPreview; diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index bd17c7afb..cb847536d 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.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/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index e5b593030..1d2088108 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -11,12 +11,16 @@ #include #include +#include #include #include #include #include #include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -69,6 +73,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 @@ -111,11 +118,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()) @@ -129,7 +149,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) { @@ -149,6 +170,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)); @@ -163,12 +190,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); } } @@ -199,7 +227,7 @@ namespace MWGui getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); - configureWidgets(mMainWidget); + configureWidgets(mMainWidget, true); setTitle("#{sOptions}"); @@ -216,6 +244,9 @@ namespace MWGui getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mLightingMethodButton, "LightingMethodButton"); + getWidget(mLightsResetButton, "LightsResetButton"); + getWidget(mMaxLights, "MaxLights"); if (MWBase::Environment::get().getVrMode()) { @@ -247,6 +278,10 @@ namespace MWGui mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); + mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); + mMaxLights->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onMaxLightsChanged); + mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); @@ -273,8 +308,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); @@ -309,6 +346,8 @@ namespace MWGui waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); + updateMaxLightsComboBox(mMaxLights); + mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); mKeyboardSwitch->setStateSelected(true); @@ -409,6 +448,54 @@ namespace MWGui apply(); } + void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + std::string message = "This change requires a restart to take effect."; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, {"#{sOK}"}, true); + + Settings::Manager::setString("lighting method", "Shaders", _sender->getItemNameAt(pos)); + 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}"}; + std::string message = "Resets to default values, would you like to continue? Changes to lighting method will require a restart."; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, 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}]); + + mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(Settings::Manager::mDefaultSettings[{"Shaders", "lighting method"}])); + updateMaxLightsComboBox(mMaxLights); + + apply(); + configureWidgets(mMainWidget, false); + } + void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); @@ -511,6 +598,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)); } @@ -609,6 +702,30 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::updateLightSettings() + { + auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); + std::string lightingMethodStr = SceneUtil::LightManager::getLightingMethodString(lightingMethod); + + mLightingMethodButton->removeAllItems(); + + std::array methods = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + + for (const auto& method : methods) + { + if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) + continue; + + mLightingMethodButton->addItem(SceneUtil::LightManager::getLightingMethodString(method)); + } + + mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(lightingMethodStr)); + } + void SettingsWindow::layoutControlsBox() { const int h = 18; @@ -671,6 +788,7 @@ namespace MWGui { highlightCurrentResolution(); updateControlsBox(); + updateLightSettings(); resetScrollbars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index b4fd124ce..6f405b5e9 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -3,11 +3,6 @@ #include "windowbase.hpp" -namespace MWGui -{ - class WindowManager; -} - namespace MWGui { class SettingsWindow : public WindowBase @@ -19,6 +14,8 @@ namespace MWGui void updateControlsBox(); + void updateLightSettings(); + void onResChange(int, int) override { center(); } protected: @@ -39,6 +36,10 @@ namespace MWGui MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::ComboBox* mMaxLights; + MyGUI::ComboBox* mLightingMethodButton; + MyGUI::Button* mLightsResetButton; + // controls MyGUI::ScrollView* mControlsBox; MyGUI::Button* mResetControlsButton; @@ -62,6 +63,10 @@ namespace MWGui void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(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 onRebindAction(MyGUI::Widget* _sender); void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); void onResetDefaultBindings(MyGUI::Widget* _sender); @@ -73,7 +78,7 @@ namespace MWGui 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(); diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index 622548c95..f46c43796 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 f9de469e2..5a5dec60f 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -393,7 +393,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 +402,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 diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 068975211..7ff0c866d 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -97,7 +97,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")); diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index 7098a2c2b..08fa5d86e 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 84d9d032d..4d365eb44 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 a07da1682..c38094ae4 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 8367b5a8b..b8456f376 100644 --- a/apps/openmw/mwgui/timeadvancer.hpp +++ b/apps/openmw/mwgui/timeadvancer.hpp @@ -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/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 962d17161..00b7db730 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 diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 11225fbfb..9427621b9 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,7 +10,6 @@ namespace MWWorld namespace MWGui { - class WindowManager; class DragAndDrop; class WindowBase: public Layout diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 5e8d7e761..52a1ce686 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1,5 +1,6 @@ #include "windowmanagerimp.hpp" +#include #include #include #include @@ -199,8 +200,8 @@ namespace MWGui , mVersionDescription(versionDescription) , mWindowVisible(true) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), uiScale); + mScalingFactor = std::clamp(Settings::Manager::getFloat("scaling factor", "GUI"), 0.5f, 8.f); + mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), mScalingFactor); mGuiPlatform->initialise(resourcePath, (boost::filesystem::path(logpath) / "MyGUI.log").generic_string()); @@ -216,7 +217,7 @@ namespace MWGui MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath)); + mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath, mScalingFactor)); mFontLoader->loadBitmapFonts(exportFonts); //Register own widgets with MyGUI @@ -1372,6 +1373,11 @@ namespace MWGui return mHud->getWorldMouseOver(); } + float WindowManager::getScalingFactor() + { + return mScalingFactor; + } + void WindowManager::executeInConsole (const std::string& path) { mConsole->executeFile (path); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 0d637e6d4..97ae50515 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -120,7 +120,6 @@ namespace MWGui class TrainingWindow; class SpellIcons; class MerchantRepair; - class Repair; class SoulgemDialog; class Recharge; class CompanionWindow; @@ -216,6 +215,8 @@ namespace MWGui void setDragDrop(bool dragDrop) override; bool getWorldMouseOver() override; + float getScalingFactor() override; + bool toggleFogOfWar() override; bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; @@ -532,6 +533,8 @@ namespace MWGui SDLUtil::VideoWrapper* mVideoWrapper; + float mScalingFactor; + /** * 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: diff --git a/apps/openmw/mwgui/windowpinnablebase.hpp b/apps/openmw/mwgui/windowpinnablebase.hpp index a94212819..c91f0a148 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/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 48091541c..03d492c9c 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -32,7 +32,6 @@ namespace MWInput , mMouseManager(mouseManager) , mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input")) , mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input")) - , mInvUiScalingFactor(1.f) , mSneakToggleShortcutTimer(0.f) , mGamepadZoom(0) , mGamepadGuiCursorEnabled(true) @@ -69,10 +68,6 @@ 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); mBindingsManager->setJoystickDeadZone(deadZoneRadius); @@ -102,8 +97,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; + float yMove = yAxis * dt * 1500.0f / uiScale; + float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) { diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index 871f11102..d8c62d57c 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -52,7 +52,6 @@ namespace MWInput bool mJoystickEnabled; float mGamepadCursorSpeed; - float mInvUiScalingFactor; float mSneakToggleShortcutTimer; float mGamepadZoom; bool mGamepadGuiCursorEnabled; diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index c94030279..4843da5f1 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -29,22 +29,18 @@ 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) { - 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) @@ -82,8 +78,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); @@ -254,17 +251,14 @@ namespace MWInput 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)); } void MouseManager::setMousePosition(int x, int y) { - mGuiCursorX = x * mInvUiScalingFactor; - mGuiCursorY = y * mInvUiScalingFactor; - } - - void MouseManager::setGUIScale(float scale) - { - mInvUiScalingFactor = 1.f / scale; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = x / uiScale; + mGuiCursorY = y / uiScale; } } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 79f9ab545..a1170e313 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -40,7 +40,6 @@ namespace MWInput // Used to override mouse position when using controllers not through SDL, such as OpenXR. void setMousePosition(int x, int y); - void setGUIScale(float scale); private: bool mInvertX; @@ -51,7 +50,6 @@ namespace MWInput BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; - float mInvUiScalingFactor; float mGuiCursorX; float mGuiCursorY; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 72d5d96a2..e9467a679 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -432,7 +432,8 @@ namespace MWMechanics } void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance) + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, + bool inCombatOrPursue) { if (!actor.getRefData().getBaseNode()) return; @@ -453,7 +454,7 @@ namespace MWMechanics const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); - if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance)) + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) return; // stop tracking when target is behind the actor @@ -461,7 +462,7 @@ namespace MWMechanics osg::Vec3f targetDirection(actor2Pos - actor1Pos); actorDirection.z() = 0; targetDirection.z() = 0; - if (actorDirection * targetDirection > 0 + if ((actorDirection * targetDirection > 0 || inCombatOrPursue) && 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)) { @@ -2012,28 +2013,25 @@ namespace MWMechanics MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); bool firstPersonPlayer = isPlayer && world->isFirstPerson(); bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); + MWWorld::Ptr activePackageTarget; // 1. Unconsious actor can not track target - // 2. Actors in combat and pursue mode do not bother to headtrack + // 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 && !inCombatOrPursue) + if (!stats.getKnockedDown() && !firstPersonPlayer) { - for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + if (inCombatOrPursue) + activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); + + 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; + if (inCombatOrPursue && it->first != activePackageTarget) + continue; + + updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 59814bcc2..2de0728d5 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -131,7 +131,8 @@ namespace MWMechanics 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); + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, + bool inCombatOrPursue); void rest(double hours, bool sleep); ///< Update actors while the player is waiting or sleeping. diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index e27c9de49..04cbb8e9f 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -23,4 +23,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 1e993f560..a226fc9cb 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -31,6 +31,7 @@ namespace MWMechanics MWWorld::Ptr getPlayer(); bool isPlayerInCombat(); bool canActorMoveByZAxis(const MWWorld::Ptr& actor); + bool hasWaterWalking(const MWWorld::Ptr& actor); template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) diff --git a/apps/openmw/mwmechanics/aiavoiddoor.cpp b/apps/openmw/mwmechanics/aiavoiddoor.cpp index 73a638563..6a59ae2bf 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; diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 2740355b5..94e4ecd95 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/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 51fcb92c1..b5e7594f3 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -228,7 +228,6 @@ namespace MWMechanics 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); @@ -236,13 +235,14 @@ namespace MWMechanics if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed - vAimDir = AimDirToMovingTarget(actor, target, storage.mLastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); + 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 } @@ -263,8 +263,9 @@ namespace MWMechanics && ((!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 halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); const auto pathGridGraph = getPathGridGraph(actor.getCell()); @@ -274,11 +275,7 @@ namespace MWMechanics { // 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 MWBase::World* world = MWBase::Environment::get().getWorld(); - const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigator = world->getNavigator(); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); const auto hit = navigator->raycast(halfExtents, vActorPos, vTargetPos, navigatorFlags); if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) @@ -534,7 +531,7 @@ 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; + moveDuration = 0.2f; else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); if (moveDuration > 0) @@ -698,7 +695,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; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 214aad320..204cf2f3f 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -25,6 +25,14 @@ #include +namespace +{ + float divOrMax(float dividend, float divisor) + { + return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; + } +} + MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : mTypeId(typeId), mOptions(options), @@ -411,13 +419,20 @@ 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 (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) @@ -433,15 +448,15 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P const MWWorld::Class& actorClass = actor.getClass(); if (flags & DetourNavigator::Flag_swim) - costs.mWater = costs.mWater / actorClass.getSwimSpeed(actor); + costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); if (flags & DetourNavigator::Flag_walk) { float walkCost; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = 1.0 / actorClass.getWalkSpeed(actor); + walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); else - walkCost = 1.0 / actorClass.getRunSpeed(actor); + walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); costs.mDoor = costs.mDoor * walkCost; costs.mPathgrid = costs.mPathgrid * walkCost; costs.mGround = costs.mGround * walkCost; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 675654b5a..526eade6c 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -217,38 +217,48 @@ void CharacterController::refreshHitRecoilAnims(CharacterState& idle) if(mHitState == CharState_None) { if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 - || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) - && mAnimation->hasAnimation("knockout")) + || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0)) { mTimeUntilWake = Misc::Rng::rollClosedProbability() * 2 + 1; // Wake up after 1 to 3 seconds if (isSwimming && mAnimation->hasAnimation("swimknockout")) { mHitState = CharState_SwimKnockOut; mCurrentHit = "swimknockout"; + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); } - else + else if (!isSwimming && mAnimation->hasAnimation("knockout")) { mHitState = CharState_KnockOut; mCurrentHit = "knockout"; + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); + } + else + { + // Knockout animations are missing. Fall back to idle animation, so target actor still can be killed via HtH. + mCurrentHit.erase(); } - 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")) + else if (knockdown) { if (isSwimming && mAnimation->hasAnimation("swimknockdown")) { mHitState = CharState_SwimKnockDown; mCurrentHit = "swimknockdown"; + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } - else + else if (!isSwimming && mAnimation->hasAnimation("knockdown")) { mHitState = CharState_KnockDown; mCurrentHit = "knockdown"; + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); + } + else + { + // Knockdown animation is missing. Cancel knockdown state. + mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); } - - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } else if (recovery) { @@ -2968,7 +2978,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(); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index fb6721853..f1e40ef7f 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -681,7 +681,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 diff --git a/apps/openmw/mwmechanics/spelllist.cpp b/apps/openmw/mwmechanics/spelllist.cpp index 891b28619..d8fbcf25a 100644 --- a/apps/openmw/mwmechanics/spelllist.cpp +++ b/apps/openmw/mwmechanics/spelllist.cpp @@ -153,23 +153,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 b01722fe8..c95ee812b 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -61,6 +61,8 @@ namespace MWMechanics void removeListener(Spells* spells); + void updateListener(Spells* before, Spells* after); + const std::vector getSpells() const; }; } diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 0af74e01b..b87137600 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -32,6 +32,15 @@ namespace MWMechanics mSpellList->addListener(this); } + Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), + mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), + mSourcedEffects(std::move(spells.mSourcedEffects)) + { + if (mSpellList) + mSpellList->updateListener(&spells, this); + } + std::map::const_iterator Spells::begin() const { return mSpells.begin(); diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 3df89a537..055339795 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -59,7 +59,7 @@ namespace MWMechanics Spells(const Spells&); - Spells(const Spells&&) = delete; + Spells(Spells&& spells); ~Spells(); diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 905034cde..f14805c6f 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -121,6 +121,7 @@ void Actor::updatePosition() mPreviousPosition = mWorldPosition; mPosition = mWorldPosition; mSimulationPosition = mWorldPosition; + mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; mSkipSimulation = true; } @@ -179,9 +180,10 @@ bool Actor::setPosition(const osg::Vec3f& position) if (mSkipSimulation) return false; bool hasChanged = mPosition != position || mPositionOffset.length() != 0 || mWorldPositionChanged; - mPreviousPosition = mPosition + mPositionOffset; - mPosition = position + mPositionOffset; - mPositionOffset = osg::Vec3f(); + updateWorldPosition(); + applyOffsetChange(); + mPreviousPosition = mPosition; + mPosition = position; return hasChanged; } @@ -195,9 +197,9 @@ void Actor::applyOffsetChange() { if (mPositionOffset.length() == 0) return; - mWorldPosition += mPositionOffset; mPosition += mPositionOffset; mPreviousPosition += mPositionOffset; + mSimulationPosition += mPositionOffset; mPositionOffset = osg::Vec3f(); mWorldPositionChanged = true; } diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index d552ed49e..eaeb308d5 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -17,10 +17,10 @@ namespace MWPhysics // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static constexpr int sMaxIterations = 8; // Allows for more precise movement solving without getting stuck or snagging too easily. - static constexpr float sCollisionMargin = 0.1; + static constexpr float sCollisionMargin = 0.1f; // 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.0; + static constexpr float sAllowedPenetration = 0.0f; } #endif diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 7469be029..38c13166c 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -593,8 +593,7 @@ namespace MWPhysics const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); // use a 3d approximation of the movement vector to better judge player intent - const ESM::Position& refpos = ptr.getRefData().getPosition(); - auto velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + auto velocity = (osg::Quat(actor.mRefpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRefpos.rot[2], 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 (!physicActor->getOnGround() || physicActor->getOnSlope()) velocity += physicActor->getInertialForce(); diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 11eb7f909..4957ef422 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -9,6 +9,7 @@ #include "components/settings/settings.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" @@ -137,11 +138,12 @@ namespace namespace MWPhysics { - PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, std::shared_ptr collisionWorld) + PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, btCollisionWorld *collisionWorld, MWRender::DebugDrawer* debugDrawer) : mDefaultPhysicsDt(physicsDt) , mPhysicsDt(physicsDt) , mTimeAccum(0.f) - , mCollisionWorld(std::move(collisionWorld)) + , mCollisionWorld(collisionWorld) + , mDebugDrawer(debugDrawer) , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) @@ -185,7 +187,7 @@ namespace MWPhysics if (data.mActor.lock()) { std::unique_lock lock(mCollisionWorldMutex); - MovementSolver::unstuck(data, mCollisionWorld.get()); + MovementSolver::unstuck(data, mCollisionWorld); } }); @@ -315,7 +317,7 @@ namespace MWPhysics // init for (auto& data : actorsData) - data.updatePosition(); + data.updatePosition(mCollisionWorld); mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; @@ -381,7 +383,7 @@ namespace MWPhysics void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { std::shared_lock lock(mCollisionWorldMutex); - ContactTestWrapper::contactTest(mCollisionWorld.get(), colObj, resultCallback); + ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) @@ -532,7 +534,7 @@ namespace MWPhysics if(const auto actor = mActorsFrameData[job].mActor.lock()) { MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); } } @@ -594,8 +596,8 @@ namespace MWPhysics { for (auto& actorData : mActorsFrameData) { - MovementSolver::unstuck(actorData, mCollisionWorld.get()); - MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + MovementSolver::unstuck(actorData, mCollisionWorld); + MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld, *mWorldFrameData); } updateActorsPositions(); @@ -626,4 +628,10 @@ namespace MWPhysics mTimeBegin = mTimer->tick(); mFrameNumber = frameNumber; } + + void PhysicsTaskScheduler::debugDraw() + { + std::shared_lock lock(mCollisionWorldMutex); + mDebugDrawer->step(); + } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 137755c21..6d2c392c0 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -20,12 +20,17 @@ 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 @@ -49,6 +54,7 @@ namespace MWPhysics void removeCollisionObject(btCollisionObject* collisionObject); void updateSingleAabb(std::weak_ptr ptr, bool immediate=false); bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); + void debugDraw(); private: void syncComputation(); @@ -67,7 +73,8 @@ namespace MWPhysics float mDefaultPhysicsDt; float mPhysicsDt; float mTimeAccum; - std::shared_ptr mCollisionWorld; + btCollisionWorld* mCollisionWorld; + MWRender::DebugDrawer* mDebugDrawer; std::vector mLOSCache; std::set, std::owner_less>> mUpdateAabb; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 65ed13f80..c5ba902fc 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -60,6 +60,22 @@ #include "movementsolver.hpp" #include "mtphysics.hpp" +namespace +{ + bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world) + { + 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); + MWPhysics::ActorTracer tracer; + tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); + } +} + namespace MWPhysics { PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode) @@ -79,7 +95,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. @@ -97,8 +113,8 @@ namespace MWPhysics } } - mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld); mDebugDrawer = std::make_unique(mParentNode, mCollisionWorld.get(), mDebugDrawEnabled); + mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld.get(), mDebugDrawer.get()); } PhysicsSystem::~PhysicsSystem() @@ -347,16 +363,7 @@ namespace MWPhysics 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); + return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const @@ -605,9 +612,6 @@ namespace MWPhysics return nullptr; }(); - if (caster == nullptr) - Log(Debug::Warning) << "No caster for projectile " << projectileId; - ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile); resultCallback.m_collisionFilterMask = 0xff; resultCallback.m_collisionFilterGroup = CollisionType_Projectile; @@ -772,16 +776,10 @@ namespace MWPhysics const MWMechanics::MagicEffects& effects = character.getClass().getCreatureStats(character).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()))) - waterCollision = true; - else if (physicActor->getCollisionMode() && canMoveToWaterSurface(character, waterlevel)) - { - moveToWaterSurface = true; + if (physicActor->getCollisionMode() || !world->isUnderwater(character.getCell(), osg::Vec3f(character.getRefData().getPosition().asVec3()))) waterCollision = true; - } } physicActor->setCanWaterWalk(waterCollision); @@ -794,7 +792,7 @@ namespace MWPhysics if (!willSimulate) standingOn = physicActor->getStandingOnPtr(); - actorsFrameData.emplace_back(std::move(physicActor), standingOn, moveToWaterSurface, movement, slowFall, waterlevel); + actorsFrameData.emplace_back(std::move(physicActor), standingOn, waterCollision, movement, slowFall, waterlevel); } mMovementQueue.clear(); return actorsFrameData; @@ -827,7 +825,7 @@ namespace MWPhysics void PhysicsSystem::debugDraw() { if (mDebugDrawEnabled) - mDebugDrawer->step(); + mTaskScheduler->debugDraw(); } bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const @@ -937,9 +935,9 @@ namespace MWPhysics } ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, - bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel) + bool waterCollision, osg::Vec3f movement, float slowFall, float waterlevel) : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), - mDidJump(false), mNeedLand(false), mMoveToWaterSurface(moveToWaterSurface), + mDidJump(false), mNeedLand(false), mWaterCollision(waterCollision), mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(movement), mPosition(), mRefpos() { const MWBase::World *world = MWBase::Environment::get().getWorld(); @@ -953,7 +951,7 @@ namespace MWPhysics mWasOnGround = actor->getOnGround(); } - void ActorFrameData::updatePosition() + void ActorFrameData::updatePosition(btCollisionWorld* world) { mActorRaw->updateWorldPosition(); // If physics runs "fast enough", position are interpolated without simulation @@ -961,10 +959,10 @@ namespace MWPhysics // regardless of simulation speed. mActorRaw->applyOffsetChange(); mPosition = mActorRaw->getPosition(); - if (mMoveToWaterSurface) + if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(mActorRaw, mWaterlevel, world)) { mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z()); + MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z(), false); } mOldHeight = mPosition.z(); mRefpos = mActorRaw->getPtr().getRefData().getPosition(); diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 354823986..ce10b4246 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -79,7 +79,7 @@ namespace MWPhysics struct ActorFrameData { ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel); - void updatePosition(); + void updatePosition(btCollisionWorld* world); std::weak_ptr mActor; Actor* mActorRaw; MWWorld::Ptr mStandingOn; @@ -90,7 +90,7 @@ namespace MWPhysics bool mDidJump; bool mFloatToSurface; bool mNeedLand; - bool mMoveToWaterSurface; + bool mWaterCollision; float mWaterlevel; float mSlowFall; float mOldHeight; @@ -259,7 +259,7 @@ namespace MWPhysics 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; diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 2a28381be..4f5a27e53 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -60,14 +60,14 @@ namespace MWPhysics // 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; + float downStepSize = 0; while(attempt < 3) { attempt++; if(attempt == 1) tracerDest = tracerPos + toMove; - else if (!firstIteration || !sDoExtraStairHacks) // first attempt failed and not on first movement solver iteration, can't retry -- or we have extra hacks disabled + else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled { return false; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index fcffe220b..845e917fb 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -358,6 +358,8 @@ void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) } mScabbard = attachMesh(scabbardName, boneName); + if (mScabbard) + resetControllers(mScabbard->getNode()); osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); if (!weaponNode) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index fa49a2e33..473721d86 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -500,6 +500,11 @@ namespace MWRender mAlpha = alpha; } + void setLightSource(const osg::ref_ptr& lightSource) + { + mLightSource = lightSource; + } + protected: void setDefaults(osg::StateSet* stateset) override { @@ -522,10 +527,13 @@ namespace MWRender { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); + if (mLightSource) + mLightSource->setActorFade(mAlpha); } private: float mAlpha; + osg::ref_ptr mLightSource; }; struct Animation::AnimSource @@ -1667,7 +1675,7 @@ 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_ParticleSystem, Mask_Lighting, exterior); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1817,6 +1825,7 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); + mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index fbc3b0cff..df9c2d5b4 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -278,6 +278,7 @@ protected: osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; + osg::ref_ptr mExtraLightSource; float mAlpha; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index a4fb597fd..1a954b8cc 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -453,7 +453,7 @@ namespace MWRender void Camera::setPitch(float angle) { const float epsilon = 0.000001f; - float limit = osg::PI_2 - epsilon; + float limit = static_cast(osg::PI_2) - epsilon; mPitch = osg::clampBetween(angle, -limit, limit); } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 2c9b28e78..b75e45906 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -174,7 +174,9 @@ namespace MWRender mCamera->setNodeMask(Mask_RenderToTexture); - osg::ref_ptr lightManager = new SceneUtil::LightManager; + 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->setMode(GL_LIGHTING, osg::StateAttribute::ON); @@ -231,12 +233,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); @@ -414,7 +426,7 @@ namespace MWRender visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); osg::Node::NodeMask nodeMask = mCamera->getNodeMask(); - mCamera->setNodeMask(~0); + mCamera->setNodeMask(~0u); mCamera->accept(visitor); mCamera->setNodeMask(nodeMask); diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 0baa85c52..fd2246253 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" @@ -271,6 +272,8 @@ namespace MWRender group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); group->getBound(); group->setNodeMask(Mask_Groundcover); + if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) + group->setCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", false, true); return group; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 64931aa88..ec2bf54a7 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -19,9 +19,13 @@ #include #include #include +#include #include +#include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -89,9 +93,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); @@ -220,6 +223,9 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + // override sun for local map + SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot.get()), light, stateset); + camera->addChild(lightSource); camera->setStateSet(stateset); camera->setViewport(0, 0, mMapResolution, mMapResolution); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 7386c0069..064d3aa35 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -402,7 +402,7 @@ namespace MWRender refs[ref.mRefNum] = ref; } } - catch (std::exception& e) + catch (std::exception&) { continue; } diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp index 65f53d530..c24cdf4f8 100644 --- a/apps/openmw/mwrender/objectpaging.hpp +++ b/apps/openmw/mwrender/objectpaging.hpp @@ -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/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index faf972158..483431823 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -54,6 +54,7 @@ #include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -205,14 +206,20 @@ namespace MWRender , mWorkQueue(workQueue) , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) + , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) { + 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"); + bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") + || Settings::Manager::getBool("force shaders", "Shaders") + || Settings::Manager::getBool("enable shadows", "Shadows") + || lightingMethod != SceneUtil::LightingMethod::FFP; //resourceSystem->getSceneManager()->setForceShaders(forceShaders); resourceSystem->getSceneManager()->setForceShaders(true); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped @@ -225,7 +232,13 @@ namespace MWRender 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); - osg::ref_ptr sceneRoot = new SceneUtil::LightManager; + // 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()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); + sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); @@ -247,6 +260,7 @@ namespace MWRender mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, 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++) @@ -258,9 +272,15 @@ namespace MWRender globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; globalDefines["useGPUShader4"] = "0"; + 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 = (Constants::CellSizeInUnits * std::max(1, Settings::Manager::getInt("distance", "Groundcover")) - 1024) * 0.93; 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)); // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); @@ -363,6 +383,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); @@ -536,7 +557,32 @@ namespace MWRender void RenderingManager::configureAmbient(const ESM::Cell *cell) { - setAmbientColour(SceneUtil::colourFromRGB(cell->mAmbi.mAmbient)); + bool needsAdjusting = false; + if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) + needsAdjusting = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); + + 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 + float targetBrightnessIncreaseFactor = mMinimumAmbientLuminance / relativeLuminance; + if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) + ambient = osg::Vec4(mMinimumAmbientLuminance, mMinimumAmbientLuminance, mMinimumAmbientLuminance, ambient.a()); + else + ambient *= targetBrightnessIncreaseFactor; + } + } + + setAmbientColour(ambient); osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); mSunLight->setDiffuse(diffuse); @@ -630,7 +676,7 @@ namespace MWRender } else if (mode == Render_Scene) { - int mask = mViewer->getCamera()->getCullMask(); + unsigned int mask = mViewer->getCamera()->getCullMask(); bool enabled = mask&Mask_Scene; enabled = !enabled; if (enabled) @@ -787,7 +833,7 @@ namespace MWRender return false; } - int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); + unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); @@ -832,7 +878,7 @@ namespace MWRender { RayResult result; result.mHit = false; - result.mHitRefnum.mContentFile = -1; + result.mHitRefnum.unset(); result.mRatio = 0; result.mHitNode = nullptr; if (intersector->containsIntersections()) @@ -893,7 +939,7 @@ namespace MWRender mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); mIntersectionVisitor->setIntersector(intersector); - int mask = ~0; + unsigned int mask = ~0u; mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); if (ignorePlayer) mask &= ~(Mask_Player); @@ -1154,9 +1200,47 @@ namespace MWRender 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 = static_cast(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); + + mSceneRoot->removeUpdateCallback(mStateUpdater); + mStateUpdater = new StateUpdater; + mSceneRoot->addUpdateCallback(mStateUpdater); + mStateUpdater->setFogEnd(mViewDistance); + updateAmbient(); + + mViewer->startThreading(); + } + } } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index ef1677185..7002f0175 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -306,6 +306,7 @@ namespace MWRender osg::ref_ptr mStateUpdater; osg::Vec4f mAmbientColor; + float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 5a6ec06e5..f0e876470 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -45,6 +45,9 @@ #include #include +#include +#include + #include #include "../mwbase/environment.hpp" @@ -314,8 +317,8 @@ public: if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) ++numPlanes; - int mask = 0x1; - int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + 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) @@ -438,7 +441,7 @@ private: class CelestialBody { public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0) + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) : mVisibleMask(visibleMask) { mGeom = createTexturedQuad(numUvSets); @@ -1164,7 +1167,7 @@ void SkyManager::create() { assert(!mCreated); - mAtmosphereDay = mSceneManager->getInstance("meshes/sky_atmosphere.nif", mEarlyRenderBinRoot); + mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(0); mAtmosphereDay->accept(modAtmosphere); @@ -1176,10 +1179,10 @@ void SkyManager::create() 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); + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); else - atmosphereNight = mSceneManager->getInstance("meshes/sky_night_01.nif", mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); ModVertexAlphaVisitor modStars(2); atmosphereNight->accept(modStars); @@ -1193,14 +1196,14 @@ void SkyManager::create() mCloudNode = new osg::PositionAttitudeTransform; mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance("meshes/sky_clouds_01.nif", mCloudNode); + mCloudMesh = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), 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 = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); mCloudMesh2->accept(modClouds); mCloudUpdater2 = new CloudUpdater; mCloudUpdater2->setOpacity(0.f); @@ -1597,7 +1600,7 @@ void SkyManager::update(float duration) if (mParticleNode) { // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == "meshes\\blizzard.nif") + if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); mParticleNode->setAttitude(quat); } @@ -1621,7 +1624,7 @@ void SkyManager::setEnabled(bool enabled) if (enabled && !mCreated) create(); - mRootNode->setNodeMask(enabled ? Mask_Sky : 0); + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); mEnabled = enabled; } @@ -1782,7 +1785,7 @@ void SkyManager::setWeather(const WeatherResult& weather) mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0 : 0); + mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); } if (mCloudColour != weather.mFogColor) @@ -1827,7 +1830,7 @@ void SkyManager::setWeather(const WeatherResult& weather) mAtmosphereNightUpdater->setFade(mStarsOpacity); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0 : 0); + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); mPrecipitationAlpha = weather.mPrecipitationAlpha; } @@ -1897,16 +1900,16 @@ void SkyManager::setWaterHeight(float height) 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"); + models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + models.emplace_back(Settings::Manager::getString("skynight02", "Models")); + models.emplace_back(Settings::Manager::getString("skynight01", "Models")); + models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); + + models.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")); textures.emplace_back("textures/tx_mooncircle_full_s.dds"); textures.emplace_back("textures/tx_mooncircle_full_m.dds"); diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index a1c951f25..1e0091057 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 diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index b96135f12..1fd64bb27 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -676,6 +677,10 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + + auto method = mResourceSystem->getSceneManager()->getLightingMethod(); + if (method == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); mRainIntensityUpdater = new RainIntensityUpdater(); node->setUpdateCallback(mRainIntensityUpdater); @@ -775,11 +780,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() diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 186940dd9..375242f17 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -93,7 +93,7 @@ namespace MWScript 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) diff --git a/apps/openmw/mwscript/globalscripts.cpp b/apps/openmw/mwscript/globalscripts.cpp index 1a7e3ebbc..0d579abdc 100644 --- a/apps/openmw/mwscript/globalscripts.cpp +++ b/apps/openmw/mwscript/globalscripts.cpp @@ -235,13 +235,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()) diff --git a/apps/openmw/mwscript/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index c5c5a9a45..049e78804 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -73,7 +73,7 @@ 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? diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index c1481d6d0..298454bcd 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; diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index e8b406977..ba211503e 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -32,7 +32,7 @@ namespace MWScript std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) - MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false); } template @@ -290,11 +290,11 @@ namespace MWScript float terrainHeight = -std::numeric_limits::max(); if (ptr.getCell()->isExterior()) terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(curPos); - + if (pos < terrainHeight) pos = terrainHeight; } - + newPos[2] = pos; } else @@ -303,7 +303,7 @@ namespace MWScript } dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true)); } }; @@ -439,7 +439,7 @@ namespace MWScript } else { - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true, true); } dynamic_cast(runtime.getContext()).updatePtr(base,ptr); @@ -726,7 +726,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false)); } }; @@ -762,7 +762,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false)); } }; diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 6b9de800f..934402cd4 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -141,7 +141,7 @@ namespace MWSound public: SoundManager(const VFS::Manager* vfs, bool useSound); - virtual ~SoundManager(); + ~SoundManager() override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; diff --git a/apps/openmw/mwstate/quicksavemanager.cpp b/apps/openmw/mwstate/quicksavemanager.cpp index df078e026..bf1781520 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 a5237d7c3..cdeff42c2 100644 --- a/apps/openmw/mwstate/quicksavemanager.hpp +++ b/apps/openmw/mwstate/quicksavemanager.hpp @@ -13,8 +13,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 f605344bf..fb418b94a 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -461,7 +461,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.intval, contentFileMap); break; case ESM::REC_GMAP: @@ -505,6 +505,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(); diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 31af5b24b..6bc60968c 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -84,7 +84,7 @@ namespace MWWorld mTerrain->cacheCell(mTerrainView.get(), mX, mY); mPreloadedObjects.insert(mLandManager->getLand(mX, mY)); } - catch(std::exception& e) + catch(std::exception&) { } } @@ -127,7 +127,7 @@ namespace MWWorld 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 diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index 15c1b46ba..ad4dfcfb9 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -132,9 +132,11 @@ void MWWorld::Cells::writeCell (ESM::ESMWriter& writer, CellStore& cell) const 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*)nullptr)), 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) { @@ -259,8 +261,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); diff --git a/apps/openmw/mwworld/cells.hpp b/apps/openmw/mwworld/cells.hpp index 90ede409b..654d9a14b 100644 --- a/apps/openmw/mwworld/cells.hpp +++ b/apps/openmw/mwworld/cells.hpp @@ -28,11 +28,12 @@ namespace MWWorld /// \brief Cell container class Cells { + typedef std::vector > IdCache; const MWWorld::ESMStore& mStore; std::vector& mReader; mutable std::map mInteriors; mutable std::map, CellStore> mExteriors; - std::vector > mIdCache; + IdCache mIdCache; std::size_t mIdCacheIndex; Cells (const Cells&); diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 635485dde..cced17688 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -75,13 +75,13 @@ namespace MWWorld::ResolutionListener::~ResolutionListener() { - if(!mStore.mModified && mStore.mResolved && !mStore.mPtr.isEmpty()) + try { - for(const auto&& 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(); } } @@ -648,6 +648,21 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() 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) diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 882f5efc4..5044b0f45 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -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; @@ -250,6 +250,7 @@ namespace MWWorld void resolve(); ResolutionHandle resolveTemporarily(); + void unresolve(); friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 8af45e36a..7200e7684 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/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 90bc80b48..a69d74b05 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -48,6 +48,57 @@ namespace } } } + + std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) + { + // Cache first class from store - we will use it if current class is not found + std::string defaultCls; + auto it = classes.begin(); + if (it != classes.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 (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; + } + } + + std::string npcClass = npc.mClass; + if (!npcClass.empty()) + { + 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; + } } namespace MWWorld @@ -218,49 +269,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 +340,14 @@ void ESMStore::validate() } } +void ESMStore::validateDynamic() +{ + std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic); + + for (const ESM::NPC &npc : npcsToReplace) + mNpcs.insert(npc); +} + int ESMStore::countSavedGameRecords() const { return 1 // DYNA (dynamic name counter) @@ -384,12 +401,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: diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index d69c56d8c..26f497a52 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -80,8 +80,6 @@ namespace MWWorld std::map mStores; - ESM::NPC mPlayerTemplate; - unsigned int mDynamicCount; mutable std::map > mSpellListCache; @@ -172,16 +170,18 @@ 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); } + /// Validate entries in store after loading a save + void validateDynamic(); + void load(ESM::ESMReader &esm, Loading::Listener* listener); template diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 6809e63b2..32dc0d2e9 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -123,7 +123,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) diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 71ff6d040..f6a445d61 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -40,8 +40,6 @@ namespace MWWorld void RefData::cleanup() { mBaseNode = nullptr; - - delete mCustomData; mCustomData = nullptr; } @@ -223,21 +221,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 738a6d53a..8979c8505 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -5,8 +5,10 @@ #include #include "../mwscript/locals.hpp" +#include "../mwworld/customdata.hpp" #include +#include namespace SceneUtil { @@ -44,7 +46,7 @@ namespace MWWorld ESM::AnimationState mAnimationState; - CustomData *mCustomData; + std::unique_ptr mCustomData; void copy (const RefData& refData); @@ -68,6 +70,7 @@ namespace MWWorld /// perform these operations). RefData (const RefData& refData); + RefData (RefData&& other) noexcept = default; ~RefData(); @@ -76,6 +79,7 @@ namespace MWWorld /// perform this operations). RefData& operator= (const RefData& refData); + RefData& operator= (RefData&& other) noexcept = default; /// Return base node (can be a null pointer). SceneUtil::PositionAttitudeTransform* getBaseNode(); @@ -117,7 +121,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/scene.cpp b/apps/openmw/mwworld/scene.cpp index 3f050ba36..dae703c8c 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -965,7 +965,7 @@ namespace MWWorld { mSceneManager->getTemplate(mMesh); } - catch (std::exception& e) + catch (std::exception&) { } } @@ -1049,7 +1049,7 @@ namespace MWWorld exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } - catch (std::exception& e) + catch (std::exception&) { // ignore error for now, would spam the log too much } diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index a70d3ccdd..f87a0ca73 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -36,7 +36,6 @@ namespace Loading namespace DetourNavigator { struct Navigator; - class Water; } namespace MWRender diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 69ca1f8c2..9d6552106 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -250,9 +250,15 @@ 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); + if(overrideOnly) + { + auto it = mStatic.find(id); + if(it == mStatic.end()) + return nullptr; + } std::pair result = mDynamic.insert(std::pair(id, item)); T *ptr = &result.first->second; @@ -337,13 +343,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); } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index cb9f4f1e0..9cb1c7473 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -46,7 +46,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 }; @@ -192,7 +192,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 +201,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 <> diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index a68434cc4..f22a5af7d 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1280,7 +1280,7 @@ 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, float x, float y, float z, bool movePhysics, bool moveToActive) { int cellX, cellY; positionToIndex(x, y, cellX, cellY); @@ -1295,20 +1295,14 @@ namespace MWWorld return moveObject(ptr, cell, x, y, z, movePhysics); } - MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z, bool moveToActive) - { - return moveObjectImp(ptr, x, y, z, true, moveToActive); - } - - MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive) { auto* actor = mPhysics->getActor(ptr); + osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) - { actor->adjustPosition(vec); - return ptr; - } - osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; + if (ptr.getClass().isActor()) + return moveObject(ptr, newpos.x(), newpos.y(), newpos.z(), false, moveToActive && ptr != getPlayerPtr()); return moveObject(ptr, newpos.x(), newpos.y(), newpos.z()); } @@ -1575,7 +1569,7 @@ namespace MWWorld auto* physactor = mPhysics->getActor(actor); assert(physactor); const auto position = physactor->getSimulationPosition(); - moveObjectImp(actor, position.x(), position.y(), position.z(), false); + moveObject(actor, position.x(), position.y(), position.z(), false, false); } } @@ -1585,7 +1579,7 @@ namespace MWWorld auto* physactor = mPhysics->getActor(*player); assert(physactor); const auto position = physactor->getSimulationPosition(); - moveObjectImp(*player, position.x(), position.y(), position.z(), false); + moveObject(*player, position.x(), position.y(), position.z(), false, false); } } @@ -2515,6 +2509,11 @@ namespace MWWorld mRendering->getCamera()->adjustCameraDistance(dist); } + void World::saveLoaded() + { + mStore.validateDynamic(); + } + void World::setupPlayer() { const ESM::NPC *player = mStore.get().find("player"); @@ -3926,14 +3925,12 @@ namespace MWWorld return false; } - osg::Vec3f World::aimToTarget(const ConstPtr &actor, const ConstPtr &target) + osg::Vec3f World::aimToTarget(const ConstPtr &actor, const ConstPtr &target, bool isRangedCombat) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); - osg::Vec3f weaponHalfExtents = mPhysics->getHalfExtents(actor); - osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); - osg::Vec3f targetHalfExtents = mPhysics->getHalfExtents(target); - weaponPos.z() += weaponHalfExtents.z() * 2 * Constants::TorsoHeight; - targetPos.z() += targetHalfExtents.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); } @@ -3957,7 +3954,7 @@ namespace MWWorld if (!model.empty()) scene->preload(model, ref.getPtr().getClass().useAnim()); } - catch(std::exception& e) + catch(std::exception&) { } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 88abaca99..37be8174e 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -62,8 +62,6 @@ namespace ToUTF8 class Utf8Encoder; } -struct ContentLoader; - namespace MWPhysics { class Object; @@ -140,9 +138,6 @@ namespace MWWorld 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(); @@ -381,13 +376,13 @@ 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, float x, float y, float z, 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; ///< @return an updated Ptr - MWWorld::Ptr moveObjectBy(const Ptr& ptr, osg::Vec3f vec) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; @@ -554,6 +549,8 @@ namespace MWWorld void applyDeferredPreviewRotationToPlayer(float dt) override; void disableDeferredPreviewRotation() override; + void saveLoaded() override; + void setupPlayer() override; void renderPlayer() override; @@ -708,7 +705,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; diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index a3bb0c6f8..d78cb6955 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -13,6 +13,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) mwdialogue/test_keywordsearch.cpp esm/test_fixed_string.cpp + esm/variant.cpp misc/test_stringops.cpp misc/test_endianness.cpp diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index d377aed1e..3b7dc4915 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -835,4 +835,55 @@ namespace ASSERT_THAT(result, Optional(Vec3fEq(mEnd.x(), mEnd.y(), 1.87719))); } + + 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, + }}; + btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); + heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + + const btBoxShape oscillatingBoxShape(btVector3(20, 20, 20)); + const btVector3 oscillatingBoxShapePosition(32, 32, 400); + const btBoxShape boderBoxShape(btVector3(50, 50, 50)); + + mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition)); + // add this box to make navmesh bound box independent from oscillatingBoxShape rotations + mNavigator->addObject(ObjectId(&boderBoxShape), boderBoxShape, + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200))); + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + + const auto navMeshes = mNavigator->getNavMeshes(); + ASSERT_EQ(navMeshes.size(), 1); + { + const auto navMesh = navMeshes.begin()->second->lockConst(); + ASSERT_EQ(navMesh->getGeneration(), 1); + ASSERT_EQ(navMesh->getNavMeshRevision(), 4); + } + + for (int n = 0; n < 10; ++n) + { + const btTransform transform(btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10), + oscillatingBoxShapePosition); + mNavigator->updateObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, transform); + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + } + + ASSERT_EQ(navMeshes.size(), 1); + { + const auto navMesh = navMeshes.begin()->second->lockConst(); + ASSERT_EQ(navMesh->getGeneration(), 1); + ASSERT_EQ(navMesh->getNavMeshRevision(), 4); + } + } } diff --git a/apps/openmw_test_suite/esm/variant.cpp b/apps/openmw_test_suite/esm/variant.cpp new file mode 100644 index 000000000..10d35e486 --- /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; + ESM::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; + ESM::ESMReader reader; + reader.open(std::make_shared(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; + ESM::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/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index e00344af9..37234468b 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 c189ea171..57d551d27 100644 --- a/apps/wizard/methodselectionpage.hpp +++ b/apps/wizard/methodselectionpage.hpp @@ -17,6 +17,9 @@ namespace Wizard int nextId() const override; + private slots: + void handleBuyButton(); + private: MainWizard *mWizard; diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake index 8d7324242..502910a9d 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,224 +151,17 @@ 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 - INTERFACE_LINK_LIBRARIES "Threads::Threads" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GTEST_LIBRARY}" - ) - -if(GTEST_INCLUDE_DIR) - set_target_properties(GTest::GTest PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" - INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" - ) -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" @@ -477,13 +177,6 @@ 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() @@ -492,17 +185,6 @@ set_target_properties(GMock::Main PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") -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() -endif() - if(GMOCK_FOUND) set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR}) set(GMOCK_LIBRARIES GMock::GMock) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index ef7eb1298..d5bd9f80f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -87,7 +87,7 @@ add_component_dir (esmterrain ) add_component_dir (misc - gcd constants utf8stream stringops resourcehelpers rng messageformatparser weakcache stereo callbackmanager + constants utf8stream stringops resourcehelpers rng messageformatparser weakcache stereo callbackmanager thread ) add_component_dir (debug @@ -182,6 +182,8 @@ add_component_dir(detournavigator navigator findrandompointaroundcircle raycast + navmeshtileview + oscillatingrecastmeshobject ) set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui diff --git a/components/bsa/bsa_file.cpp b/components/bsa/bsa_file.cpp index f6220b7ce..ec455ea1f 100644 --- a/components/bsa/bsa_file.cpp +++ b/components/bsa/bsa_file.cpp @@ -27,6 +27,7 @@ #include #include +#include using namespace Bsa; @@ -37,6 +38,31 @@ 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 = (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() { @@ -113,14 +139,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 @@ -129,23 +158,80 @@ void BSAFile::readHeader() // Set up the the FileStruct table mFiles.resize(filenum); + size_t endOfNameBuffer = 0; for(size_t i=0;i= 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; + 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; } +/// Write header information to the output sink +void Bsa::BSAFile::writeHeader() +{ + namespace bfs = boost::filesystem; + bfs::fstream output(mFilename, std::ios::binary | std::ios::in | std::ios::out); + + uint32_t head[3]; + head[0] = 0x100; + auto fileDataOffset = mFiles.empty() ? 12 : mFiles.front().offset; + head[1] = fileDataOffset - 12 - 8*mFiles.size(); + + output.seekp(0, std::ios_base::end); + + head[2] = 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()); +} + /// Get the index of a given file name, or -1 if not found int BSAFile::getIndex(const char *str) const { @@ -162,7 +248,22 @@ int BSAFile::getIndex(const char *str) const void BSAFile::open(const std::string &file) { mFilename = file; - readHeader(); + if(boost::filesystem::exists(file)) + readHeader(); + else + { + { boost::filesystem::fstream(mFilename, std::ios::binary | std::ios::out); } + writeHeader(); + } +} + +/// Close the archive, write the updated headers to the file +void Bsa::BSAFile::close() +{ + if (!mHasChanged) + return; + + writeHeader(); } Files::IStreamPtr BSAFile::getFile(const char *file) @@ -181,3 +282,56 @@ Files::IStreamPtr BSAFile::getFile(const FileStruct *file) { return Files::openConstrainedFileStream (mFilename.c_str (), file->offset, file->fileSize); } + +void Bsa::BSAFile::addFile(const std::string& filename, std::istream& file) +{ + namespace bfs = boost::filesystem; + + auto newStartOfDataBuffer = 12 + (12 + 8) * (mFiles.size() + 1) + mStringBuf.size() + filename.size() + 1; + if (mFiles.empty()) + bfs::resize_file(mFilename, newStartOfDataBuffer); + + bfs::fstream stream(mFilename, std::ios::binary | std::ios::in | std::ios::out); + + FileStruct newFile; + file.seekg(0, std::ios::end); + newFile.fileSize = file.tellg(); + newFile.setNameInfos(mStringBuf.size(), &mStringBuf); + newFile.hash = getHash(filename); + + if(mFiles.empty()) + newFile.offset = 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 = 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 = stream.tellp(); + } + + mStringBuf.insert(mStringBuf.end(), filename.begin(), filename.end()); + mStringBuf.push_back('\0'); + mFiles.push_back(newFile); + + mHasChanged = true; + + mLookup[filename.c_str()] = mFiles.size() - 1; + + 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 3e7538401..fa6e5fc1c 100644 --- a/components/bsa/bsa_file.hpp +++ b/components/bsa/bsa_file.hpp @@ -43,20 +43,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 = 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; @@ -72,7 +94,7 @@ protected: /// Case insensitive string comparison struct iltstr { - bool operator()(const char *s1, const char *s2) const + bool operator()(const std::string& s1, const std::string& s2) const { return Misc::StringUtils::ciLess(s1, s2); } }; @@ -80,7 +102,7 @@ protected: the files[] vector above. The iltstr ensures that file name checks are case insensitive. */ - typedef std::map Lookup; + typedef std::map Lookup; Lookup mLookup; /// Error handling @@ -88,9 +110,7 @@ protected: /// Read header information from the input source virtual void readHeader(); - - /// Read header information from the input source - + virtual void writeHeader(); /// Get the index of a given file name, or -1 if not found /// @note Thread safe. @@ -106,11 +126,16 @@ public: : mIsLoaded(false) { } - virtual ~BSAFile() = default; + virtual ~BSAFile() + { + close(); + } /// Open an archive file. void open(const std::string &file); + void close(); + /* ----------------------------------- * Archive file routines * ----------------------------------- @@ -131,6 +156,8 @@ public: */ virtual Files::IStreamPtr getFile(const FileStruct* file); + virtual void addFile(const std::string& filename, std::istream& file); + /// Get a list of all files /// @note Thread safe. const FileList &getList() const diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index 77e477ac5..0f3a26e15 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -226,7 +226,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 +248,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,7 +275,7 @@ 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; @@ -320,13 +319,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); @@ -376,8 +381,10 @@ Files::IStreamPtr CompressedBSAFile::getFile(const FileRecord& fileRecord) 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.get(), &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)); } @@ -430,10 +437,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)) diff --git a/components/bsa/compressedbsafile.hpp b/components/bsa/compressedbsafile.hpp index deddfae38..215a1fc49 100644 --- a/components/bsa/compressedbsafile.hpp +++ b/components/bsa/compressedbsafile.hpp @@ -94,7 +94,7 @@ namespace Bsa Files::IStreamPtr getFile(const char* filePath) override; Files::IStreamPtr getFile(const FileStruct* fileStruct) override; - + void addFile(const std::string& filename, std::istream& file) override; }; } diff --git a/components/bullethelpers/aabb.hpp b/components/bullethelpers/aabb.hpp new file mode 100644 index 000000000..6509b946c --- /dev/null +++ b/components/bullethelpers/aabb.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_AABB_H +#define OPENMW_COMPONENTS_BULLETHELPERS_AABB_H + +#include +#include +#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/crashcatcher/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp index 4ad856548..86571e1e3 100644 --- a/components/crashcatcher/crashcatcher.cpp +++ b/components/crashcatcher/crashcatcher.cpp @@ -43,7 +43,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"; @@ -146,11 +146,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" diff --git a/components/crashcatcher/crashcatcher.hpp b/components/crashcatcher/crashcatcher.hpp index fd8f0d154..b693ccae4 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 index 4c3dfa8f6..33ec319aa 100644 --- a/components/crashcatcher/windows_crashcatcher.cpp +++ b/components/crashcatcher/windows_crashcatcher.cpp @@ -178,8 +178,6 @@ namespace Crash sInstance->handleVectoredException(info); _Exit(1); - - return EXCEPTION_CONTINUE_SEARCH; } void CrashCatcher::handleVectoredException(PEXCEPTION_POINTERS info) diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index e9bcf8ad7..0e372942f 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -179,7 +179,12 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c std::cerr.rdbuf (&sb); #else // Redirect cout and cerr to the log file - logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName)); + // If we are collecting a stack trace, append to existing log file + std::ios_base::openmode mode = std::ios::out; + if(argc == 2 && strcmp(argv[1], crash_switch) == 0) + mode |= std::ios::app; + + logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName), mode); coutsb.open (Debug::Tee(logfile, oldcout)); cerrsb.open (Debug::Tee(logfile, oldcerr)); diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 1ac928f07..4855cf0ad 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -2,8 +2,10 @@ #include "debug.hpp" #include "makenavmesh.hpp" #include "settings.hpp" +#include "version.hpp" #include +#include #include @@ -135,6 +137,7 @@ namespace DetourNavigator void AsyncNavMeshUpdater::process() noexcept { Log(Debug::Debug) << "Start process navigator jobs by thread=" << std::this_thread::get_id(); + Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { try @@ -178,6 +181,19 @@ namespace DetourNavigator const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile, offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache); + if (recastMesh != nullptr) + { + Version navMeshVersion; + { + const auto locked = navMeshCacheItem->lockConst(); + navMeshVersion.mGeneration = locked->getGeneration(); + navMeshVersion.mRevision = locked->getNavMeshRevision(); + } + mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, + Version {recastMesh->getGeneration(), recastMesh->getRevision()}, + navMeshVersion); + } + const auto finish = std::chrono::steady_clock::now(); writeDebugFiles(job, recastMesh.get()); diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index 90b426610..22b047fbd 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -61,4 +61,9 @@ namespace DetourNavigator { return mImpl.isEmpty(); } + + void CachedRecastMeshManager::reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion) + { + mImpl.reportNavMeshChange(recastMeshVersion, navMeshVersion); + } } diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index 54574aa99..a19f017a4 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_CACHEDRECASTMESHMANAGER_H #include "recastmeshmanager.hpp" +#include "version.hpp" namespace DetourNavigator { @@ -25,6 +26,8 @@ namespace DetourNavigator bool isEmpty() const; + void reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion); + private: RecastMeshManager mImpl; std::shared_ptr mCached; diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index 76f74f266..a9a4ebee6 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -5,6 +5,7 @@ #include "tileposition.hpp" #include "navmeshtilescache.hpp" #include "dtstatus.hpp" +#include "navmeshtileview.hpp" #include @@ -141,6 +142,12 @@ namespace DetourNavigator template UpdateNavMeshStatus updateTile(const TilePosition& position, T&& navMeshData) { + const dtMeshTile* currentTile = getTile(position); + if (currentTile != nullptr + && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(getRawData(navMeshData))) + { + return UpdateNavMeshStatus::ignored; + } const auto removed = removeTileImpl(position); const auto addStatus = addTileImpl(getRawData(navMeshData), getSize(navMeshData)); if (dtStatusSucceed(addStatus)) @@ -206,6 +213,12 @@ namespace DetourNavigator int* const dataSize = nullptr; return dtStatusSucceed(mImpl->removeTile(tileRef, data, dataSize)); } + + const dtMeshTile* getTile(const TilePosition& position) const + { + const int layer = 0; + return mImpl->getTileAt(position.x(), position.y(), layer); + } }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp new file mode 100644 index 000000000..96f07c6b1 --- /dev/null +++ b/components/detournavigator/navmeshtileview.cpp @@ -0,0 +1,183 @@ +#include "navmeshtileview.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace +{ + template + struct Ref + { + T& mRef; + + explicit Ref(T& ref) : mRef(ref) {} + + friend bool operator==(const Ref& lhs, const Ref& rhs) + { + return lhs.mRef == rhs.mRef; + } + }; + + template + struct ArrayRef + { + T (&mRef)[size]; + + explicit ArrayRef(T (&ref)[size]) : 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; + + explicit Span(T* data, int size) : 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); + } + }; + + auto makeTuple(const dtMeshHeader& v) + { + 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 + ); + } + + auto makeTuple(const dtPoly& v) + { + return std::tuple(ArrayRef(v.verts), ArrayRef(v.neis), v.flags, v.vertCount, v.areaAndtype); + } + + auto makeTuple(const dtPolyDetail& v) + { + return std::tuple(v.vertBase, v.triBase, v.vertCount, v.triCount); + } + + auto makeTuple(const dtBVNode& v) + { + return std::tuple(ArrayRef(v.bmin), ArrayRef(v.bmax), v.i); + } + + auto makeTuple(const dtOffMeshConnection& v) + { + return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId); + } + + 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) + ); + } +} + +template +inline auto operator==(const T& lhs, const T& rhs) + -> std::enable_if_t, void>, bool> +{ + 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) + { + return makeTuple(lhs) == makeTuple(rhs); + } +} diff --git a/components/detournavigator/navmeshtileview.hpp b/components/detournavigator/navmeshtileview.hpp new file mode 100644 index 000000000..92017360c --- /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); + }; + + NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data); + NavMeshTileConstView asNavMeshTileConstView(const dtMeshTile& tile); +} + +#endif diff --git a/components/detournavigator/oscillatingrecastmeshobject.cpp b/components/detournavigator/oscillatingrecastmeshobject.cpp new file mode 100644 index 000000000..dd0b95600 --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.cpp @@ -0,0 +1,35 @@ +#include "oscillatingrecastmeshobject.hpp" + +#include + +namespace DetourNavigator +{ + OscillatingRecastMeshObject::OscillatingRecastMeshObject(RecastMeshObject impl, std::size_t lastChangeRevision) + : mImpl(std::move(impl)) + , mLastChangeRevision(lastChangeRevision) + , mAabb(BulletHelpers::getAabb(mImpl.getShape(), mImpl.getTransform())) + { + } + + bool OscillatingRecastMeshObject::update(const btTransform& transform, const AreaType areaType, + std::size_t lastChangeRevision) + { + 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)); + return currentAabb != mAabb; + } +} diff --git a/components/detournavigator/oscillatingrecastmeshobject.hpp b/components/detournavigator/oscillatingrecastmeshobject.hpp new file mode 100644 index 000000000..476fd4e0d --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H + +#include "areatype.hpp" +#include "recastmeshobject.hpp" + +#include +#include + +namespace DetourNavigator +{ + class OscillatingRecastMeshObject + { + public: + explicit OscillatingRecastMeshObject(RecastMeshObject impl, std::size_t lastChangeRevision); + + bool update(const btTransform& transform, const AreaType areaType, std::size_t lastChangeRevision); + + const RecastMeshObject& getImpl() const { return mImpl; } + + private: + RecastMeshObject mImpl; + std::size_t mLastChangeRevision; + btAABB mAabb; + }; +} + +#endif diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp index 3796c9816..146038ae6 100644 --- a/components/detournavigator/recastmeshmanager.cpp +++ b/components/detournavigator/recastmeshmanager.cpp @@ -13,7 +13,8 @@ namespace DetourNavigator bool RecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) { - const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), RecastMeshObject(shape, transform, areaType)); + const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), + OscillatingRecastMeshObject(RecastMeshObject(shape, transform, areaType), mRevision + 1)); if (!mObjects.emplace(id, iterator).second) { mObjectsOrder.erase(iterator); @@ -28,7 +29,9 @@ namespace DetourNavigator 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)) return false; ++mRevision; return true; @@ -39,7 +42,7 @@ namespace DetourNavigator const auto object = mObjects.find(id); if (object == mObjects.end()) return std::nullopt; - const RemovedRecastMeshObject result {object->second->getShape(), object->second->getTransform()}; + const RemovedRecastMeshObject result {object->second->getImpl().getShape(), object->second->getImpl().getTransform()}; mObjectsOrder.erase(object->second); mObjects.erase(object); ++mRevision; @@ -74,7 +77,7 @@ namespace DetourNavigator std::shared_ptr RecastMeshManager::getMesh() { rebuild(); - return mMeshBuilder.create(mGeneration, mLastBuildRevision); + return mMeshBuilder.create(mGeneration, mRevision); } bool RecastMeshManager::isEmpty() const @@ -82,15 +85,27 @@ namespace DetourNavigator return mObjects.empty(); } - void RecastMeshManager::rebuild() + void RecastMeshManager::reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion) { - if (mLastBuildRevision == mRevision) + if (recastMeshVersion.mGeneration != mGeneration) + return; + if (mLastNavMeshReport.has_value() && navMeshVersion < mLastNavMeshReport->mNavMeshVersion) return; + mLastNavMeshReport = {recastMeshVersion.mRevision, navMeshVersion}; + if (!mLastNavMeshReportedChange.has_value() + || mLastNavMeshReportedChange->mNavMeshVersion < mLastNavMeshReport->mNavMeshVersion) + mLastNavMeshReportedChange = mLastNavMeshReport; + } + + void RecastMeshManager::rebuild() + { mMeshBuilder.reset(); for (const auto& v : mWaterOrder) mMeshBuilder.addWater(v.mCellSize, v.mTransform); - for (const auto& v : mObjectsOrder) + for (const auto& object : mObjectsOrder) + { + const RecastMeshObject& v = object.getImpl(); mMeshBuilder.addObject(v.getShape(), v.getTransform(), v.getAreaType()); - mLastBuildRevision = mRevision; + } } } diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp index 5b568e004..daa123fcb 100644 --- a/components/detournavigator/recastmeshmanager.hpp +++ b/components/detournavigator/recastmeshmanager.hpp @@ -2,8 +2,9 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H #include "recastmeshbuilder.hpp" -#include "recastmeshobject.hpp" +#include "oscillatingrecastmeshobject.hpp" #include "objectid.hpp" +#include "version.hpp" #include @@ -50,15 +51,24 @@ namespace DetourNavigator bool isEmpty() const; + void reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion); + private: + struct Report + { + std::size_t mRevision; + Version mNavMeshVersion; + }; + 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 mObjectsOrder; + std::unordered_map::iterator> mObjects; std::list mWaterOrder; std::map::iterator> mWater; + std::optional mLastNavMeshReportedChange; + std::optional mLastNavMeshReport; void rebuild(); }; diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index aac0b4c3c..3f3546278 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -8,6 +8,22 @@ namespace DetourNavigator { + 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; + } + } + RecastMeshObject::RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) : mShape(shape) @@ -42,19 +58,6 @@ 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()) diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index f25647ae5..e659300e6 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -41,9 +41,6 @@ namespace DetourNavigator AreaType mAreaType; btVector3 mLocalScaling; std::vector mChildren; - - static bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children); }; std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType); diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index fddaa88f1..20ebc8fea 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -145,6 +145,15 @@ namespace DetourNavigator return mRevision; } + void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) + { + const auto tiles = mTiles.lock(); + const auto it = tiles->find(tilePosition); + if (it == tiles->end()) + return; + it->second.reportNavMeshChange(recastMeshVersion, navMeshVersion); + } + bool TileCachedRecastMeshManager::addTile(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border, std::map& tiles) diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index fa192bd73..68683f410 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -5,6 +5,7 @@ #include "tileposition.hpp" #include "settingsutils.hpp" #include "gettilespositions.hpp" +#include "version.hpp" #include @@ -88,6 +89,8 @@ namespace DetourNavigator std::size_t getRevision() const; + void reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion); + private: const Settings& mSettings; Misc::ScopeGuarded> mTiles; diff --git a/components/detournavigator/version.hpp b/components/detournavigator/version.hpp new file mode 100644 index 000000000..c9de98459 --- /dev/null +++ b/components/detournavigator/version.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H + +#include +#include + +namespace DetourNavigator +{ + struct Version + { + std::size_t mGeneration; + std::size_t mRevision; + + friend inline bool operator<(const Version& lhs, const Version& rhs) + { + return std::tie(lhs.mGeneration, lhs.mRevision) < std::tie(rhs.mGeneration, rhs.mRevision); + } + }; +} + +#endif diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index 98edca48f..a56b9f247 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -115,7 +115,7 @@ namespace ESM void Region::blank() { 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.mA = mData.mB = 0; mMapColor = 0; diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index a7f348cb1..53b6aedd3 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -30,7 +30,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"; @@ -51,6 +51,7 @@ namespace ESM ss << "\n Subrecord: " << "SCVR"; ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); Log(Debug::Verbose) << ss.str(); + str = tmp.data(); } for (size_t i = 0; i < mVarNames.size(); i++) diff --git a/components/esm/loadtes3.hpp b/components/esm/loadtes3.hpp index 5b26ac7d2..014e2a136 100644 --- a/components/esm/loadtes3.hpp +++ b/components/esm/loadtes3.hpp @@ -49,7 +49,6 @@ namespace ESM { 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/variant.cpp b/components/esm/variant.cpp index cbcb6e002..3e0417ef7 100644 --- a/components/esm/variant.cpp +++ b/components/esm/variant.cpp @@ -14,106 +14,53 @@ namespace 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 (nullptr) {} - -ESM::Variant::Variant(const std::string &value) -{ - mData = nullptr; - mType = VT_None; - setType(VT_String); - setString(value); -} - -ESM::Variant::Variant(int value) -{ - mData = nullptr; - mType = VT_None; - setType(VT_Long); - setInteger(value); -} - -ESM::Variant::Variant(float value) -{ - mData = nullptr; - mType = VT_None; - setType(VT_Float); - setFloat(value); -} - -ESM::Variant::~Variant() -{ - delete mData; -} -ESM::Variant& ESM::Variant::operator= (const Variant& variant) -{ - if (&variant!=this) + template + struct GetValue { - VariantDataBase *newData = variant.mData ? variant.mData->clone() : nullptr; - - delete mData; + T operator()(int value) const { return static_cast(value); } - mType = variant.mType; - mData = newData; - } + T operator()(float value) const { return static_cast(value); } - return *this; -} + template + T operator()(const V&) const + { + if constexpr (orDefault) + return T {}; + else + throw std::runtime_error("cannot convert variant"); + } + }; -ESM::Variant& ESM::Variant::operator= (Variant&& variant) -{ - if (&variant!=this) + template + struct SetValue { - delete mData; - - mType = variant.mType; - mData = variant.mData; - - variant.mData = nullptr; - } + T mValue; - return *this; -} + explicit SetValue(T value) : mValue(value) {} -ESM::Variant::Variant (const Variant& variant) -: mType (variant.mType), mData (variant.mData ? variant.mData->clone() : nullptr) -{} + void operator()(int& value) const { value = static_cast(mValue); } -ESM::Variant::Variant(Variant&& variant) -: mType (variant.mType), mData (variant.mData) -{ - variant.mData = nullptr; -} + void operator()(float& value) const { value = static_cast(mValue); } -ESM::VarType ESM::Variant::getType() const -{ - return mType; + template + void operator()(V&) const { throw std::runtime_error("cannot convert variant"); } + }; } std::string ESM::Variant::getString() const { - if (!mData) - throw std::runtime_error ("can not convert empty variant to string"); - - return mData->getString(); + return std::get(mData); } int ESM::Variant::getInteger() const { - if (!mData) - throw std::runtime_error ("can not convert empty variant to integer"); - - return mData->getInteger(); + return std::visit(GetValue{}, mData); } float ESM::Variant::getFloat() const { - if (!mData) - throw std::runtime_error ("can not convert empty variant to float"); - - return mData->getFloat(); + return std::visit(GetValue{}, mData); } void ESM::Variant::read (ESMReader& esm, Format format) @@ -202,9 +149,7 @@ void ESM::Variant::read (ESMReader& esm, Format format) setType (type); - // data - if (mData) - mData->read (esm, format, mType); + std::visit(ReadESMVariantValue {esm, format, mType}, mData); } void ESM::Variant::write (ESMWriter& esm, Format format) const @@ -227,7 +172,7 @@ void ESM::Variant::write (ESMWriter& esm, Format format) const // nothing to do here for GMST format } else - mData->write (esm, format, mType); + std::visit(WriteESMVariantValue {esm, format, mType}, mData); } void ESM::Variant::write (std::ostream& stream) const @@ -246,27 +191,27 @@ void ESM::Variant::write (std::ostream& stream) const case VT_Short: - stream << "variant short: " << mData->getInteger(); + stream << "variant short: " << std::get(mData); break; case VT_Int: - stream << "variant int: " << mData->getInteger(); + stream << "variant int: " << std::get(mData); break; case VT_Long: - stream << "variant long: " << mData->getInteger(); + stream << "variant long: " << std::get(mData); break; case VT_Float: - stream << "variant float: " << mData->getFloat(); + stream << "variant float: " << std::get(mData); break; case VT_String: - stream << "variant string: \"" << mData->getString() << "\""; + stream << "variant string: \"" << std::get(mData) << "\""; break; } } @@ -275,74 +220,50 @@ void ESM::Variant::setType (VarType type) { if (type!=mType) { - VariantDataBase *newData = nullptr; - switch (type) { case VT_Unknown: case VT_None: - - break; // no data + mData = std::monostate {}; + break; case VT_Short: case VT_Int: case VT_Long: - - newData = new VariantIntegerData (mData); + mData = std::visit(GetValue{}, mData); break; case VT_Float: - - newData = new VariantFloatData (mData); + mData = std::visit(GetValue{}, mData); break; case VT_String: - - newData = new VariantStringData (mData); + mData = std::string {}; 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); + std::get(mData) = value; } -void ESM::Variant::setInteger (int value) +void ESM::Variant::setString (std::string&& value) { - if (!mData) - throw std::runtime_error ("can not assign integer to empty variant"); - - mData->setInteger (value); + std::get(mData) = std::move(value); } -void ESM::Variant::setFloat (float value) +void ESM::Variant::setInteger (int value) { - if (!mData) - throw std::runtime_error ("can not assign float to empty variant"); - - mData->setFloat (value); + std::visit(SetValue(value), mData); } -bool ESM::Variant::isEqual (const Variant& value) const +void ESM::Variant::setFloat (float value) { - if (mType!=value.mType) - return false; - - if (!mData) - return true; - - assert (value.mData); - - return mData->isEqual (*value.mData); + std::visit(SetValue(value), mData); } std::ostream& ESM::operator<< (std::ostream& stream, const Variant& value) @@ -350,13 +271,3 @@ 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/variant.hpp b/components/esm/variant.hpp index 20b3aa76e..f0a16d4d5 100644 --- a/components/esm/variant.hpp +++ b/components/esm/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,17 @@ 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); - Variant& operator= (Variant && variant); + explicit Variant(int value) : mType(VT_Long), mData(value) {} - Variant (const Variant& variant); - Variant (Variant&& variant); + explicit Variant(float value) : mType(VT_Float), mData(value) {} - VarType getType() const; + VarType getType() const { return mType; } std::string getString() const; ///< Will throw an exception, if value can not be represented as a string. @@ -75,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/esm/variantimp.cpp b/components/esm/variantimp.cpp index aeea5017e..b9cd9a853 100644 --- a/components/esm/variantimp.cpp +++ b/components/esm/variantimp.cpp @@ -1,75 +1,12 @@ #include "variantimp.hpp" #include +#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) +void ESM::readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, std::string& out) { if (type!=VT_String) throw std::logic_error ("not a string type"); @@ -84,10 +21,10 @@ void ESM::VariantStringData::read (ESMReader& esm, Variant::Format format, VarTy esm.fail ("local variables of type string not supported"); // GMST - mValue = esm.getHString(); + out = esm.getHString(); } -void ESM::VariantStringData::write (ESMWriter& esm, Variant::Format format, VarType type) const +void ESM::writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, const std::string& in) { if (type!=VT_String) throw std::logic_error ("not a string type"); @@ -98,49 +35,14 @@ void ESM::VariantStringData::write (ESMWriter& esm, Variant::Format format, VarT 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; -} + if (format==Variant::Format_Local) + throw std::runtime_error ("local variables of type string not supported"); -void ESM::VariantIntegerData::setFloat (float value) -{ - mValue = static_cast (value); + // GMST + esm.writeHNString("STRV", in); } -void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarType type) +void ESM::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"); @@ -151,14 +53,12 @@ void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarT esm.getHNT (value, "FLTV"); if (type==VT_Short) - { - if (value!=value) - mValue = 0; // nan + if (std::isnan(value)) + out = 0; else - mValue = static_cast (value); - } + out = static_cast (value); else if (type==VT_Long) - mValue = static_cast (value); + out = static_cast (value); else esm.fail ("unsupported global variable integer type"); } @@ -173,7 +73,7 @@ void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarT esm.fail (stream.str()); } - esm.getHT (mValue); + esm.getHT(out); } else if (format==Variant::Format_Local) { @@ -181,18 +81,18 @@ void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarT { short value; esm.getHT(value); - mValue = value; + out = value; } else if (type==VT_Int) { - esm.getHT(mValue); + esm.getHT(out); } else esm.fail("unsupported local variable integer type"); } } -void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, VarType type) const +void ESM::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"); @@ -201,7 +101,7 @@ void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, Var { if (type==VT_Short || type==VT_Long) { - float value = static_cast(mValue); + float value = static_cast(in); esm.writeHNString ("FNAM", type==VT_Short ? "s" : "l"); esm.writeHNT ("FLTV", value); } @@ -219,72 +119,35 @@ void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, Var throw std::runtime_error (stream.str()); } - esm.writeHNT ("INTV", mValue); + esm.writeHNT("INTV", in); } else if (format==Variant::Format_Local) { if (type==VT_Short) - esm.writeHNT ("STTV", (short)mValue); + esm.writeHNT("STTV", static_cast(in)); else if (type == VT_Int) - esm.writeHNT ("INTV", mValue); + esm.writeHNT("INTV", in); 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) +void ESM::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 (mValue, "FLTV"); + esm.getHNT(out, "FLTV"); } else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) { - esm.getHT (mValue); + esm.getHT(out); } } -void ESM::VariantFloatData::write (ESMWriter& esm, Variant::Format format, VarType type) const +void ESM::writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, float in) { if (type!=VT_Float) throw std::logic_error ("not a float type"); @@ -292,15 +155,10 @@ void ESM::VariantFloatData::write (ESMWriter& esm, Variant::Format format, VarTy if (format==Variant::Format_Global) { esm.writeHNString ("FNAM", "f"); - esm.writeHNT ("FLTV", mValue); + esm.writeHNT("FLTV", in); } else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) { - esm.writeHNT ("FLTV", mValue); + esm.writeHNT("FLTV", in); } } - -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 index c1203ddf8..945872811 100644 --- a/components/esm/variantimp.hpp +++ b/components/esm/variantimp.hpp @@ -2,177 +2,58 @@ #define OPENMW_ESM_VARIANTIMP_H #include +#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: + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, std::string& value); - VariantStringData (const VariantDataBase *data = nullptr); - ///< Calling the constructor with an incompatible data type will result in a silent - /// default initialisation. + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, float& value); - VariantDataBase *clone() const override; + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int& value); - 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 writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, const std::string& value); - void setString (const std::string& value) override; - ///< Will throw an exception, if type is not compatible with string. + void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, float value); - 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 writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int value); - 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 + struct ReadESMVariantValue { - int mValue; - - public: - - VariantIntegerData (const VariantDataBase *data = nullptr); - ///< Calling the constructor with an incompatible data type will result in a silent - /// default initialisation. - - VariantDataBase *clone() const override; + std::reference_wrapper mReader; + Variant::Format mFormat; + VarType mType; - 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. + ReadESMVariantValue(ESMReader& reader, Variant::Format format, VarType type) + : mReader(reader), mFormat(format), mType(type) {} - 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 operator()(std::monostate) const {} - 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. + template + void operator()(T& value) const + { + readESMVariantValue(mReader.get(), mFormat, mType, value); + } }; - class VariantFloatData : public VariantDataBase + struct WriteESMVariantValue { - float mValue; - - public: - - VariantFloatData (const VariantDataBase *data = nullptr); - ///< 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. + std::reference_wrapper mWriter; + Variant::Format mFormat; + VarType mType; - 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 + WriteESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type) + : mWriter(writer), mFormat(format), mType(type) {} - void write (ESMWriter& esm, Variant::Format format, VarType type) const override; - ///< If \a type is not supported by \a format, an exception is thrown. + void operator()(std::monostate) const {} - 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. + template + void operator()(const T& value) const + { + writeESMVariantValue(mWriter.get(), mFormat, mType, value); + } }; } diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 2bed079e1..b4739110f 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -146,10 +146,11 @@ 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, const std::string& userDataPath, float scalingFactor) : mVFS(vfs) , mUserDataPath(userDataPath) , mFontHeight(16) + , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) mEncoding = ToUTF8::CP437; @@ -566,10 +567,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::min(960, std::max(48, resolution)) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp index 94b022501..8a46a0069 100644 --- a/components/fontloader/fontloader.hpp +++ b/components/fontloader/fontloader.hpp @@ -27,7 +27,7 @@ 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, const std::string& userDataPath, float scalingFactor); ~FontLoader(); /// @param exportToFile export the converted fonts (Images and XML with glyph metrics) to files? @@ -43,6 +43,7 @@ namespace Gui const VFS::Manager* mVFS; std::string mUserDataPath; int mFontHeight; + float mScalingFactor; std::vector mTextures; std::vector mFonts; diff --git a/components/misc/frameratelimiter.hpp b/components/misc/frameratelimiter.hpp index b8e210165..e3689cbcf 100644 --- a/components/misc/frameratelimiter.hpp +++ b/components/misc/frameratelimiter.hpp @@ -14,6 +14,7 @@ namespace Misc 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 diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp new file mode 100644 index 000000000..30a9c41ee --- /dev/null +++ b/components/misc/hash.hpp @@ -0,0 +1,15 @@ +#ifndef MISC_HASH_H +#define MISC_HASH_H + +namespace Misc +{ + /// Implemented similar to the boost::hash_combine + template + inline void hashCombine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + } +} + +#endif \ No newline at end of file diff --git a/components/misc/thread.cpp b/components/misc/thread.cpp new file mode 100644 index 000000000..c78bf17e5 --- /dev/null +++ b/components/misc/thread.cpp @@ -0,0 +1,73 @@ +#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) + +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN + +#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 000000000..191c43bba --- /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/nif/data.cpp b/components/nif/data.cpp index 415a013b3..b6674611b 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -363,11 +363,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 +395,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) diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 96beafcbb..b45916693 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -24,11 +24,6 @@ namespace osg class Material; } -namespace osgParticle -{ - class Emitter; -} - namespace NifOsg { diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index ae1726f06..5f083f925 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -214,7 +214,7 @@ namespace NifOsg return sHiddenNodeMask; } - unsigned int Loader::sIntersectionDisabledNodeMask = ~0; + unsigned int Loader::sIntersectionDisabledNodeMask = ~0u; void Loader::setIntersectionDisabledNodeMask(unsigned int mask) { @@ -238,7 +238,7 @@ namespace NifOsg std::string mFilename; unsigned int mVersion, mUserVersion, mBethVersion; - size_t mFirstRootTextureIndex = -1; + size_t mFirstRootTextureIndex{~0u}; bool mFoundFirstRootTexturingProperty = false; bool mHasNightDayLabel = false; diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index f4ab79519..d7eeeeb97 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -77,7 +77,7 @@ namespace Resource mTarget.mTextKeys.emplace(parseTimeSignature(line), parseTextKey(line)); } } - catch (std::exception& e) + catch (std::exception&) { Log(Debug::Warning) << "No textkey file found for " << mNormalized; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 66d48f971..a3c751f7a 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -226,6 +226,8 @@ namespace Resource , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) + , mLightingMethod(SceneUtil::LightingMethod::FFP) + , mConvertAlphaTestToAlphaToCoverage(false) , mInstanceCache(new MultiObjectCache) , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) @@ -303,6 +305,26 @@ 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; + } + + SceneUtil::LightingMethod SceneManager::getLightingMethod() const + { + return mLightingMethod; + } + void SceneManager::setConvertAlphaTestToAlphaToCoverage(bool convert) { mConvertAlphaTestToAlphaToCoverage = convert; diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index bf69a8c4b..7635cd20f 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -12,6 +12,8 @@ #include "resourcemanager.hpp" +#include + namespace Resource { class ImageManager; @@ -105,6 +107,12 @@ namespace Resource void setApplyLightingToEnvMaps(bool apply); + void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); + bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + + void setLightingMethod(SceneUtil::LightingMethod method); + SceneUtil::LightingMethod getLightingMethod() const; + void setConvertAlphaTestToAlphaToCoverage(bool convert); void setShaderPath(const std::string& path); @@ -191,6 +199,8 @@ namespace Resource bool mAutoUseSpecularMaps; std::string mSpecularMapPattern; bool mApplyLightingToEnvMaps; + SceneUtil::LightingMethod mLightingMethod; + SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; bool mConvertAlphaTestToAlphaToCoverage; osg::ref_ptr mInstanceCache; diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index c759fabc7..cc320aecf 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -62,7 +62,8 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness); + auto* lightSource = static_cast(node); + lightSource->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * lightSource->getActorFade()); traverse(node, nv); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index bea4da8bc..f592460bb 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,362 +1,1252 @@ #include "lightmanager.hpp" +#include + +#include +#include +#include +#include +#include + #include #include +#include +#include + +#include + +namespace +{ + 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; + } + + float getLightRadius(const osg::Light* light) + { + float value = 0.0; + light->getUserValue("radius", value); + return value; + } + + void setLightRadius(osg::Light* light, float value) + { + light->setUserValue("radius", value); + } + + 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; + } + + bool isReflectionCamera(osg::Camera* camera) + { + return (camera->getName() == "ReflectionCamera"); + } +} + namespace SceneUtil { + 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: + + enum LayoutOffset + { + Diffuse, + DiffuseSign, + Ambient, + Specular, + Position, + AttenuationRadius + }; + + LightBuffer(int count) + : mData(new osg::FloatArray(3*4*count)) + , mEndian(osg::getCpuByteOrder()) + , mCount(count) + , mStride(12) + , mCachedSunPos(osg::Vec4()) + { + mOffsets[Diffuse] = 0; + mOffsets[Ambient] = 1; + mOffsets[Specular] = 2; + mOffsets[DiffuseSign] = 3; + mOffsets[Position] = 4; + mOffsets[AttenuationRadius] = 8; + } + + LightBuffer(const LightBuffer& copy) + : osg::Referenced() + , mData(copy.mData) + , mEndian(copy.mEndian) + , mCount(copy.mCount) + , mStride(copy.mStride) + , mOffsets(copy.mOffsets) + , mCachedSunPos(copy.mCachedSunPos) + {} + + 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(GL_FLOAT) * 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) + { + return mStride * index + mOffsets[slot]; + } + + void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) + { + constexpr auto sizeofFloat = sizeof(GL_FLOAT); + constexpr auto sizeofVec4 = sizeofFloat * osg::Vec4::num_components; + + LightBuffer oldBuffer = LightBuffer(*this); + + mOffsets[Diffuse] = offsetColors / sizeofFloat; + mOffsets[Ambient] = mOffsets[Diffuse] + 1; + mOffsets[Specular] = mOffsets[Diffuse] + 2; + mOffsets[DiffuseSign] = mOffsets[Diffuse] + 3; + mOffsets[Position] = offsetPosition / sizeofFloat; + mOffsets[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; + mStride = (offsetAttenuationRadius + sizeofVec4 + stride) / 4; + + // Copy over previous buffers light data. Buffers populate before we know the layout. + mData->resize(size / sizeofFloat); + for (int i = 0; i < oldBuffer.mCount; ++i) + { + std::memcpy(&(*mData)[getOffset(i, Diffuse)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Diffuse)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, Position)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Position)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, AttenuationRadius)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, AttenuationRadius)], sizeof(osg::Vec4f)); + } + } + + private: + osg::ref_ptr mData; + osg::Endian mEndian; + int mCount; + int mStride; + std::array mOffsets; + osg::Vec4 mCachedSunPos; + }; class LightStateCache { public: - osg::Light* lastAppliedLight[8]; + std::vector lastAppliedLight; }; - LightStateCache* getLightStateCache(unsigned int contextid) + 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()); + + osg::ref_ptr uni = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", lightManager->getMaxLights()); + uni->setElement(0, lightMat); + stateset->addUniform(uni, 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(Shader::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, mode); + + break; + } + } + } + + class DisableLight : public osg::StateAttribute + { + public: + 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) {} + + 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; } + + unsigned int getMember() const override + { + return mIndex; + } + + 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 + { + return mIndex; + } + + bool getModeUsage(ModeUsage & usage) const override + { + for (size_t i = 0; i < mLights.size(); ++i) + usage.usesMode(GL_LIGHT0 + mIndex + i); + return true; + } + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("FFPLightStateAttribute::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, FFPLightStateAttribute, osg::StateAttribute::LIGHT) + + void apply(osg::State& state) const override + { + if (mLights.empty()) + return; + osg::Matrix modelViewMatrix = state.getModelViewMatrix(); + + state.applyModelViewMatrix(state.getInitialViewMatrix()); + + LightStateCache* cache = getLightStateCache(state.getContextID()); + + for (size_t i = 0; i < mLights.size(); ++i) + { + osg::Light* current = cache->lastAppliedLight[i+mIndex]; + if (current != mLights[i].get()) + { + applyLight((GLenum)((int)GL_LIGHT0 + i + mIndex), mLights[i].get()); + cache->lastAppliedLight[i+mIndex] = mLights[i].get(); + } + } + + state.applyModelViewMatrix(modelViewMatrix); + } + + 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()); + // 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()); + } + + private: + size_t mIndex; + std::vector> mLights; + }; + + LightManager* findLightManager(const osg::NodePath& path) + { + for (size_t i = 0; i < path.size(); ++i) + { + if (LightManager* lightManager = dynamic_cast(path[i])) + return lightManager; + } + return nullptr; + } + + class LightStateAttributePerObjectUniform : public osg::StateAttribute + { + public: + LightStateAttributePerObjectUniform() : mLightManager(nullptr) {} + LightStateAttributePerObjectUniform(const std::vector>& lights, LightManager* lightManager) : mLights(lights), mLightManager(lightManager) {} + + LightStateAttributePerObjectUniform(const LightStateAttributePerObjectUniform& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLights(copy.mLights), mLightManager(copy.mLightManager) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightStateAttributePerObjectUniform::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightStateAttributePerObjectUniform, osg::StateAttribute::LIGHT) + + void resize(int numLights) + { + mLights.resize(std::min(static_cast(numLights), mLights.size())); + } + + void apply(osg::State &state) const override + { + auto* lightUniform = mLightManager->getStateSet()->getUniform("LightBuffer"); + for (size_t i = 0; i < mLights.size(); ++i) + { + auto light = mLights[i]; + osg::Matrixf lightMat; + + configurePosition(lightMat, light->getPosition() * state.getInitialViewMatrix()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), getLightRadius(light)); + + lightUniform->setElement(i+1, lightMat); + } + + auto sun = mLightManager->getSunlightBuffer(state.getFrameStamp()->getFrameNumber()); + configurePosition(sun, osg::Vec4(sun(0,0), sun(0,1), sun(0,2), 0.0) * state.getInitialViewMatrix()); + lightUniform->setElement(0, sun); + + lightUniform->dirty(); + } + + private: + std::vector> mLights; + LightManager* mLightManager; + }; + + 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) {} + }; + + 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); + } + + 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; + + std::vector> lights(lightList.size()); + + for (size_t i = 0; i < lightList.size(); ++i) + { + auto* light = lightList[i]->mLightSource->getLight(frameNum); + lights[i] = light; + setLightRadius(light, lightList[i]->mLightSource->getRadius()); + } + + stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); + + return stateset; + } + }; + + // 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 + { + public: + CollectLightCallback() + : mLightManager(nullptr) { } + + CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) + : osg::NodeCallback(copy, copyop) + , mLightManager(nullptr) { } + + META_Object(SceneUtil, SceneUtil::CollectLightCallback) + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + if (!mLightManager) + { + mLightManager = findLightManager(nv->getNodePath()); + + if (!mLightManager) + throw std::runtime_error("can't find parent LightManager"); + } + + mLightManager->addLight(static_cast(node), osg::computeLocalToWorld(nv->getNodePath()), nv->getTraversalNumber()); + + traverse(node, nv); + } + + private: + LightManager* mLightManager; + }; + + // Set on a LightManager. Clears the data from the previous frame. + class LightManagerUpdateCallback : public osg::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 + { + LightManager* lightManager = static_cast(node); + lightManager->update(nv->getTraversalNumber()); + + traverse(node, nv); + } + }; + + class LightManagerCullCallback : public osg::NodeCallback + { + public: + LightManagerCullCallback(LightManager* lightManager) : mLightManager(lightManager), mLastFrameNumber(0) {} + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + + if (mLastFrameNumber != cv->getTraversalNumber()) + { + mLastFrameNumber = cv->getTraversalNumber(); + + if (mLightManager->getLightingMethod() == LightingMethod::SingleUBO) + { + auto stateset = mLightManager->getStateSet(); + auto bo = mLightManager->getLightBuffer(mLastFrameNumber); + +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); + } + + auto sun = mLightManager->getSunlight(); + + if (sun) + { + // we must defer uploading the transformation to view-space position to deal with different cameras (e.g. reflection RTT). + if (mLightManager->getLightingMethod() == LightingMethod::PerObjectUniform) + { + osg::Matrixf lightMat; + configurePosition(lightMat, sun->getPosition()); + configureAmbient(lightMat, sun->getAmbient()); + configureDiffuse(lightMat, sun->getDiffuse()); + configureSpecular(lightMat, sun->getSpecular()); + mLightManager->setSunlightBuffer(lightMat, mLastFrameNumber); + } + else + { + auto buf = mLightManager->getLightBuffer(mLastFrameNumber); + + buf->setCachedSunPos(sun->getPosition()); + buf->setAmbient(0, sun->getAmbient()); + buf->setDiffuse(0, sun->getDiffuse()); + buf->setSpecular(0, sun->getSpecular()); + } + } + } + + traverse(node, nv); + } + + private: + LightManager* mLightManager; + size_t mLastFrameNumber; + }; + + class LightManagerStateAttribute : public osg::StateAttribute + { + public: + LightManagerStateAttribute() + : mLightManager(nullptr) + , mInitLayout(false) + { + } + + LightManagerStateAttribute(LightManager* lightManager) + : mLightManager(lightManager) + , mDummyProgram(new osg::Program) + , mInitLayout(false) + { + static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene()); + + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); + // 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. + mLightManager->getOrCreateStateSet()->setAttributeAndModes(mDummyProgram, osg::StateAttribute::ON); + } + + LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager), mInitLayout(copy.mInitLayout) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT) + + void initSharedLayout(osg::GLExtensions* ext, int handle) const + { + constexpr std::array index = { static_cast(Shader::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()); + + for (int i = 0; i < 2; ++i) + { + auto& buf = mLightManager->getLightBuffer(i); + buf->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } + } + + void apply(osg::State& state) const override + { + if (!mInitLayout) + { + 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); + mInitLayout = true; + } + } + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->uploadCachedSunPos(state.getInitialViewMatrix()); + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); + } + + private: + + std::string 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; + } + + LightManager* mLightManager; + osg::ref_ptr mDummyProgram; + mutable bool mInitLayout; + }; + + const std::unordered_map LightManager::mLightingMethodSettingMap = { + {"legacy", LightingMethod::FFP} + ,{"shaders compatibility", LightingMethod::PerObjectUniform} + ,{"shaders", LightingMethod::SingleUBO} + }; + + LightingMethod LightManager::getLightingMethodFromString(const std::string& value) + { + auto it = LightManager::mLightingMethodSettingMap.find(value); + if (it != LightManager::mLightingMethodSettingMap.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 : LightManager::mLightingMethodSettingMap) + 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) { - public: - LightStateAttribute() : mIndex(0) {} - LightStateAttribute(unsigned int index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + osg::GLExtensions* exts = osg::GLExtensions::Get(0, false); + bool supportsUBO = exts && exts->isUniformBufferObjectSupported; + bool supportsGPU4 = exts && exts->isGpuShader4Supported; - LightStateAttribute(const LightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex), mLights(copy.mLights) {} + mSupported[static_cast(LightingMethod::FFP)] = true; + mSupported[static_cast(LightingMethod::PerObjectUniform)] = true; + mSupported[static_cast(LightingMethod::SingleUBO)] = supportsUBO && supportsGPU4; - unsigned int getMember() const override - { - return mIndex; - } + setUpdateCallback(new LightManagerUpdateCallback); - bool getModeUsage(ModeUsage & usage) const override + if (ffp) { - for (unsigned int i=0; iaddUniform(new osg::Uniform("PointLightCount", 0)); - LightStateCache* cache = getLightStateCache(state.getContextID()); + addCullCallback(new LightManagerCullCallback(this)); + } - for (unsigned int i=0; ilastAppliedLight[i+mIndex]; - if (current != mLights[i].get()) - { - applyLight((GLenum)((int)GL_LIGHT0 + i + mIndex), mLights[i].get()); - cache->lastAppliedLight[i+mIndex] = mLights[i].get(); - } - } + 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) + { + } - state.applyModelViewMatrix(modelViewMatrix); - } + LightingMethod LightManager::getLightingMethod() const + { + return mLightingMethod; + } - 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() ); - // 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() ); - } + bool LightManager::usingFFP() const + { + return mLightingMethod == LightingMethod::FFP; + } - private: - unsigned int mIndex; + int LightManager::getMaxLights() const + { + return mMaxLights; + } - std::vector > mLights; - }; + void LightManager::setMaxLights(int value) + { + mMaxLights = value; + } - LightManager* findLightManager(const osg::NodePath& path) + int LightManager::getMaxLightsInScene() const { - for (unsigned int i=0;i(path[i])) - return lightManager; - } - return nullptr; + static constexpr int max = 16384 / LightBuffer::queryBlockSize(1); + return max; } - // 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 + Shader::ShaderManager::DefineMap LightManager::getLightDefines() const { - public: - CollectLightCallback() - : mLightManager(nullptr) { } + 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; + } - CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) - , mLightManager(nullptr) { } + void LightManager::processChangedSettings(const Settings::CategorySettingVector& changed) + { + updateSettings(); + } - META_Object(SceneUtil, SceneUtil::CollectLightCallback) + void LightManager::updateMaxLights() + { + if (usingFFP()) + return; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit); + setMaxLights(targetLights); + + if (getLightingMethod() == LightingMethod::PerObjectUniform) { - if (!mLightManager) + auto* prevUniform = getStateSet()->getUniform("LightBuffer"); + osg::ref_ptr newUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); + + for (int i = 0; i < getMaxLights(); ++i) { - mLightManager = findLightManager(nv->getNodePath()); + osg::Matrixf prevLightData; + prevUniform->getElement(i, prevLightData); + newUniform->setElement(i, prevLightData); + } - if (!mLightManager) - throw std::runtime_error("can't find parent LightManager"); + getStateSet()->removeUniform(prevUniform); + getStateSet()->addUniform(newUniform); + + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + static_cast(pair.second->getAttribute(osg::StateAttribute::LIGHT))->resize(getMaxLights()); + mStateSetCache[i].clear(); } + } + else + { + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + { + auto& stateset = pair.second; + osg::Uniform* uOldArray = stateset->getUniform("PointLightIndex"); + osg::Uniform* uOldCount = stateset->getUniform("PointLightCount"); - mLightManager->addLight(static_cast(node), osg::computeLocalToWorld(nv->getNodePath()), nv->getTraversalNumber()); + int prevCount; + uOldCount->get(prevCount); + int newCount = std::min(getMaxLights(), prevCount); + uOldCount->set(newCount); - traverse(node, nv); + osg::ref_ptr newArray = uOldArray->getIntArray(); + newArray->resize(newCount); + + stateset->removeUniform(uOldArray); + stateset->addUniform(new osg::Uniform("PointLightIndex", newArray)); + } + mStateSetCache[i].clear(); + } } + } - private: - LightManager* mLightManager; - }; + void LightManager::updateSettings() + { + if (getLightingMethod() == LightingMethod::FFP) + return; - // Set on a LightManager. Clears the data from the previous frame. - class LightManagerUpdateCallback : public osg::NodeCallback + 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) { - public: - LightManagerUpdateCallback() - { } + setLightingMethod(LightingMethod::FFP); + setMaxLights(targetLights); - LightManagerUpdateCallback(const LightManagerUpdateCallback& copy, const osg::CopyOp& copyop) - : osg::NodeCallback(copy, copyop) - { } + for (int i = 0; i < getMaxLights(); ++i) + mDummies.push_back(new FFPLightStateAttribute(i, std::vector>())); + } - META_Object(SceneUtil, LightManagerUpdateCallback) + void LightManager::initPerObjectUniform(int targetLights) + { + auto* stateset = getOrCreateStateSet(); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + setLightingMethod(LightingMethod::PerObjectUniform); + setMaxLights(targetLights); + + // ensures sunlight element in our uniform array is updated when there are no point lights in scene + stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform({}, this), osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights())); + } + + void LightManager::initSingleUBO(int targetLights) + { + setLightingMethod(LightingMethod::SingleUBO); + setMaxLights(targetLights); + + for (int i = 0; i < 2; ++i) { - LightManager* lightManager = static_cast(node); - lightManager->update(); + mLightBuffers[i] = new LightBuffer(getMaxLightsInScene()); - traverse(node, nv); + osg::ref_ptr ubo = new osg::UniformBufferObject; + ubo->setUsage(GL_STREAM_DRAW); + + mLightBuffers[i]->getData()->setBufferObject(ubo); } - }; - LightManager::LightManager() - : mStartLight(0) - , mLightingMask(~0u) - { - setUpdateCallback(new LightManagerUpdateCallback); - for (unsigned int i=0; i<8; ++i) - mDummies.push_back(new LightStateAttribute(i, std::vector >())); + getOrCreateStateSet()->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON); } - LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) - : osg::Group(copy, copyop) - , mStartLight(copy.mStartLight) - , mLightingMask(copy.mLightingMask) + 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(unsigned int mask) + 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) { + 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::getSunlight() + { + return mSun; } - osg::ref_ptr LightManager::getLightListStateSet(const LightList &lightList, unsigned int frameNum) + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) { // 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()); - - LightStateSetMap& stateSetCache = mStateSetCache[frameNum%2]; - - LightStateSetMap::iterator found = stateSetCache.find(hash); - if (found != stateSetCache.end()) - return found->second; - else + for (size_t i = 0; i < lightList.size(); ++i) { - osg::ref_ptr stateset = new osg::StateSet; - std::vector > lights; - lights.reserve(lightList.size()); - for (unsigned int i=0; imLightSource->getLight(frameNum)); + auto id = lightList[i]->mLightSource->getId(); + Misc::hashCombine(hash, id); - // 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); + if (getLightingMethod() != LightingMethod::SingleUBO) + continue; - for (unsigned int i=0; isetMode(GL_LIGHT0 + mStartLight + i, osg::StateAttribute::ON); + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; - // 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); + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + } - stateSetCache.emplace(hash, stateset); - return stateset; + auto& stateSetCache = mStateSetCache[frameNum%2]; + + auto found = stateSetCache.find(hash); + 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(hash, stateset); + return stateset; } - const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix) + const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum) { + bool isReflection = isReflectionCamera(camera); 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 * mPointLightRadiusMultiplier); transformBoundingSphere(worldViewMat, viewBound); + if (!isReflection && mPointLightFadeEnd != 0.f) + { + const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; + float fade = 1 - std::clamp((viewBound.center().length() - mPointLightFadeStart) / fadeDelta, 0.f, 1.f); + if (fade == 0.f) + continue; + + auto* light = transform.mLightSource->getLight(frameNum); + light->setDiffuse(light->getDiffuse() * fade); + } + 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) {} - - 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; } - - unsigned int getMember() const override - { - return mIndex; - } - - 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 + if (getLightingMethod() == LightingMethod::SingleUBO) { - 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; + if (it->second.size() > static_cast(getMaxLightsInScene() - 1)) + { + auto sorter = [] (const LightSourceViewBound& left, const LightSourceViewBound& right) { + return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2(); + }; + std::sort(it->second.begin() + 1, it->second.end(), sorter); + it->second.erase((it->second.begin() + 1) + (getMaxLightsInScene() - 2), it->second.end()); + } } - private: - unsigned int mIndex; - osg::Vec4f mnullptr; - }; - - void LightManager::setStartLight(int start) - { - mStartLight = start; - - // 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 it->second; } - int LightManager::getStartLight() const + void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix) { - return mStartLight; + auto* light = lightSource->getLight(frameNum); + auto& buf = 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)); } - static int sLightId = 0; - LightSource::LightSource() : mRadius(0.f) + , mActorFade(1.f) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; @@ -365,19 +1255,14 @@ namespace SceneUtil LightSource::LightSource(const LightSource ©, const osg::CopyOp ©op) : osg::Node(copy, copyop) , mRadius(copy.mRadius) + , mActorFade(copy.mActorFade) { mId = sLightId++; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } - - bool sortLights (const LightManager::LightSourceViewBound* left, const LightManager::LightSourceViewBound* right) - { - 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); @@ -413,7 +1298,7 @@ namespace SceneUtil // 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); + const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix, mLastFrameNumber); // get the node bounds in view space // NB do not node->getBound() * modelView, that would apply the node's transformation twice @@ -421,7 +1306,7 @@ namespace SceneUtil osg::Transform* transform = node->asTransform(); if (transform) { - for (unsigned int i=0; igetNumChildren(); ++i) + for (size_t i = 0; i < transform->getNumChildren(); ++i) nodeBound.expandBy(transform->getChild(i)->getBound()); } else @@ -430,6 +1315,7 @@ namespace SceneUtil transformBoundingSphere(mat, nodeBound); mLightList.clear(); + for (size_t i=0; i (8 - mLightManager->getStartLight()); + size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); osg::StateSet* stateset = nullptr; @@ -455,12 +1341,12 @@ namespace SceneUtil if (mLightList.size() > maxLights) { // remove lights culled by this camera - for (LightManager::LightList::iterator it = mLightListCropped.begin(); it != mLightListCropped.end() && mLightListCropped.size() > maxLights; ) + for (auto it = mLightListCropped.begin(); it != mLightListCropped.end() && mLightListCropped.size() > maxLights; ) { osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius * 2; + bs._radius = bs._radius * 2.0; osg::CullingSet& cullingSet = stack.front(); if (cullingSet.isCulled(bs)) { @@ -481,8 +1367,7 @@ namespace SceneUtil } } - stateset = mLightManager->getLightListStateSet(mLightListCropped, cv->getTraversalNumber()); - + stateset = mLightManager->getLightListStateSet(mLightListCropped, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); cv->pushStateSet(stateset); return true; diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index 31b987d5b..3b9d12634 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -2,6 +2,10 @@ #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H #include +#include +#include +#include +#include #include @@ -9,6 +13,10 @@ #include #include +#include + +#include + namespace osgUtil { class CullVisitor; @@ -16,6 +24,15 @@ namespace osgUtil namespace SceneUtil { + class LightBuffer; + struct StateSetGenerator; + + 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 @@ -35,6 +52,8 @@ namespace SceneUtil int mId; + float mActorFade; + public: META_Node(SceneUtil, LightSource) @@ -54,10 +73,20 @@ namespace SceneUtil mRadius = radius; } + void setActorFade(float alpha) + { + mActorFade = alpha; + } + + float getActorFade() const + { + return mActorFade; + } + /// 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]; } @@ -83,10 +112,28 @@ namespace SceneUtil 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 +141,106 @@ 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(osg::Camera* camera, 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(); + + bool usingFFP() const; + + LightingMethod getLightingMethod() const; - const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix); + int getMaxLights() const; - typedef std::vector LightList; + int getMaxLightsInScene() const; - osg::ref_ptr getLightListStateSet(const LightList& lightList, unsigned int frameNum); + auto& getDummies() { return mDummies; } + + auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; } + + auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + + 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(); 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 LightStateSetMap = std::map>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; int mStartLight; - unsigned int mLightingMask; + size_t mLightingMask; + + osg::ref_ptr mSun; + + osg::ref_ptr mLightBuffers[2]; + + osg::Matrixf mSunlightBuffers[2]; + + // < Light ID , Buffer Index > + using LightIndexMap = std::unordered_map; + LightIndexMap mLightIndexMaps[2]; + + std::unique_ptr mStateSetGenerator; + + LightingMethod mLightingMethod; + + float mPointLightRadiusMultiplier; + float mPointLightFadeEnd; + float mPointLightFadeStart; + + int mMaxLights; + + SupportedMethods mSupported; + + static constexpr auto mMaxLightsLowerLimit = 2; + static constexpr auto mMaxLightsUpperLimit = 64; + static constexpr auto mFFPMaxLights = 8; + + static const std::unordered_map mLightingMethodSettingMap; }; /// To receive lighting, objects must be decorated by a LightListCallback. Light list callbacks must be added via @@ -180,12 +276,14 @@ namespace SceneUtil private: LightManager* mLightManager; - unsigned int mLastFrameNumber; + size_t mLastFrameNumber; LightManager::LightList mLightList; LightManager::LightList mLightListCropped; 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 e9be05908..6a1a1376e 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -58,7 +58,7 @@ 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 partsysMask, unsigned int lightMask, bool isExterior) { SceneUtil::FindByNameVisitor visitor("AttachLight"); node->accept(visitor); @@ -85,8 +85,9 @@ namespace SceneUtil attachTo = trans; } - osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior); + osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1)); 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 7096c38b2..1ddfa3d45 100644 --- a/components/sceneutil/lightutil.hpp +++ b/components/sceneutil/lightutil.hpp @@ -32,7 +32,7 @@ namespace SceneUtil /// @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 partsysMask, 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/pathgridutil.cpp b/components/sceneutil/pathgridutil.cpp index ed6894dfc..f37a8ba59 100644 --- a/components/sceneutil/pathgridutil.cpp +++ b/components/sceneutil/pathgridutil.cpp @@ -142,7 +142,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; diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index fa3c7d26d..1fe368572 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -263,6 +264,12 @@ osg::ref_ptr addEnchantedGlow(osg::ref_ptr node, Resourc bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture * texture, unsigned int level, unsigned int face, bool mipMapGeneration) { +#if OSG_VERSION_LESS_THAN(3, 6, 6) + // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028 + osg::ref_ptr extensions = osg::GLExtensions::Get(0, false); + if (extensions) + extensions->glRenderbufferStorageMultisampleCoverageNV = nullptr; +#endif unsigned int samples = 0; unsigned int colourSamples = 0; bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1; diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index fb25baad0..74abedd6d 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -243,7 +243,7 @@ osg::GraphicsContext* GraphicsWindowSDL2::findContext(osgViewer::View& view) return view.getCamera()->getGraphicsContext(); } - for (auto i = 0; i < view.getNumSlaves(); i++) + for (std::size_t i = 0; i < view.getNumSlaves(); i++) { if (view.getSlave(i)._camera->getGraphicsContext()) return view.getSlave(i)._camera->getGraphicsContext(); diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 99e2b5ae2..705301714 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -362,13 +362,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) { @@ -376,6 +373,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 @@ -388,6 +386,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/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 40c122b70..3a2ed5b8e 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -10,17 +10,28 @@ #include #include +#include #include #include namespace Shader { - void ShaderManager::setShaderPath(const std::string& path) + ShaderManager::ShaderManager() + : mLightingMethod(SceneUtil::LightingMethod::FFP) + { + } + + void ShaderManager::setShaderPath(const std::string &path) { mPath = path; } + void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + } + bool addLineDirectivesAfterConditionalBlocks(std::string& source) { for (size_t position = 0; position < source.length(); ) @@ -451,6 +462,8 @@ namespace Shader program->addShader(git->second); } + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 05434a09b..6d12a7078 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -11,16 +11,38 @@ #include +#include + +namespace Resource +{ + class SceneManager; +} + +namespace SceneUtil +{ + enum class LightingMethod; +} + namespace Shader { + enum class UBOBinding + { + LightBuffer + }; + /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's. /// @par Shader templates can get the value of a define with the syntax @define. class ShaderManager { public: + + ShaderManager(); + void setShaderPath(const std::string& path); + void setLightingMethod(SceneUtil::LightingMethod method); + typedef std::map DefineMap; /// Create or retrieve a shader instance. @@ -73,6 +95,8 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; + SceneUtil::LightingMethod mLightingMethod; + std::mutex mMutex; }; diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 25dce21b2..2e0bec023 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -50,6 +50,7 @@ namespace Shader , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) + , mConvertAlphaTestToAlphaToCoverage(false) , mTranslucentFramebuffer(false) , mShaderManager(shaderManager) , mImageManager(imageManager) @@ -277,8 +278,7 @@ namespace Shader const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); osg::StateSet::AttributeList removedAttributes; - osg::ref_ptr removedState; - if (removedState = getRemovedState(*stateset)) + if (osg::ref_ptr removedState = getRemovedState(*stateset)) removedAttributes = removedState->getAttributeList(); for (const auto& attributeMap : { attributes, removedAttributes }) { @@ -475,8 +475,7 @@ namespace Shader writableStateSet->removeAttribute(osg::StateAttribute::PROGRAM); - osg::ref_ptr removedState; - if (removedState = getRemovedState(*writableStateSet)) + if (osg::ref_ptr removedState = getRemovedState(*writableStateSet)) { // user data is normally shallow copied so shared with the original stateset osg::ref_ptr writableUserData; diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 7f184c70e..70b44935c 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -241,7 +241,7 @@ 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) +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) : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) @@ -256,7 +256,7 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour mChunkManagers.push_back(mChunkManager.get()); } -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize) +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize) : TerrainGrid(parent, storage, nodeMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index aba2dccf3..2ddea4204 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -20,9 +20,9 @@ namespace Terrain 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); - QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize); + QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize); ~QuadTreeWorld(); diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index cf8debc69..bbd0508ef 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -20,13 +20,13 @@ 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, int nodeMask) +TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, unsigned int nodeMask) : Terrain::World(parent, storage, nodeMask) , mNumSplits(4) { diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index dc9203466..3f44b18b0 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -14,8 +14,8 @@ namespace Terrain 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, Storage* storage, int nodeMask=~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/world.cpp b/components/terrain/world.cpp index 15ec72973..d1581724e 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -13,7 +13,7 @@ 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) @@ -49,7 +49,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mResourceSystem->addResourceManager(mTextureManager.get()); } -World::World(osg::Group* parent, Storage* storage, int nodeMask) +World::World(osg::Group* parent, Storage* storage, unsigned int nodeMask) : mStorage(storage) , mParent(parent) , mCompositeMapCamera(nullptr) diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index a4be57e8e..5797d894e 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -105,8 +105,8 @@ 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, Storage* storage, int nodeMask); + 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. diff --git a/components/vfs/bsaarchive.cpp b/components/vfs/bsaarchive.cpp index e6d779aab..90899ac61 100644 --- a/components/vfs/bsaarchive.cpp +++ b/components/vfs/bsaarchive.cpp @@ -32,7 +32,7 @@ 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; @@ -43,7 +43,7 @@ bool BsaArchive::contains(const std::string& file, char (*normalize_function)(ch { for (const auto& it : mResources) { - std::string ent = it.mInfo->name; + std::string ent = it.mInfo->name(); std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); if(file == ent) return true; diff --git a/docs/source/reference/modding/custom-models/index.rst b/docs/source/reference/modding/custom-models/index.rst new file mode 100644 index 000000000..8f575bf3c --- /dev/null +++ b/docs/source/reference/modding/custom-models/index.rst @@ -0,0 +1,19 @@ +############# +Custom Models +############# + +Custom models can be imported into OpenMW using a variety of formats. Below is a quick overview of supported formats, followed by separate articles with further look at the pipelines. + +* **COLLADA** has no license restrictions and is suitable for modding as well as standalone games based on the OpenMW engine. It supports static and animated models. While it doesn't yet work in all parts of the engine, work is being done to resolve the remaining limitations. + +* **OSG native** has no license restrictions, but currently supports only static, non-animated models. + +* **NIF** is the proprietary format used in the original Morrowind game. It supports static and animated models and everything else the format included in the original game. + +.. toctree:: + :caption: Table of Contents + :maxdepth: 1 + + pipeline-blender-collada + pipeline-blender-osgnative + pipeline-blender-nif diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst b/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst new file mode 100644 index 000000000..47096a83e --- /dev/null +++ b/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst @@ -0,0 +1,127 @@ +############################## +Blender to OpenMW with COLLADA +############################## + +Requirements +************ +To use the Blender to OpenMW pipeline via COLLADA, you will need the following. + +* `OpenMW 0.47 `_ or later +* `Blender 2.81 `_ or later. Latest confirmed, working version is Blender 2.91 +* `Better COLLADA Exporter `_ tuned for OpenMW +* A model you would like to export + + +Static Models +************* +Static models are those that don't have any animations included in the exported file. First, let's take a look at how the fundamental properties of a scene in Blender translate to a COLLADA model suitable for use in OpenMW. These apply the same to static and animated models. + +Location +======== + +Objects keep their visual location and origin they had in the original scene. + +Rotation +======== + +* Blender’s +Z axis is up axis in OpenMW +* Blender’s +Y axis is front axis in OpenMW +* Blender’s X axis is left-right axis in OpenMW + +Scale +===== + +Scale ratio between Blender and OpenMW is 70 to 1. This means 70 units in Blender translate to 1 m in OpenMW. + +However, a scale factor like this is impractical to work with. A better approach is to work with a scale of 1 Blender unit = 1 m and apply the 70 scale factor at export. The exporter will automatically scale all object, mesh, armature and animation data. + +Materials +========= + +OpenMW uses the classic, specular material setup and currently doesn't use the more mainstream `PBR `_ way. In Blender, the mesh needs a default material with a diffuse texture connected to the ``Base Color`` socket. This is enough for the material to be included in the exported COLLADA file. + +Additional texture types, such as specular or normal maps, will be automatically recognized and used by OpenMW. They need an identical base name as the diffuse texture, a suffix, and be in the same folder. How to enable this and what suffix is used for what texture type is covered in more detail in :doc:`../../modding/texture-modding/texture-basics`. + +Collision Shapes +================ + +In Blender, create an empty and name it ``collision``. Any mesh that is a child of this empty will be used for physics collision and will not be visible. There can be multiple child meshes under ``collision`` and they will all contribute to the collision shapes. The meshes themselves can have an arbitrary name, it's only the name of the empty that is important. The ``tcb`` command in OpenMW's in-game console will make the collision shapes visible and you will be able to inspect them. + +Exporter Settings +================= + +For static models, use the following exporter settings. Before export, select all objects you wish to include in the exported file and have the "Selected Objects" option enabled. Without this, the exporter could fail. + + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_exporter_static.jpg + :align: center + + +Animated Models +*************** + +Animated models are those where a hierarchy of bones, known as armature, deforms the mesh and makes things move. Besides the topics covered above, the following requirements apply. + +Armature +======== + +* A single armature per COLLADA file is advised to avoid any potential problems. +* There needs to be a single top-most bone in the armature’s hierarchy, where both the deformation and control bones fall under it. +* Not all bones need to be exported. By disabing the bone’s “Deform” property and using the corresponding option in the exporter, it is possible to export only the bones needed for animation. + + +Animations +========== + +Every action in Blender is exported as its own animation clip in COLLADA. Actions you don't wish to export need to have "-noexp" added to their name, with the corresponding option enabled in the exporter. + +Due to current limitations of the format and exporter, the keyframes of individual actions must not overlap with other actions. The keyframes need to be manually offset to a unique range on the timeline as shown in this example. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_animations_on_timeline.jpg + :align: center + +Textkeys +-------- + +The exported COLLADA file requires a corresponding textkeys file for OpenMW to properly read the animations. Textkeys is a .txt file containing animation definitions. Textkeys file is placed in the same folder as the model and uses a name matching the model. + + - ``OpenMWDude.dae`` + - ``OpenMWDude.txt`` + +Textkeys use a simple format as shown in the example. Name, start and stop values can be taken from the corresponding COLLADA file for each ````. + +.. code:: + + idle: start 0.03333333333333333 + idle: stop 2.033333333333333 + runforward: start 2.0666666666666664 + runforward: stop 3.0666666666666664 + runback: start 3.1 + runback: stop 4.1 + ... + + +Root Motion +=========== + +OpenMW can read the movement of the root (top-most in hierarchy) bone and use it to move objects in the game world. For this to work, the root bone must be animated to move through space. The root bone must, in its default pose, be alligned with the world. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_rig_root.jpg + :align: center + + +Exporter Settings +================= + +For animated models, use the following exporter settings. Before export, select all objects you wish to include in the exported file and have the "Selected Objects" option enabled. Without this, the exporter could fail. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_exporter_animated.jpg + :align: center + + + + + + + + diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-nif.rst b/docs/source/reference/modding/custom-models/pipeline-blender-nif.rst new file mode 100644 index 000000000..a1b7b0f70 --- /dev/null +++ b/docs/source/reference/modding/custom-models/pipeline-blender-nif.rst @@ -0,0 +1,17 @@ +########################## +Blender to OpenMW with NIF +########################## + +There is a lot of information available around the Internet on how to work with NIF files. We recommend you refer to https://www.niftools.org/ for more information. + +For Blender specifically, you will need the following requirements. + +Requirements +------------ +* `OpenMW `_ +* `Blender 2.8+ `_ +* Either `Niftools addon `_ +* Or `Morrowind Blender Plugin `_ +* A model you would like to export + + diff --git a/docs/source/reference/modding/texture-modding/native-mesh-format.rst b/docs/source/reference/modding/custom-models/pipeline-blender-osgnative.rst similarity index 77% rename from docs/source/reference/modding/texture-modding/native-mesh-format.rst rename to docs/source/reference/modding/custom-models/pipeline-blender-osgnative.rst index c2ddd26ba..95bc7e5ac 100644 --- a/docs/source/reference/modding/texture-modding/native-mesh-format.rst +++ b/docs/source/reference/modding/custom-models/pipeline-blender-osgnative.rst @@ -1,17 +1,9 @@ -################## -Native Mesh Format -################## +################################# +Blender to OpenMW with OSG native +################################# -This article explains how to export a model from Blender to OpenMW using the OSG model format. -Starting with OpenMW version 0.38 we can utilize the OSG native model format. -The OSG model format doesn't yet support all the features that NIF's support, -but works for basic models. For more details on the format, refer to -`this forum post `_. - -Previously, NIF files were the only way to get models into the game. -Unfortunately, the NIF format is proprietary, bloated, -and the available exporters are not in great shape. -For example, the Blender NIF exporter currently only works with the very old Blender 2.49. +This article explains how to export a model from Blender to OpenMW using the OSG model format. It supports only basic, static models. +For more details on the format, refer to `this forum post `_. Prerequisites ############# @@ -103,12 +95,5 @@ Using shaders/normal maps ######################### See :ref:`OSG Native Files` + -Conclusion -########## - -These are the basics of getting a textured, static model from Blender into the game. -In the future, we will want a way to add texture animations, -skeletal animations, separate collision shapes, -and some other features that are currently only available via NIF files. -We will likely add these features to the native OSG format after OpenMW 1.0. \ No newline at end of file diff --git a/docs/source/reference/modding/index.rst b/docs/source/reference/modding/index.rst index 69ec0a56a..df98137d8 100644 --- a/docs/source/reference/modding/index.rst +++ b/docs/source/reference/modding/index.rst @@ -22,6 +22,7 @@ about creating new content for OpenMW, please refer to mod-install settings/index texture-modding/index + custom-models/index font extended paths diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index cad04ab5c..00f99e47b 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -5,12 +5,13 @@ scaling factor -------------- :Type: floating point -:Range: > 0.0 +:Range: 0.5 to 8.0 :Default: 1.0 -This setting scales the GUI interface windows. +This setting scales GUI windows. A value of 1.0 results in the normal scale. Larger values are useful to increase the scale of the GUI for high resolution displays. -This setting can only be configured by editing the settings configuration file. + +This setting can be configured in the Interface section of Advanced tab of the launcher. font size --------- diff --git a/docs/source/reference/modding/settings/cells.rst b/docs/source/reference/modding/settings/cells.rst index 620b545ac..ab707faf8 100644 --- a/docs/source/reference/modding/settings/cells.rst +++ b/docs/source/reference/modding/settings/cells.rst @@ -175,7 +175,7 @@ pointers cache size ------------------- :Type: integer -:Range: >0 +:Range: 40 to 1000 :Default: 40 The count of object pointers that will be saved for a faster search by object ID. diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 4e1fe1318..878485b3b 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -439,3 +439,19 @@ graphic herbalism Some mods add harvestable container models. When this setting is enabled, activating a container using a harvestable model will visually harvest from it instead of opening the menu. When this setting is turned off or when activating a regular container, the menu will open as usual. + +allow actors to follow over water surface +--------------------- + +:Type: boolean +:Range: True/False +:Default: True + +If enabled actors will always find path over the water surface when following other actors. This makes OpenMW behaviour closer to the vanilla engine. + +If disabled actors without the ability to swim will not follow other actors to the water. + +.. note:: + Has effect only when Navigator is enabled. + +This setting can be controlled in Advanced tab of the launcher. diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index f0c37b738..ef97caeec 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -54,3 +54,57 @@ chunks near player will have size 4096x4096 game units. Larger chunks reduce CPU Smaller values do an opposite. This setting can only be configured by editing the settings configuration file. + +stomp mode +---------- + +:Type: integer +:Range: 0, 1, 2 +:Default: 2 + +Determines whether grass should respond to the player treading on it. + +.. list-table:: Modes + :header-rows: 1 + * - Mode number + - Meaning + * - 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. + +In MGE XE, which existing grass mods were made for, only the player's XY position was taken into account. +However, presumably due to a bug, jumping straight up would change the XY position, so grass *does* respond to the player jumping. +Levitating above grass in MGE XE still considers it stood-on, which can look bad. +OpenMW's height-aware system ensures grass does not act as if it's being stood on when the player levitates above it, but that means grass rapidly snaps back to its original position when the player jumps out of it. +Therefore, it is not recommended to use MGE XE's intensity constants if this setting is set to 2, i.e. :ref:`stomp intensity` should be 0 or 1 when :ref:`stomp mode` is 2. + +stomp intensity +--------------- + +:Type: integer +:Range: 0, 1, 2 +:Default: 1 + +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. + +.. list-table:: Presets + :header-rows: 1 + * - Preset number + - Range (Units) + - Distance (Units) + - Description + * - 2 + - 150 + - 60 + - MGE XE levels. Generally excessive/comical, but what existing mods were made with in mind. + * - 1 + - 80 + - 40 + - Reduced levels. Usually looks better. + * - 0 + - 50 + - 20 + - Gentle levels. diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index acc848299..03b7805de 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -40,7 +40,7 @@ Only affects objects that render with shaders (see 'force shaders' option). Always affects terrain. Leaving this option at its default makes the lighting compatible with Morrowind's fixed-function method, -but the lighting may appear dull and there might be colour shifts. +but the lighting may appear dull and there might be colour shifts. Setting this option to 'false' results in more dynamic lighting. auto use object normal maps @@ -148,6 +148,117 @@ By default, the fog becomes thicker proportionally to your distance from the cli 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. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +lighting method +--------------- + +:Type: string +:Range: legacy|shaders compatibility|shaders +:Default: default + +Sets the internal handling of light sources. + +'legacy' is restricted to 8 lights per object and emulates fixed function +pipeline compatible lighting. + +'shaders compatibility' removes the light limit controllable through :ref:`max +lights` and follows a modifed attenuation formula which can drastically reduce +light popping and seams. This mode also enables lighting on groundcover and a +configurable light fade. It is recommended to use this with older hardware and a +light limit closer to 8. Because of its wide range of compatibility it is set as +the default. + +'shaders' carries all of the benefits that 'shaders compatibility' does, but +uses a modern approach that allows for a higher :ref:`max lights` count with +little to no performance penalties on modern hardware. It is recommended to use +this mode when supported and where the GPU is not a bottleneck. On some weaker +devices, using this mode along with :ref:`force per pixel lighting` can carry +performance penalties. + +When enabled, groundcover lighting is forced to be vertex lighting, unless +normal maps are provided. This is due to some groundcover mods using the Z-Up +normals technique to avoid some common issues with shading. As a consequence, +per pixel lighting would give undesirable results. + +Note that the rendering will act as if you have 'force shaders' option enabled +when not set to 'legacy'. This means that shaders will be used to render all objects and +the terrain. + +light bounds multiplier +----------------------- + +:Type: float +:Range: 0.0-5.0 +:Default: 1.65 + +Controls the bounding sphere radius of point lights, which is used to determine +if an object should receive lighting from a particular light source. Note, this +has no direct effect on the overall illumination of lights. Larger multipliers +will allow for smoother transitions of light sources, but may require an +increase in :ref:`max lights` and thus carries a performance penalty. This +especially helps with abrupt light popping with handheld light sources such as +torches and lanterns. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +maximum light distance +---------------------- + +:Type: float +:Range: The whole range of 32-bit floating point +:Default: 8192 + +The maximum distance from the camera that lights will be illuminated, applies to +both interiors and exteriors. A lower distance will improve performance. Set +this to a non-positive value to disable fading. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +light fade start +---------------- + +:Type: float +:Range: 0.0-1.0 +:Default: 0.85 + +The fraction of the maximum distance at which lights will begin to fade away. +Tweaking it will make the transition proportionally more or less smooth. + +This setting has no effect if the :ref:`maximum light distance` is non-positive +or :ref:`lighting method` is 'legacy'. + +max lights +---------- + +:Type: integer +:Range: 2-64 +:Default: 8 + +Sets the maximum number of lights that each object can receive lighting from. +Increasing this too much can cause significant performance loss, especially if +:ref:`lighting method` is not set to 'shaders' or :ref:`force per pixel +lighting` is on. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +minimum interior brightness +------------------------ + +:Type: float +:Range: 0.0-1.0 +:Default: 0.08 + +Sets the minimum interior ambient brightness for interior cells when +:ref:`lighting method` is not 'legacy'. A consequence of the new lighting system +is that interiors will sometimes be darker since light sources now have sensible +fall-offs. A couple solutions are to either add more lights or increase their +radii to compensate, but these require content changes. For best results it is +recommended to set this to 0.0 to retain the colors that level designers +intended. If brighter interiors are wanted, however, this setting should be +increased. Note, it is advised to keep this number small (< 0.1) to avoid the +aforementioned changes in visuals. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + antialias alpha test --------------------------------------- diff --git a/docs/source/reference/modding/settings/sound.rst b/docs/source/reference/modding/settings/sound.rst index 895b919fb..4cc665582 100644 --- a/docs/source/reference/modding/settings/sound.rst +++ b/docs/source/reference/modding/settings/sound.rst @@ -5,7 +5,7 @@ device ------ :Type: string -:Range: +:Range: :Default: "" This setting determines which audio device to use. A blank or missing setting means to use the default device, @@ -13,7 +13,7 @@ which should usually be sufficient, but if you need to explicitly specify a devi The names of detected devices can be found in the openmw.log file in your configuration directory. -This setting can only be configured by editing the settings configuration file. +This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. master volume ------------- @@ -111,13 +111,13 @@ Enabling HRTF may also require an OpenAL Soft version greater than 1.17.0, and possibly some operating system configuration. A value of 0 disables HRTF processing, while a value of 1 explicitly enables HRTF processing. The default value is -1, which should enable the feature automatically for most users when possible. -This setting can only be configured by editing the settings configuration file. +This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. hrtf ---- :Type: string -:Range: +:Range: :Default: "" This setting specifies which HRTF profile to use when HRTF is enabled. Blank means use the default. @@ -125,4 +125,4 @@ This setting has no effect if HRTF is not enabled based on the hrtf enable setti Allowed values for this field are enumerated in openmw.log file is an HRTF enabled audio system is installed. The default value is empty, which uses the default profile. -This setting can only be configured by editing the settings configuration file. +This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. diff --git a/docs/source/reference/modding/texture-modding/index.rst b/docs/source/reference/modding/texture-modding/index.rst index 3e0b359ee..aa2802af5 100644 --- a/docs/source/reference/modding/texture-modding/index.rst +++ b/docs/source/reference/modding/texture-modding/index.rst @@ -13,4 +13,3 @@ to texture modding in OpenMW. texture-basics convert-bump-mapped-mods - native-mesh-format diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index fc00ae254..3842b8357 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -35,9 +35,13 @@ if(NOT OPENMW_USE_SYSTEM_BULLET) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) else() set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) - if(MSVC) - set(USE_MSVC_RUNTIME_LIBRARY_DLL ON CACHE BOOL "" FORCE) - endif() + endif() + + if(MSVC) + # this setting is badly named - having it off forces the static runtime library, + # but having it on does nothing, letting the defaults get used. + # OpenMW uses the defaults, and you can't mix and match. + set(USE_MSVC_RUNTIME_LIBRARY_DLL ON CACHE BOOL "" FORCE) endif() # master on 12 Mar 2021 diff --git a/extern/osg-ffmpeg-videoplayer/videostate.cpp b/extern/osg-ffmpeg-videoplayer/videostate.cpp index 7c4bddb01..c153aa14c 100644 --- a/extern/osg-ffmpeg-videoplayer/videostate.cpp +++ b/extern/osg-ffmpeg-videoplayer/videostate.cpp @@ -50,8 +50,9 @@ VideoState::VideoState() , av_sync_type(AV_SYNC_DEFAULT) , audio_st(nullptr) , video_st(nullptr), frame_last_pts(0.0) - , video_clock(0.0), sws_context(nullptr), pictq_size(0) - , pictq_rindex(0), pictq_windex(0) + , video_clock(0.0), sws_context(nullptr) + , sws_context_w(0), sws_context_h(0) + , pictq_size(0), pictq_rindex(0), pictq_windex(0) , mSeekRequested(false) , mSeekPos(0) , mVideoEnded(false) @@ -349,7 +350,10 @@ int VideoState::queue_picture(AVFrame *pFrame, double pts) vp->pts = pts; if (vp->set_dimensions(w, h) < 0) + { + this->pictq_mutex.unlock(); return -1; + } sws_scale(this->sws_context, pFrame->data, pFrame->linesize, 0, this->video_ctx->height, vp->rgbaFrame->data, vp->rgbaFrame->linesize); diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index babb5c28f..92c185413 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -457,6 +457,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 diff --git a/files/settings-default.cfg b/files/settings-default.cfg index d0af95661..f80b6d098 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -139,7 +139,7 @@ max composite geometry size = 4.0 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 @@ -364,6 +364,10 @@ 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 + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). @@ -442,6 +446,38 @@ apply lighting to environment maps = false # 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. @@ -1005,6 +1041,30 @@ 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 @@ -1020,6 +1080,18 @@ distance = 1 # A minimum size of groundcover chunk in cells (0.125, 0.25, 0.5, 1.0) min chunk size = 0.5 +# 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 + [Stereo] # Enable/disable stereo view. This setting is ignored in VR. diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 5314d9ff6..8b0bc10b1 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -18,6 +18,7 @@ set(SHADER_FILES terrain_vertex.glsl terrain_fragment.glsl lighting.glsl + lighting_util.glsl parallax.glsl s360_fragment.glsl s360_vertex.glsl diff --git a/files/shaders/groundcover_fragment.glsl b/files/shaders/groundcover_fragment.glsl index f25b7d487..d66963419 100644 --- a/files/shaders/groundcover_fragment.glsl +++ b/files/shaders/groundcover_fragment.glsl @@ -1,5 +1,9 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + #if @useGPUShader4 #extension GL_EXT_gpu_shader4: require #endif @@ -69,12 +73,7 @@ void main() vec3 diffuseLight, ambientLight; doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); lighting = diffuseLight + ambientLight; -#endif - -#if @clamp - lighting = clamp(lighting, vec3(0.0), vec3(1.0)); -#else - lighting = max(lighting, 0.0); + clampLightingResult(lighting); #endif gl_FragData[0].xyz *= lighting; diff --git a/files/shaders/groundcover_vertex.glsl b/files/shaders/groundcover_vertex.glsl index 407599eff..a12fb3d9d 100644 --- a/files/shaders/groundcover_vertex.glsl +++ b/files/shaders/groundcover_vertex.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 + #define GROUNDCOVER attribute vec4 aOffset; @@ -38,6 +46,15 @@ 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); @@ -53,14 +70,31 @@ vec2 groundcoverDisplacement(in vec3 worldpos, float h) 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)); - float d = length(worldpos - footPos.xyz); - vec3 stomp = vec3(0.0); - if (d < 150.0 && d > 0.0) - { - stomp = (60.0 / d - 0.4) * (worldpos - footPos.xyz); - } + 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.xy); + return clamp(0.02 * h, 0.0, 1.0) * (harmonics * displace + stomp); } mat4 rotation(in vec3 angle) @@ -131,6 +165,7 @@ void main(void) vec3 diffuseLight, ambientLight; doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); passLighting = diffuseLight + ambientLight; + clampLightingResult(passLighting); #endif #if (@shadows_enabled) diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index a2dcc758a..177017f82 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -1,15 +1,48 @@ -#define MAX_LIGHTS 8 +#include "lighting_util.glsl" -void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) +void perLightSun(out vec3 diffuseOut, vec3 viewPos, vec3 viewNormal) { - vec3 lightDir = gl_LightSource[lightIndex].position.xyz - viewPos * gl_LightSource[lightIndex].position.w; - float 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); + vec3 lightDir = normalize(lcalcPosition(0)); + float lambert = dot(viewNormal.xyz, lightDir); - ambientOut = gl_LightSource[lightIndex].ambient.xyz * illumination; +#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 + + diffuseOut = lcalcDiffuse(0).xyz * lambert; +} + +void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) +{ + 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) + { + ambientOut = vec3(0.0); + diffuseOut = vec3(0.0); + return; + } +#endif + + lightPos = normalize(lightPos); + + float illumination = lcalcIllumination(lightIndex, lightDistance); + ambientOut = lcalcAmbient(lightIndex) * illumination; + float lambert = dot(viewNormal.xyz, lightPos) * illumination; - float lambert = dot(viewNormal.xyz, lightDir) * illumination; #ifndef GROUNDCOVER lambert = max(lambert, 0.0); #else @@ -21,7 +54,8 @@ void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 vie } lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); #endif - diffuseOut = gl_LightSource[lightIndex].diffuse.xyz * lambert; + + diffuseOut = lcalcDiffuse(lightIndex) * lambert; } #if PER_PIXEL_LIGHTING @@ -31,31 +65,35 @@ void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 a #endif { vec3 ambientOut, diffuseOut; - // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. - perLight(ambientOut, diffuseOut, 0, viewPos, viewNormal); + + perLightSun(diffuseOut, viewPos, viewNormal); + ambientLight = gl_LightModel.ambient.xyz; #if PER_PIXEL_LIGHTING - diffuseLight = diffuseOut * shadowing - diffuseOut; + diffuseLight = diffuseOut * shadowing; #else shadowDiffuse = diffuseOut; - diffuseLight = -diffuseOut; + diffuseLight = vec3(0.0); #endif - ambientLight = gl_LightModel.ambient.xyz; - 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/nv_default_fragment.glsl b/files/shaders/nv_default_fragment.glsl index 03fa378a6..eadadba39 100644 --- a/files/shaders/nv_default_fragment.glsl +++ b/files/shaders/nv_default_fragment.glsl @@ -1,5 +1,9 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + #if @useGPUShader4 #extension GL_EXT_gpu_shader4: require #endif @@ -74,11 +78,7 @@ void main() #endif vec3 lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; -#if @clamp - lighting = clamp(lighting, vec3(0.0), vec3(1.0)); -#else - lighting = max(lighting, 0.0); -#endif + clampLightingResult(lighting); gl_FragData[0].xyz *= lighting; diff --git a/files/shaders/nv_default_vertex.glsl b/files/shaders/nv_default_vertex.glsl index 7c9d434f1..50f5daf25 100644 --- a/files/shaders/nv_default_vertex.glsl +++ b/files/shaders/nv_default_vertex.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 + #if @diffuseMap varying vec2 diffuseMapUV; #endif diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index 6b67be937..74929ec24 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -1,5 +1,9 @@ #version 120 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + #if @useGPUShader4 #extension GL_EXT_gpu_shader4: require #endif @@ -172,12 +176,7 @@ void main() doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); vec3 emission = getEmissionColor().xyz * emissiveMult; lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; -#endif - -#if @clamp - lighting = clamp(lighting, vec3(0.0), vec3(1.0)); -#else - lighting = max(lighting, 0.0); + clampLightingResult(lighting); #endif gl_FragData[0].xyz *= lighting; diff --git a/files/shaders/objects_vertex.glsl b/files/shaders/objects_vertex.glsl index bf5bdb40c..64c35a21c 100644 --- a/files/shaders/objects_vertex.glsl +++ b/files/shaders/objects_vertex.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 + #if @diffuseMap varying vec2 diffuseMapUV; #endif @@ -117,6 +125,7 @@ void main(void) 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 diff --git a/files/shaders/terrain_fragment.glsl b/files/shaders/terrain_fragment.glsl index 4736bb9b7..d9d4a6dc3 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; @@ -79,12 +87,7 @@ void main() vec3 diffuseLight, ambientLight; doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight); lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz; -#endif - -#if @clamp - lighting = clamp(lighting, vec3(0.0), vec3(1.0)); -#else - lighting = max(lighting, 0.0); + clampLightingResult(lighting); #endif gl_FragData[0].xyz *= lighting; diff --git a/files/shaders/terrain_vertex.glsl b/files/shaders/terrain_vertex.glsl index 17e204479..570628be0 100644 --- a/files/shaders/terrain_vertex.glsl +++ b/files/shaders/terrain_vertex.glsl @@ -1,5 +1,13 @@ #version 130 +#if @useUBO + #extension GL_ARB_uniform_buffer_object : require +#endif + +#if @useGPUShader4 + #extension GL_EXT_gpu_shader4: require +#endif + varying vec2 uv; varying float euclideanDepth; varying float linearDepth; @@ -39,6 +47,7 @@ void main(void) 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 diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl index db208ea42..24c93d11a 100644 --- a/files/shaders/water_fragment.glsl +++ b/files/shaders/water_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 + #define REFRACTION @refraction_enabled // Inspired by Blender GLSL Water by martinsh ( https://devlog-martinsh.blogspot.de/2012/07/waterundewater-shader-wip.html ) @@ -29,21 +37,20 @@ const float BUMP_RAIN = 2.5; const float REFL_BUMP = 0.10; // reflection distortion amount const float REFR_BUMP = 0.07; // refraction distortion amount +const float SCATTER_AMOUNT = 0.3; // amount of sunlight scattering +const vec3 SCATTER_COLOUR = vec3(0.0,1.0,0.95); // colour of sunlight scattering + const vec3 SUN_EXT = vec3(0.45, 0.55, 0.68); //sunlight extinction const float SPEC_HARDNESS = 256.0; // specular highlights hardness const float BUMP_SUPPRESS_DEPTH = 300.0; // at what water depth bumpmap will be suppressed for reflections and refractions (prevents artifacts at shores) -const float BUMP_SUPPRESS_DEPTH_SS = 1000.0; // modifier using screenspace depth (helps prevent same artifacts but at higher distances) const vec2 WIND_DIR = vec2(0.5f, -0.8f); const float WIND_SPEED = 0.2f; const vec3 WATER_COLOR = vec3(0.090195, 0.115685, 0.12745); -const float SCATTER_AMOUNT = 0.5; // amount of sunlight scattering -const vec3 SCATTER_COLOUR = WATER_COLOR * 8.0; // colour of sunlight scattering - // ---------------- rain ripples related stuff --------------------- const float RAIN_RIPPLE_GAPS = 5.0; @@ -143,7 +150,10 @@ uniform vec3 nodePosition; uniform float rainIntensity; +#define PER_PIXEL_LIGHTING 0 + #include "shadows_fragment.glsl" +#include "lighting.glsl" float frustumDepth; @@ -193,8 +203,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); @@ -219,10 +228,7 @@ void main(void) float depthSampleDistorted = linearizeDepth(texture2D(refractionDepthMap,screenCoords-screenCoordsOffset).x) * 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 - * max(1, depthSample / BUMP_SUPPRESS_DEPTH_SS)) // suppress more at distance - ,0 ,1); + screenCoordsOffset *= clamp(realWaterDepth / BUMP_SUPPRESS_DEPTH,0,1); #endif // reflection vec3 reflection = texture2D(reflectionMap, screenCoords + screenCoordsOffset).rgb; @@ -232,6 +238,8 @@ void main(void) vec3 waterColor = WATER_COLOR * sunFade; + vec4 sunSpec = lcalcSpecular(0); + #if REFRACTION // refraction vec3 refraction = texture2D(refractionMap, screenCoords - screenCoordsOffset).rgb; @@ -240,11 +248,7 @@ void main(void) if (cameraPos.z < 0.0) refraction = clamp(refraction * 1.5, 0.0, 1.0); else - { - vec3 refractionA = mix(refraction, waterColor, clamp(depthSampleDistorted/VISIBILITY, 0.0, 1.0)); - vec3 refractionB = mix(refraction, waterColor, clamp(realWaterDepth/VISIBILITY, 0.0, 1.0)); - refraction = mix(refractionA, refractionB, 0.8); - } + refraction = mix(refraction, waterColor, clamp(depthSampleDistorted/VISIBILITY, 0.0, 1.0)); // sunlight scattering // normal for sunlight scattering @@ -255,11 +259,11 @@ void main(void) 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 = 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; + gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.2; gl_FragData[0].w = 1.0; #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 + vec3(rainRipple.w) * 0.7; + 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 diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 6a3a60391..7ac94dfba 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -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 + + + @@ -273,6 +283,36 @@ + + + + + + Lighting Method: + + + + + + + + legacy + + + + + shaders compatibility + + + + + shaders + + + + + + @@ -467,6 +507,150 @@ + + + Audio + + + + + + + + Audio Device + + + Select your preferred audio device. + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + + + HRTF + + + This setting controls HRTF, which simulates 3D sound on stereo systems. + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Automatic + + + + + Off + + + + + On + + + + + + + + + + + + HRTF Profile + + + Select your preferred HRTF profile. + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + Camera @@ -662,9 +846,58 @@ + + + + + + + + <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 + + + + + + + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> diff --git a/files/ui/wizard/methodselectionpage.ui b/files/ui/wizard/methodselectionpage.ui index 4d4d66bad..c2dd26052 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/wizard/icons/tango/48x48/dollar.png b/files/wizard/icons/tango/48x48/dollar.png new file mode 100644 index 000000000..a14ba2505 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 99623e985..7dbb8fe08 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